Разработка кастомного модуля Craft CMS
Модули в Craft CMS — это Yii2-модули, встраиваемые в приложение без публикации в Plugin Store. Используются для кастомной бизнес-логики конкретного проекта: уникальные элементы, сервисы, Twig-расширения, консольные команды.
Разница между модулем и плагином
Модуль — код в modules/ текущего проекта. Не распространяется, не имеет версии для Plugin Store, не требует лицензии. Подходит для логики, специфичной для одного сайта.
Плагин — пакет Composer с composer.json, CHANGELOG.md, иконкой. Можно публиковать и продавать. Подходит для переиспользуемой функциональности.
Структура модуля
modules/
└── sitecustom/
├── SiteCustomModule.php # основной класс
├── services/
│ ├── ProductService.php
│ └── SearchService.php
├── controllers/
│ └── ApiController.php
├── variables/
│ └── SiteVariable.php # Twig-переменные
├── twigextensions/
│ └── SiteTwigExtension.php
└── console/
└── controllers/
└── SyncController.php
Основной класс модуля
// modules/sitecustom/SiteCustomModule.php
namespace modules\sitecustom;
use Craft;
use craft\events\RegisterComponentTypesEvent;
use craft\services\Elements;
use craft\web\twig\variables\CraftVariable;
use modules\sitecustom\services\ProductService;
use modules\sitecustom\variables\SiteVariable;
use modules\sitecustom\twigextensions\SiteTwigExtension;
use yii\base\Event;
use yii\base\Module;
class SiteCustomModule extends Module
{
public static SiteCustomModule $instance;
public function init(): void
{
parent::init();
self::$instance = $this;
// Устанавливаем псевдоним для путей
Craft::setAlias('@modules/sitecustom', __DIR__);
// Регистрируем сервисы
$this->setComponents([
'products' => ProductService::class,
'search' => SearchService::class,
]);
// Регистрируем Twig-переменные
Event::on(
CraftVariable::class,
CraftVariable::EVENT_INIT,
function (Event $event) {
$event->sender->set('site', SiteVariable::class);
}
);
// Регистрируем Twig-расширения
if (Craft::$app->request->getIsSiteRequest()) {
Craft::$app->view->registerTwigExtension(new SiteTwigExtension());
}
}
}
Регистрация в config/app.php:
return [
'modules' => [
'site-custom' => \modules\sitecustom\SiteCustomModule::class,
],
'bootstrap' => ['site-custom'],
];
Сервис с бизнес-логикой
// modules/sitecustom/services/ProductService.php
namespace modules\sitecustom\services;
use craft\base\Component;
use craft\elements\Entry;
use craft\helpers\ElementHelper;
class ProductService extends Component
{
public function getFeaturedProducts(int $limit = 6): array
{
return Entry::find()
->section('products')
->featured(true)
->inStock(true)
->orderBy('sortOrder asc, postDate desc')
->limit($limit)
->with(['featuredImage', 'categories'])
->all();
}
public function getRelated(Entry $product, int $limit = 4): array
{
return Entry::find()
->section('products')
->relatedTo([
'element' => $product->categories->all(),
'field' => 'categories',
])
->id('not ' . $product->id)
->limit($limit)
->all();
}
public function updateStock(int $entryId, int $quantity): bool
{
$entry = Entry::find()->id($entryId)->one();
if (!$entry) return false;
$entry->setFieldValue('stockQuantity', $quantity);
return \Craft::$app->elements->saveElement($entry);
}
}
Twig-переменные
// modules/sitecustom/variables/SiteVariable.php
namespace modules\sitecustom\variables;
use modules\sitecustom\SiteCustomModule;
class SiteVariable
{
public function featuredProducts(int $limit = 6): array
{
return SiteCustomModule::$instance->products->getFeaturedProducts($limit);
}
public function cartCount(): int
{
return \Craft::$app->session->get('cart_count', 0);
}
}
В Twig:
{% set products = craft.site.featuredProducts(4) %}
{% for product in products %}
{% include '_components/product-card' with { product: product } %}
{% endfor %}
Twig-расширения (фильтры и функции)
namespace modules\sitecustom\twigextensions;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class SiteTwigExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('formatPrice', [$this, 'formatPrice']),
new TwigFilter('phoneFormat', [$this, 'formatPhone']),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('svg', [$this, 'inlineSvg'], ['is_safe' => ['html']]),
];
}
public function formatPrice(float $price, string $currency = 'RUB'): string
{
return number_format($price, 0, '.', ' ') . ' ' . $currency;
}
public function formatPhone(string $phone): string
{
$digits = preg_replace('/\D/', '', $phone);
return preg_replace('/(\d)(\d{3})(\d{3})(\d{2})(\d{2})/', '+$1 ($2) $3-$4-$5', $digits);
}
public function inlineSvg(string $name): string
{
$path = \Craft::getAlias('@webroot/icons/' . $name . '.svg');
return file_exists($path) ? file_get_contents($path) : '';
}
}
Консольные команды
// modules/sitecustom/console/controllers/SyncController.php
namespace modules\sitecustom\console\controllers;
use craft\console\Controller;
use yii\console\ExitCode;
class SyncController extends Controller
{
public function actionProducts(): int
{
$this->stdout("Синхронизация товаров...\n");
// Логика синхронизации с внешним API
$count = SiteCustomModule::$instance->products->syncFromExternalApi();
$this->stdout("Синхронизировано: {$count} товаров\n", Console::FG_GREEN);
return ExitCode::OK;
}
}
php craft site-custom/sync/products
Разработка базового модуля с 1–2 сервисами и Twig-переменными — 1–2 дня. Полный модуль со сложной бизнес-логикой и консольными командами — 3–7 дней.







