Розробка кастомного модуля 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 днів.







