Разработка кастомного пакета (Package) Concrete CMS
Пакет (Package) в Concrete CMS — это контейнер, объединяющий темы, блоки, атрибуты, типы страниц, одиночные страницы, Express-объекты и задачи автоматизации. Правильно структурированный пакет позволяет установить всё необходимое одной командой и легко переносить решение между инсталляциями.
Структура пакета
packages/my-package/
controller.php # главный контроллер пакета
blocks/
feature-card/ # кастомный блок
controller.php
db.xml
add.php
edit.php
view.php
themes/
my-theme/ # тема
attributes/
color_picker/ # кастомный тип атрибута
controller.php
single_pages/
dashboard/
my_package/
settings.php # страница настроек в Dashboard
elements/
my_package/
settings_form.php
jobs/
sync_products.php # автоматическая задача (Cron Job)
config/
generated_overrides/
mail/
order_notification.php # шаблон email
src/
Entity/ # Doctrine-сущности
Order.php
Repository/
OrderRepository.php
Service/
OrderService.php
db.xml # общие таблицы пакета
controller.php — сердце пакета
<?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('Полный функциональный пакет для корпоративного сайта'); }
public function on_start(): void {
// Регистрация сервисов, автолоадер, роуты
$this->app->make(\Concrete\Package\MyPackage\Routing\RouteRegistrar::class)->register();
// Регистрация Doctrine-сущностей
$this->app->make('Concrete\Core\Foundation\Service\ProviderList')
->registerProvider(\Concrete\Package\MyPackage\Provider\ServiceProvider::class);
}
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 {
// Блоки
$this->installBlock('feature-card', $pkg);
$this->installBlock('team-member', $pkg);
$this->installBlock('testimonial', $pkg);
// Типы страниц
$this->installPageType('service-detail', 'Service Detail', $pkg);
$this->installPageType('team-member', 'Team Member', $pkg);
// Атрибуты страниц
$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-страница настроек
$sp = SinglePage::add('/dashboard/my_package', $pkg);
if ($sp) { $sp->update(['cName' => 'My Package', 'cDescription' => 'Настройки']); }
$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();
// Опционально: удалить таблицы пакета
$db = $this->app->make('database')->connection();
$db->executeStatement('DROP TABLE IF EXISTS MyPackageOrders');
}
}
Doctrine-сущности
Concrete CMS 9+ использует Doctrine ORM. Сущности пакета:
<?php
// src/Entity/Order.php
namespace Concrete\Package\MyPackage\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Concrete\Package\MyPackage\Repository\OrderRepository")
* @ORM\Table(name="MyPackageOrders")
*/
class Order {
/** @ORM\Id @ORM\Column(type="integer") @ORM\GeneratedValue */
private int $id;
/** @ORM\Column(type="string", length=255) */
private string $customerEmail;
/** @ORM\Column(type="decimal", precision=10, scale=2) */
private float $total;
/** @ORM\Column(type="string", length=50) */
private string $status = 'pending';
/** @ORM\Column(type="datetime") */
private \DateTime $createdAt;
// getters/setters...
}
<?php
// Provider/ServiceProvider.php — регистрация сущностей
namespace Concrete\Package\MyPackage\Provider;
use Concrete\Core\Foundation\Service\Provider;
class ServiceProvider extends Provider {
public function register(): void {
$this->app->make('Concrete\Core\Database\DatabaseStructureManager')
->installDatabase($this->app->make(\Concrete\Package\MyPackage\Controller::class)->getPackageEntity());
}
}
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('Синхронизация товаров с внешним API'); }
public function run(): string {
$service = $this->app->make(\Concrete\Package\MyPackage\Service\ProductSyncService::class);
$count = $service->sync();
return t('Синхронизировано: %d товаров', $count);
}
}
Job запускается через Dashboard → System → Automated Jobs или по cron:
*/30 * * * * /usr/bin/php /var/www/mysite/concrete/bin/concrete5 c5:job:run sync_products
Dashboard-страница настроек
<?php
// single_pages/dashboard/my_package/settings.php
namespace Concrete\Package\MyPackage\Controller\SinglePage\Dashboard\MyPackage;
use Concrete\Core\Page\Controller\DashboardPageController;
use Concrete\Core\Http\Request;
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('Недействительный токен'));
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('Настройки сохранены'));
$this->redirect('/dashboard/my_package/settings');
}
}
REST API для пакета
Concrete CMS 9 поддерживает регистрацию кастомных API-эндпоинтов:
// Routing/RouteRegistrar.php
namespace Concrete\Package\MyPackage\Routing;
use Concrete\Core\Routing\RouteListInterface;
use Concrete\Core\Routing\Router;
class RouteRegistrar implements RouteListInterface {
public function loadRoutes(Router $router): void {
$router->buildGroup()
->setPrefix('/api/v1/my-package')
->setNamespace('Concrete\Package\MyPackage\Controller\Api')
->routes(function($groupRouter) {
$groupRouter->get('/products', 'Products::index');
$groupRouter->post('/products', 'Products::store');
$groupRouter->get('/products/{id}', 'Products::show');
});
}
}
Миграции структуры БД
Для изменений схемы между версиями используют db.xml с инкрементальными изменениями. Concrete CMS сравнивает текущую схему с db.xml при upgrade:
<!-- db.xml v2 — добавлено поле notes -->
<table name="MyPackageOrders">
<!-- ... существующие поля ... -->
<field name="notes" type="X2"/>
<field name="updated_at" type="T"/>
</table>
Упаковка и распространение
# Создать архив пакета для Concrete Marketplace
cd /var/www/mysite
zip -r my-package-2.1.0.zip packages/my-package \
--exclude "*.git*" \
--exclude "node_modules/*" \
--exclude "*.map"
Сроки разработки пакета
| Компонент | Оценка |
|---|---|
| Контроллер пакета + установка | 4–8 ч |
| 3–5 кастомных блоков | 3–6 дней |
| Тема с 8–12 типами страниц | 2–4 недели |
| Doctrine-сущности + CRUD | 2–4 дня |
| Dashboard-страница + настройки | 1–2 дня |
| REST API (3–5 эндпоинтов) | 2–3 дня |
| Cron Jobs (1–3 задачи) | 4–8 ч |
| Полный корпоративный пакет | 8–16 недель |







