Concrete CMS Custom Package Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Concrete CMS Custom Package Development

A package in Concrete CMS is a container combining themes, blocks, attributes, page types, single pages, Express objects, and automation tasks. Properly structured package allows installing everything in one command and easily move solution between installations.

Package Structure

packages/my-package/
  controller.php                    # main package controller
  blocks/
    feature-card/                   # custom block
      controller.php
      db.xml
      add.php
      edit.php
      view.php
  themes/
    my-theme/                       # theme
  attributes/
    color_picker/                   # custom attribute type
      controller.php
  single_pages/
    dashboard/
      my_package/
        settings.php                # Dashboard settings page
  elements/
    my_package/
      settings_form.php
  jobs/
    sync_products.php               # automated task (Cron Job)
  config/
    generated_overrides/
  mail/
    order_notification.php          # email template
  src/
    Entity/                         # Doctrine entities
      Order.php
    Repository/
      OrderRepository.php
    Service/
      OrderService.php
  db.xml                            # package common tables

controller.php — Package Heart

<?php
namespace Concrete\Package\MyPackage;

use Concrete\Core\Package\Package;
use Concrete\Core\Page\Single as SinglePage;
use Concrete\Core\Block\BlockType\BlockType;
use Concrete\Core\Page\Type\Type as PageType;
use Concrete\Core\Attribute\Type as AttributeType;
use Concrete\Core\Job\Job;

defined('C5_EXECUTE') or die('Access Denied.');

class Controller extends Package {

    protected string $pkgHandle          = 'my-package';
    protected string $appVersionRequired = '9.0.0';
    protected string $pkgVersion         = '2.1.0';

    public function getPackageName(): string        { return t('My Package'); }
    public function getPackageDescription(): string { return t('Full-featured package for corporate site'); }

    public function on_start(): void {
        // Service registration, autoloader, routes
        $this->app->make(\Concrete\Package\MyPackage\Routing\RouteRegistrar::class)->register();
    }

    public function install(): void {
        $pkg = parent::install();
        $this->installOrUpgrade($pkg);
    }

    public function upgrade(): void {
        parent::upgrade();
        $pkg = $this->getPackageEntity();
        $this->installOrUpgrade($pkg);
    }

    private function installOrUpgrade(\Concrete\Core\Entity\Package $pkg): void {
        // Blocks
        $this->installBlock('feature-card', $pkg);
        $this->installBlock('team-member', $pkg);
        $this->installBlock('testimonial', $pkg);

        // Page types
        $this->installPageType('service-detail', 'Service Detail', $pkg);
        $this->installPageType('team-member', 'Team Member', $pkg);

        // Page attributes
        $this->installPageAttribute('hero_image', 'image', 'Hero Image', $pkg);
        $this->installPageAttribute('intro_text', 'text', 'Intro Text', $pkg);
        $this->installPageAttribute('meta_description', 'textarea', 'Meta Description', $pkg);
        $this->installPageAttribute('show_in_nav', 'boolean', 'Show in Navigation', $pkg);

        // Dashboard settings page
        $sp = SinglePage::add('/dashboard/my_package', $pkg);
        if ($sp) { $sp->update(['cName' => 'My Package', 'cDescription' => 'Settings']); }

        $sp = SinglePage::add('/dashboard/my_package/settings', $pkg);
        if ($sp) { $sp->update(['cName' => 'Settings']); }

        // Cron Job
        Job::installByPackage('sync_products', $pkg);
    }

    private function installBlock(string $handle, $pkg): void {
        if (!\Concrete\Core\Block\BlockType\BlockType::getByHandle($handle)) {
            BlockType::installBlockTypeFromPackage($handle, $pkg);
        }
    }

    private function installPageType(string $handle, string $name, $pkg): void {
        if (!PageType::getByHandle($handle)) {
            PageType::add([
                'ptHandle'            => $handle,
                'ptName'              => $name,
                'ptIsFrequentlyAdded' => 0,
                'ptLaunchInComposer'  => 1,
            ], $pkg);
        }
    }

    private function installPageAttribute(string $handle, string $type, string $name, $pkg): void {
        $at = AttributeType::getByHandle($type);
        $ak = \Concrete\Core\Attribute\Key\CollectionKey::getByHandle($handle);
        if (!$ak) {
            \Concrete\Core\Attribute\Key\CollectionKey::add($at, [
                'akHandle' => $handle,
                'akName'   => $name,
            ], $pkg);
        }
    }

    public function uninstall(): void {
        parent::uninstall();
    }
}

Cron Job

<?php
// jobs/sync_products.php
namespace Concrete\Package\MyPackage\Job;

use Concrete\Core\Job\Job;

class SyncProducts extends Job {

    public function getJobName(): string        { return t('Sync Products'); }
    public function getJobDescription(): string { return t('Sync products with external API'); }

    public function run(): string {
        $service = $this->app->make(\Concrete\Package\MyPackage\Service\ProductSyncService::class);
        $count   = $service->sync();
        return t('Synced: %d products', $count);
    }
}

Job runs via Dashboard → System → Automated Jobs or cron:

*/30 * * * * /usr/bin/php /var/www/mysite/concrete/bin/concrete5 c5:job:run sync_products

Dashboard Settings Page

<?php
// single_pages/dashboard/my_package/settings.php

class Settings extends DashboardPageController {

    public function view(): void {
        $config = $this->app->make('config');
        $this->set('api_key', $config->get('my_package.api_key', ''));
        $this->set('sync_interval', $config->get('my_package.sync_interval', 30));
    }

    public function save(): void {
        $token = $this->app->make('token');
        if (!$token->validate('my_package_settings')) {
            $this->error->add(t('Invalid token'));
            return $this->view();
        }

        $config = $this->app->make('config');
        $config->save('my_package.api_key', $this->request->get('api_key'));
        $config->save('my_package.sync_interval', (int)$this->request->get('sync_interval'));

        $this->flash('success', t('Settings saved'));
        $this->redirect('/dashboard/my_package/settings');
    }
}

Package Development Timeline

Component Estimate
Package controller + installation 4–8 h
3–5 custom blocks 3–6 days
Theme with 8–12 page types 2–4 weeks
Doctrine entities + CRUD 2–4 days
Dashboard page + settings 1–2 days
REST API (3–5 endpoints) 2–3 days
Cron Jobs (1–3 tasks) 4–8 h
Full corporate package 8–16 weeks