Разработка кастомного плагина Craft CMS
Плагины для Craft CMS — это пакеты Composer, расширяющие функциональность CMS через официальное API: кастомные типы элементов, fieldtype-поля, виджеты дашборда, утилиты, новые разделы в CP. В отличие от модулей, плагины можно публиковать в Plugin Store или переиспользовать между проектами.
Структура плагина
craft-myplugin/
├── composer.json
├── CHANGELOG.md
├── src/
│ ├── Plugin.php # главный класс
│ ├── models/
│ │ └── Settings.php # модель настроек
│ ├── services/
│ │ └── MyService.php
│ ├── fields/
│ │ └── ColorSwatchField.php
│ ├── elements/
│ │ └── ProductElement.php
│ ├── records/
│ │ └── ProductRecord.php # ActiveRecord Yii2
│ ├── migrations/
│ │ └── Install.php
│ ├── controllers/
│ │ └── ProductsController.php
│ ├── templates/
│ │ ├── _cp/
│ │ │ └── settings.twig
│ │ └── _fields/
│ │ └── color-swatch-input.twig
│ ├── web/
│ │ └── assets/
│ │ └── CpAsset.php
│ └── translations/
│ └── en/
│ └── myplugin.php
└── icon.svg
composer.json
{
"name": "myvendor/craft-myplugin",
"description": "My custom Craft CMS plugin",
"type": "craft-plugin",
"minimum-stability": "dev",
"require": {
"craftcms/cms": "^4.0.0"
},
"autoload": {
"psr-4": { "myvendor\\myplugin\\": "src/" }
},
"extra": {
"handle": "myplugin",
"name": "My Plugin",
"version": "1.0.0",
"schemaVersion": "1.0.0",
"class": "myvendor\\myplugin\\Plugin",
"developer": "My Company",
"developerUrl": "https://mycompany.com",
"documentationUrl": "https://mycompany.com/plugins/myplugin",
"changelogUrl": "https://raw.githubusercontent.com/myvendor/craft-myplugin/main/CHANGELOG.md",
"hasCpSettings": true,
"hasCpSection": true
}
}
Главный класс плагина
// src/Plugin.php
namespace myvendor\myplugin;
use Craft;
use craft\base\Plugin as BasePlugin;
use craft\events\RegisterComponentTypesEvent;
use craft\services\Fields;
use myvendor\myplugin\fields\ColorSwatchField;
use myvendor\myplugin\services\MyService;
use myvendor\myplugin\models\Settings;
use yii\base\Event;
class Plugin extends BasePlugin
{
public string $schemaVersion = '1.0.0';
public bool $hasCpSettings = true;
public bool $hasCpSection = true;
public static Plugin $instance;
public function init(): void
{
parent::init();
self::$instance = $this;
$this->setComponents([
'myService' => MyService::class,
]);
// Регистрация кастомного типа поля
Event::on(
Fields::class,
Fields::EVENT_REGISTER_FIELD_TYPES,
function (RegisterComponentTypesEvent $event) {
$event->types[] = ColorSwatchField::class;
}
);
Craft::info("Plugin {$this->name} loaded", __METHOD__);
}
protected function createSettingsModel(): ?Model
{
return new Settings();
}
protected function settingsHtml(): ?string
{
return Craft::$app->view->renderTemplate('myplugin/_cp/settings', [
'settings' => $this->getSettings(),
]);
}
public function getCpNavItem(): ?array
{
return [
...parent::getCpNavItem(),
'label' => 'My Plugin',
'url' => 'myplugin',
'icon' => '@myvendor/myplugin/icon.svg',
'subnav' => [
'dashboard' => ['label' => 'Dashboard', 'url' => 'myplugin'],
'settings' => ['label' => 'Settings', 'url' => 'myplugin/settings'],
],
];
}
}
Кастомный тип поля
// src/fields/ColorSwatchField.php
namespace myvendor\myplugin\fields;
use craft\base\Field;
use craft\base\ElementInterface;
class ColorSwatchField extends Field
{
public static function displayName(): string
{
return \Craft::t('myplugin', 'Color Swatch');
}
public static function phpType(): string
{
return 'string';
}
public function getInputHtml(mixed $value, ?ElementInterface $element): string
{
return \Craft::$app->view->renderTemplate(
'myplugin/_fields/color-swatch-input',
[
'id' => $this->getInputId(),
'name' => $this->handle,
'value' => $value,
'swatches' => $this->swatches,
]
);
}
public function normalizeValue(mixed $value, ?ElementInterface $element): mixed
{
// Нормализация: убираем # если нет
return ltrim($value ?? '', '#') ? '#' . ltrim($value, '#') : null;
}
protected function defineRules(): array
{
return array_merge(parent::defineRules(), [
[['value'], 'match', 'pattern' => '/^#[0-9A-Fa-f]{6}$/'],
]);
}
}
Кастомный тип элемента
Кастомные элементы (аналог Entry, User, Asset) наследуют от craft\base\Element:
// src/elements/ProductElement.php — ключевые методы
class ProductElement extends Element
{
public static function displayName(): string { return 'Product'; }
public static function pluralDisplayName(): string { return 'Products'; }
// Условие поиска
public static function find(): ElementQueryInterface
{
return new ProductQuery(static::class);
}
// Поля для индексации
protected static function defineSearchableAttributes(): array
{
return ['sku', 'title', 'description'];
}
// Сохранение дополнительных данных (в своей таблице)
public function afterSave(bool $isNew): void
{
if ($isNew) {
\Craft::$app->db->createCommand()
->insert('{{%myplugin_products}}', [
'id' => $this->id,
'sku' => $this->sku,
'price' => $this->price,
])
->execute();
} else {
\Craft::$app->db->createCommand()
->update('{{%myplugin_products}}', [
'sku' => $this->sku,
'price' => $this->price,
], ['id' => $this->id])
->execute();
}
parent::afterSave($isNew);
}
}
Миграции
// src/migrations/Install.php
namespace myvendor\myplugin\migrations;
use craft\db\Migration;
class Install extends Migration
{
public function safeUp(): bool
{
if (!$this->db->tableExists('{{%myplugin_products}}')) {
$this->createTable('{{%myplugin_products}}', [
'id' => $this->primaryKey(),
'sku' => $this->string()->notNull(),
'price' => $this->decimal(10, 2)->notNull()->defaultValue(0),
'stockCount' => $this->integer()->notNull()->defaultValue(0),
'dateCreated' => $this->dateTime()->notNull(),
'dateUpdated' => $this->dateTime()->notNull(),
'uid' => $this->uid(),
]);
$this->addForeignKey(
null, '{{%myplugin_products}}', 'id',
'{{%elements}}', 'id', 'CASCADE'
);
}
return true;
}
public function safeDown(): bool
{
$this->dropTableIfExists('{{%myplugin_products}}');
return true;
}
}
Тестирование
# Codeception для Craft
composer require craftcms/craft-pest --dev
./vendor/bin/pest
Сроки разработки
| Тип плагина | Время |
|---|---|
| Простое поле (FieldType) | 2–3 дня |
| Сервис + CP секция | 3–5 дней |
| Кастомный тип элемента | 5–10 дней |
| Полноценный плагин (элемент + CP + API) | 2–4 недели |
| Публикация в Plugin Store | +1–2 дня |







