Дата: 2025-11-10
Версия Drupal: 10.x
7 ключевых концепций:
Что взять для ЦИФРА:
- ✅ Entity API (всё - entity с единым интерфейсом)
- ✅ Plugin System (расширяемость через аннотации)
- ✅ Configuration as Code (YAML export/import)
- ✅ Field API (attach поля к любой entity)
- ✅ DI Container (чистая архитектура)
В Drupal всё контент - это Entity:
- Users (пользователи)
- Nodes (статьи, страницы)
- Comments (комментарии)
- Media (медиа файлы)
- Taxonomy Terms (категории)
- Custom entities (ваши сущности)
// Одинаковый API для всех entity
$user = User::load(1);
$node = Node::load(1);
$comment = Comment::load(1);
// Одинаковые методы
$user->save();
$node->delete();
$comment->label();
1. Content Entity (контент с полями)
- Хранятся в БД
- Поддерживают поля (Field API)
- Переводы (multilingual)
- Ревизии (versioning)
2. Config Entity (конфигурация)
- Хранятся в YAML
- Экспортируются/импортируются
- Нет переводов
- Нет ревизий
<?php
namespace Drupal\mymodule\Entity;
use Drupal\Core\Entity\ContentEntityBase;
/**
* @ContentEntityType(
* id = "product",
* label = @Translation("Product"),
* base_table = "product",
* entity_keys = {
* "id" = "id",
* "label" = "name"
* }
* )
*/
class Product extends ContentEntityBase {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
// Name field
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setRequired(TRUE);
// Price field
$fields['price'] = BaseFieldDefinition::create('integer')
->setLabel(t('Price'))
->setDefaultValue(0);
return $fields;
}
}
Результат:
- ✅ Таблица product создаётся автоматически
- ✅ CRUD API доступен сразу
- ✅ Можно добавлять поля через Field API
- ✅ Forms генерируются автоматически
/**
* @ContentEntityType(
* handlers = {
* "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "form" = {
* "default" = "Drupal\mymodule\Form\ProductForm",
* "edit" = "Drupal\mymodule\Form\ProductForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider"
* }
* }
* )
*/
Handlers - это Strategy Pattern:
- storage - как хранить (SQL, NoSQL, API)
- view_builder - как отображать
- form - как редактировать
- route_provider - какие маршруты создать
Field API - система динамических полей.
Entity ← Attach Fields
Любая entity может иметь любые поля без изменения кода.
// Все встроенные типы полей
- text (текст)
- integer (целое число)
- decimal (десятичное)
- boolean (да/нет)
- email (email)
- datetime (дата/время)
- entity_reference (ссылка на другую entity)
- image (изображение)
- file (файл)
- list (выпадающий список)
- link (ссылка)
/admin/structure/types/manage/article/fields
→ Add field
→ Выбираете тип (text, image, reference)
→ Настраиваете
→ Save
Поле сразу доступно!
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
// 1. Field Storage (определение поля)
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_price',
'entity_type' => 'node',
'type' => 'integer',
'cardinality' => 1,
]);
$field_storage->save();
// 2. Field Instance (привязка к bundle)
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'product',
'label' => 'Price',
'required' => TRUE,
'default_value' => [['value' => 0]],
]);
$field->save();
Field Storage (хранилище)
├─ Определяет тип данных
├─ Определяет кардинальность (сколько значений)
└─ Общее для всех bundles
Field Instance (экземпляр)
├─ Привязан к конкретному bundle (content type)
├─ Настройки отображения
└─ Настройки формы
// Получение значения
$node = Node::load(1);
$price = $node->get('field_price')->value;
$image_url = $node->get('field_image')->entity->url();
// Установка значения
$node->set('field_price', 1000);
$node->save();
// Мультизначные поля
$tags = $node->get('field_tags')->referencedEntities();
foreach ($tags as $term) {
echo $term->label();
}
Plugin - класс, который выполняет конкретную задачу.
- Block (блоки)
- Field Widget (виджеты для редактирования полей)
- Field Formatter (форматирование отображения)
- Field Type (типы полей)
- Filter (текстовые фильтры)
- Image Effect (эффекты для изображений)
- Menu Link (ссылки меню)
- Views (плагины для Views)
- ...и сотни других
Annotation-based (через аннотации):
<?php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Hello' Block.
*
* @Block(
* id = "hello_block",
* admin_label = @Translation("Hello block"),
* category = @Translation("Custom")
* )
*/
class HelloBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => $this->t('Hello, World!'),
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['message'] = [
'#type' => 'textfield',
'#title' => $this->t('Message'),
'#default_value' => $this->configuration['message'] ?? '',
];
return $form;
}
}
Результат:
- ✅ Блок автоматически обнаруживается
- ✅ Доступен в /admin/structure/block
- ✅ Можно разместить в любой регион темы
- ✅ Настраиваемый через форму
// Получение всех блоков
$block_manager = \Drupal::service('plugin.manager.block');
$blocks = $block_manager->getDefinitions();
// Создание экземпляра плагина
$block_plugin = $block_manager->createInstance('hello_block', [
'message' => 'Custom message'
]);
// Рендеринг
$build = $block_plugin->build();
// 1. Создать аннотацию
namespace Drupal\mymodule\Annotation;
/**
* @Annotation
*/
class PaymentGateway extends Plugin {
public $id;
public $label;
}
// 2. Создать интерфейс
interface PaymentGatewayInterface {
public function charge($amount);
}
// 3. Создать менеджер
class PaymentGatewayManager extends DefaultPluginManager {
public function __construct() {
parent::__construct(
'Plugin/PaymentGateway', // Директория
\Drupal::service('container.namespaces'),
\Drupal::service('module_handler'),
'Drupal\mymodule\Plugin\PaymentGatewayInterface',
'Drupal\mymodule\Annotation\PaymentGateway'
);
}
}
// 4. Создать плагин
/**
* @PaymentGateway(
* id = "stripe",
* label = @Translation("Stripe")
* )
*/
class StripeGateway implements PaymentGatewayInterface {
public function charge($amount) {
// Stripe API call
}
}
Config Management - экспорт всех настроек в YAML файлы.
- Content types (типы контента)
- Fields (поля)
- Views (представления)
- Blocks (блоки)
- Menus (меню)
- Permissions (права доступа)
- Text formats (форматы текста)
- Image styles (стили изображений)
- ...всё!
# config/sync/node.type.article.yml
langcode: en
status: true
dependencies: { }
name: Article
type: article
description: 'Use articles for time-sensitive content like news, press releases or blog posts.'
help: ''
new_revision: true
preview_mode: 1
display_submitted: true
# 1. Экспорт конфигурации
drush config:export
# Создаётся: config/sync/*.yml
# - node.type.article.yml
# - field.field.node.article.body.yml
# - ...
# 2. Commit в Git
git add config/sync/
git commit -m "Added article content type"
# 3. Deploy на другой сервер
git pull
# 4. Импорт конфигурации
drush config:import
# Все настройки применяются!
Configuration (настройки):
- Типы контента
- Поля
- Views
- Блоки
→ Хранится в YAML
→ Экспортируется/импортируется
Content (данные):
- Статьи
- Пользователи
- Комментарии
→ Хранится в БД
→ Не экспортируется в config
Views - модуль для построения запросов через GUI.
- Списки контента
- Таблицы
- Карты (с модулем)
- Календари
- Галереи
- RSS ленты
- REST API endpoints
- GraphQL endpoints
Display (отображение)
├─ Page (страница /products)
├─ Block (блок для размещения)
├─ Feed (RSS)
└─ REST Export (JSON API)
Query (запрос)
├─ Fields (какие поля показывать)
├─ Filters (условия WHERE)
├─ Sort (сортировка)
├─ Relationships (JOIN)
└─ Contextual Filters (параметры URL)
Output (вывод)
├─ Format (таблица, список, сетка)
├─ Pager (пагинация)
└─ Headers/Footers (доп. контент)
1. /admin/structure/views/add
2. View name: Products
3. Show: Content of type Product
4. Create a Page: /products
5. Display: Table
6. Fields:
- Title
- Price
- Image
7. Filters:
- Published: Yes
- Type: Product
8. Sort by: Created (desc)
9. Save
→ Страница /products работает!
→ SQL запрос сгенерирован автоматически
# config/sync/views.view.products.yml
id: products
label: Products
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
fields:
title:
id: title
table: node_field_data
field: title
label: ''
field_price:
id: field_price
table: node__field_price
field: field_price
label: Price
filters:
status:
value: '1'
expose:
operator: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
path: products
// Выполнение Views программно
$view = Views::getView('products');
$view->setDisplay('page_1');
$view->execute();
$results = $view->result;
foreach ($results as $row) {
$node = $row->_entity;
echo $node->label();
}
Drupal использует Symfony Service Container.
# mymodule.services.yml
services:
mymodule.product_manager:
class: Drupal\mymodule\ProductManager
arguments: ['@entity_type.manager', '@database']
mymodule.email_notifier:
class: Drupal\mymodule\EmailNotifier
arguments: ['@plugin.manager.mail']
// В контроллере/плагине
class ProductController extends ControllerBase {
protected $productManager;
public static function create(ContainerInterface $container) {
return new static(
$container->get('mymodule.product_manager')
);
}
public function __construct(ProductManager $product_manager) {
$this->productManager = $product_manager;
}
public function list() {
$products = $this->productManager->getAll();
return ['#theme' => 'product_list', '#products' => $products];
}
}
// Плохо (жёсткая зависимость)
class ProductManager {
public function send() {
$mailer = \Drupal::service('plugin.manager.mail');
$mailer->mail(...);
}
}
// Хорошо (через DI)
class ProductManager {
protected $mailer;
public function __construct(MailManagerInterface $mailer) {
$this->mailer = $mailer;
}
public function send() {
$this->mailer->mail(...);
}
}
// mymodule.module
/**
* Implements hook_node_presave().
*/
function mymodule_node_presave(NodeInterface $node) {
// Выполняется перед сохранением ноды
if ($node->bundle() == 'product') {
$node->set('field_updated', time());
}
}
/**
* Implements hook_form_alter().
*/
function mymodule_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ($form_id == 'node_product_form') {
// Изменение формы продукта
$form['title']['#description'] = t('Enter product name');
}
}
// Event Subscriber
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
$events['kernel.request'][] = ['onRequest'];
return $events;
}
public function onRequest($event) {
// Обработка события
}
}
Концепция: Всё - entity с единым интерфейсом.
Для ЦИФРА:
from abc import ABC, abstractmethod
class BaseEntity(ABC):
"""Базовый класс для всех entities"""
@abstractmethod
def id(self):
pass
@abstractmethod
def label(self):
pass
def save(self):
"""Сохранение entity"""
pass
def delete(self):
"""Удаление entity"""
pass
# Все entities наследуют BaseEntity
class User(BaseEntity):
pass
class Product(BaseEntity):
pass
class Order(BaseEntity):
pass
# Единый API для всех
user = User.load(1)
product = Product.load(1)
user.save()
product.delete()
Концепция: Поля независимы от entity.
Для ЦИФРА:
# Attach поле к любой entity
from cidl.fields import StringField, IntegerField
# Entity может иметь любые поля
User.attach_field(StringField(name='bio'))
Product.attach_field(IntegerField(name='stock'))
# Поля добавляются без изменения кода entity
Концепция: Расширяемость через плагины с аннотациями.
Для ЦИФРА:
# Декоратор для регистрации плагина
@plugin(
id="stripe_payment",
label="Stripe Payment Gateway",
category="Payment"
)
class StripePaymentGateway(BasePaymentGateway):
def charge(self, amount):
# Implementation
pass
# Автоматическое обнаружение и регистрация
plugin_manager.discover_plugins()
stripe = plugin_manager.create_instance('stripe_payment')
Концепция: Конфигурация в YAML, экспорт/импорт.
Для ЦИФРА:
# config/entity_types/product.yml
entity:
name: Product
label: Product
base_table: products
fields:
- name: name
type: string
required: true
- name: price
type: integer
default: 0
api:
endpoints:
list: /api/products
create: /api/products
# Экспорт конфигурации
$ cifra config:export
# Импорт
$ cifra config:import
Концепция: Dependency Injection через контейнер.
Для ЦИФРА:
# services.yml
services:
product_manager:
class: ProductManager
arguments: ['@database', '@email_sender']
# Использование
class ProductController:
def __init__(self, product_manager: ProductManager):
self.product_manager = product_manager
def list_products(self):
return self.product_manager.get_all()
Entity API (BaseEntity)
+ Field API (attach fields)
+ Plugin System (@plugin decorator)
+ Configuration Management (YAML)
+ Service Container (DI)
+ Event System (hooks)
= Архитектура ЦИФРА
| Концепция | Drupal | Django | Strapi | ЦИФРА |
|---|---|---|---|---|
| Entity API | ✅ Лучший | 🟡 Models | ✅ Content Types | ✅ Берём |
| Field API | ✅ Лучший | ❌ Нет | ✅ Есть | ✅ Берём |
| Plugins | ✅ Лучший | 🟡 Apps | ✅ Plugins | ✅ Берём |
| Config as Code | ✅ Да | ❌ Нет | 🟡 Частично | ✅ Берём |
| DI Container | ✅ Symfony | ❌ Нет | ❌ Нет | ✅ Берём |
Версия: 1.0.0
Статус: Анализ завершён
Следующий шаг: Применить концепции Drupal к архитектуре ЦИФРА