Сущности (Entities) являются основной абстракцией над базой данных в Drupal. Пользователи, ноды, термины таксономии, элементы конфигурации и другие объекты Drupal являются сущностями и поддерживают единый API. Помимо CRUD-операций, сущности предоставляют API для управления доступом к ним, отображения, построения формы добавления новой сущности из админки, интеграции с Views и т.д.
Сущности появились в Drupal 7. В Drupal 8 сущности разделились на два типа — сущности конфигурации и контента. Первые используются системой управления конфигурациями Drupal и хранятся в общей таблице config как строки. Вторые хранятся в отдельный таблицах, названия которых совпадают с id типа сущности, а колонки соответствуют свойствам (properties) сущности. Сущности контента могут поддерживать ревизии. К сущностям контента можно прикреплять дополнительные поля, хранящиеся в отдельных таблицах. При этом для одного типа сущности можно прикреплять разные наборы полей, создавая бандлы (bundle), по одному на разный набор полей. Примерами типа сущности и его бандлов с различными наборами полей являются ноды и типы нодов, товары commerce и типы товаров.
Если нам необходимо создавать и хранить в базе данных какие-либо объекты с различными свойствами, схожие по структуре и поведению, но стандартные типы сущностей Drupal для нас не подходят (ноды, пользователи, таксономия), то надо создать новый тип контент-сущности. Это можно сделать в админке Drupal с помощью contrib-модуля Entity Construction Kit (ECK) либо определить их в кастомном модуле. Рассмотрим как сделать последнее в Drupal 8. Создадим простейший модуль с описанием контент-сущности Example, с набором свойств, поддержкой Views, веб-сервисов, контролем доступа, без бандлов и ревизий.
Объявим новый тип сущности и опишем его свойства. Структура файлов модуля будет такая:
web/modules/custom/example$
├── example.info.yml
└── src
└── Entity
└── Example.php
В example.info.yml ничего особенного:
name: Example
type: module
description: 'Example content entity'
package: custom
core: 8.x
В Example.php создем класс Example, наследуя ContentEntityBase и реализуя интерфейс ContentEntityInterface. В аннотации описываем тип сущности и подключаем хендлеры, например, для поддержки Views. В методе baseFieldDefinitions() описываем свойства сущности, для которых будут создаваться поля в нашей таблице.
<?php namespace Drupal\example\Entity; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Url; /** * Defines the Example entity. * * @ingroup example * * @ContentEntityType( * id = "example", * label = @Translation("Example"), * handlers = { * "views_data" = "Drupal\views\EntityViewsData", * }, * base_table = "example", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * }, * ) */ class Example extends ContentEntityBase implements ContentEntityInterface { public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Standard field, used as unique if primary index. $fields['id'] = BaseFieldDefinition::create('integer') ->setLabel(t('ID')) ->setDescription(t('The ID of the Bonus entity.')) ->setReadOnly(TRUE); // Standard field, unique outside of the scope of the current project. $fields['uuid'] = BaseFieldDefinition::create('uuid') ->setLabel(t('UUID')) ->setDescription(t('The UUID of the Bonus entity.')) ->setReadOnly(TRUE); // Int field. $fields['fint'] = BaseFieldDefinition::create('integer') ->setLabel(t('Int field')) ->setDescription(t('Example int field.')); // Record creation date. $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Created')) ->setDescription(t('The time that example was created.')); // String field. $fields['fstring'] = BaseFieldDefinition::create('string') ->setLabel(t('String field')) ->setDescription(t('Example string field.')) ->setSettings(array( 'default_value' => '', 'max_length' => 100, 'text_processing' => 0, )); // Float field. $fields['fdecimal'] = BaseFieldDefinition::create('decimal') ->setLabel(t('Float field')) ->setDescription(t('Example float field.')) ->setSettings(array( 'precision' => 17, 'scale' => 2, )); return $fields; } public function toUrl($rel = 'canonical', array $options = []) { // Return default URI as a base scheme as we do not have routes yet. return Url::fromUri('base:entity/example/' . $this->id(), $options); } }
При включении модуля для хранения сущностей в базе данных будет автоматически создана таблица.
+----------+---------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+---------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | uuid | varchar(128) | NO | UNI | NULL | | | fint | int(11) | YES | | NULL | | | created | int(11) | YES | | NULL | | | fstring | varchar(100) | YES | | NULL | | | fdecimal | decimal(17,2) | YES | | NULL | | +----------+---------------+------+-----+---------+----------------+
Если в дальнейшем потребуется поменять структуру таблицы, то необходимо внести изменения в метод baseFieldDefinitions() и выполнить команду:
drush updatedb --entity-updates
Экземпляр новой сущности можно создать программно с помощью следующего кода:
<?php use Drupal\example\Entity\Example; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\LanguageInterface; $created = time(); $uuid_service = \Drupal::service('uuid'); $uuid = $uuid_service->generate(); $lc = LanguageInterface::LANGCODE_DEFAULT; $example = new Example([ 'uuid' => array($lc => $uuid), 'created' => array($lc => $created), 'fint' => array($lc => 10), 'fstring' => array($lc => 'some text'), 'fdecimal' => array($lc => 10.1), ], 'example'); $example->save();
Результат:
> select * from example; +----+--------------------------------------+------+------------+-----------+----------+ | id | uuid | fint | created | fstring | fdecimal | +----+--------------------------------------+------+------------+-----------+----------+ | 1 | cdc9537c-a140-47d6-a12b-89cdd14d20ce | 10 | 1504699716 | some text | 10.10 | +----+--------------------------------------+------+------------+-----------+----------+
Загрузить сущность программно, зная, например, uuid, можно так:
$entity_ids = \Drupal::entityQuery('example') ->condition('uuid', 'cdc9537c-a140-47d6-a12b-89cdd14d20ce', '=') ->execute();
Поля нового типа сущности Example сразу можно вывести во Views:
Если включить модули RESTful Web Services и Serialization, то все новые сущности можно будет вывести во Views в формате JSON, добавив дисплей типа REST export:
[{ "id":[{"value":1}], "uuid":[{"value":"cdc9537c-a140-47d6-a12b-89cdd14d20ce"}], "fint":[{"value":10}], "created":[{"value":1504699716}], "fstring":[{"value":"some text"}], "fdecimal":[{"value":"10.10"}]}]
Аналогично, можно получить XML, добавив в URL параметр ?_format=xml:
<response> <item key="0"> <id><value>1</value></id> <uuid><value>cdc9537c-a140-47d6-a12b-89cdd14d20ce</value></uuid> <fint><value>10</value></fint> <created><value>1504699716</value></created> <fstring><value>some text</value></fstring> <fdecimal><value>10.10</value></fdecimal> </item> </response>
Чтобы сделать веб-сервис загрузки сущности по UUID достаточно просто добавить аргумент во Views.
Чтобы читать напрямую без Views, создавать, менять, удалять новые сущности с помощью вызова веб-сервиса, необходимо установить contrib-модуль REST UI, в его настройках включить ресурс Example и настроить доступные методы и форматы, разрешить доступ всем, добавив в Example.php реализацию метода access:
use Drupal\Core\Access\AccessResult; ... public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { return AccessResult::allowed(); }
После этого можно сразу загрузить сущность через веб-сервис в формате JSON по адресу:
/entity/example/1?_format=json
Создать новую сущность Example запросом к веб-сервису с помощью curl:
curl --include --request POST --user 'superuser:123' --header 'Content-Type: application/xml' http://localhost/entity_example/drupal-8.3.7/entity/example?_format=xml --data-binary '<?xml version="1.0" ?><request><uuid><value>0000000000005</value></uuid></request>' HTTP/1.1 201 Created Date: Wed, 06 Sep 2017 13:11:19 GMT Server: Apache/2.4.25 (Debian) Cache-Control: must-revalidate, no-cache, private X-UA-Compatible: IE=edge Content-language: en X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Expires: Sun, 19 Nov 1978 05:00:00 GMT X-Generator: Drupal 8 (https://www.drupal.org) Content-Length: 182 Content-Type: text/xml; charset=UTF-8 <?xml version="1.0"?> <response> <id><value>5</value></id> <uuid><value>0000000000005</value></uuid> <fint/> <created><value>1504703479</value></created> <fstring/><fdecimal/> </response>
Аналогично можно удалять и редактировать ресурсы используя другие методы HTTP. Примечательно, что для редактирования используется метод PATCH и шлются только измененные поля.
Контроль доступа
Для контроля доступа к операциям с сущностью, в том числе при вызовах веб-сервиса, необходимо реализовать EntityAccessControlHandler. Для этого добавим файл src/ExampleAccessControlHandler.php:
<?php /** * @file * Contains \Drupal\example\exampleAccessControlHandler. */ namespace Drupal\example; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; /** * Access controller for the Example entity. * * @see \Drupal\example\Entity\Example. */ class ExampleAccessControlHandler extends EntityAccessControlHandler { /** * {@inheritdoc} * * Link the activities to the permissions. checkAccess is called with the * $operation as defined in the routing.yml file. */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { switch ($operation) { case 'view': return AccessResult::allowedIfHasPermission($account, 'view example entity'); case 'edit': return AccessResult::allowedIfHasPermission($account, 'edit example entity'); case 'delete': return AccessResult::allowedIfHasPermission($account, 'delete example entity'); } return AccessResult::allowed(); } /** * {@inheritdoc} * * Separate from the checkAccess because the entity does not yet exist, it * will be created during the 'add' process. */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { return AccessResult::allowedIfHasPermission($account, 'add example entity'); } }
Наш хендлер будет проверять права 'add example entity', 'view example entity', 'edit example entity', 'delete example entity' при обращении к веб-сервису. Чтобы добавить эти права в админку нужно создать файл example.permissions.yml:
add example entity: title: 'Add example entity' view example entity: title: 'View example entity' edit example entity: title: 'Edit example entity' delete example entity: title: 'Delete example entity'
Теперь чтобы подключить наш хендлер доступа к сущности добавим его в список хендлеров в аннотации:
"access" = "Drupal\example\ExampleAccessControlHandler",
и сделаем соответствующие вызовы в методе access():
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { if ($operation == 'create') { return $this->entityManager() ->getAccessControlHandler($this->entityTypeId) ->createAccess($this->bundle(), $account, [], $return_as_object); } return $this->entityManager() ->getAccessControlHandler($this->entityTypeId) ->access($this, $operation, $account, $return_as_object); }
Таким образом, Example.php примет вид:
<?php namespace Drupal\example\Entity; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Url; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; /** * Defines the Example entity. * * @ingroup example * * @ContentEntityType( * id = "example", * label = @Translation("Example"), * handlers = { * "views_data" = "Drupal\views\EntityViewsData", * "access" = "Drupal\example\ExampleAccessControlHandler", * }, * base_table = "example", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * }, * ) */ class Example extends ContentEntityBase implements ContentEntityInterface { public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Standard field, used as unique if primary index. $fields['id'] = BaseFieldDefinition::create('integer') ->setLabel(t('ID')) ->setDescription(t('The ID of the Bonus entity.')) ->setReadOnly(TRUE); // Standard field, unique outside of the scope of the current project. $fields['uuid'] = BaseFieldDefinition::create('uuid') ->setLabel(t('UUID')) ->setDescription(t('The UUID of the Bonus entity.')) ->setReadOnly(TRUE); // Int field. $fields['fint'] = BaseFieldDefinition::create('integer') ->setLabel(t('Int field')) ->setDescription(t('Example int field.')); // Record creation date. $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Created')) ->setDescription(t('The time that example was created.')); // String field. $fields['fstring'] = BaseFieldDefinition::create('string') ->setLabel(t('String field')) ->setDescription(t('Example string field.')) ->setSettings(array( 'default_value' => '', 'max_length' => 100, 'text_processing' => 0, )); // Float field. $fields['fdecimal'] = BaseFieldDefinition::create('decimal') ->setLabel(t('Float field')) ->setDescription(t('Example float field.')) ->setSettings(array( 'precision' => 17, 'scale' => 2, )); return $fields; } public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { if ($operation == 'create') { return $this->entityManager() ->getAccessControlHandler($this->entityTypeId) ->createAccess($this->bundle(), $account, [], $return_as_object); } return $this->entityManager() ->getAccessControlHandler($this->entityTypeId) ->access($this, $operation, $account, $return_as_object); } }
Наш модуль Example получит следующую структуру:
web/modules/custom/example$
├── example.info.yml
├── example.permissions.yml
└── src
├── ExampleAccessControlHandler.php
└── Entity
└── Example.php
Это минимальный код, которого достаточно для создания простого типа сущности, управляемой по веб-сервисам. Такие сущности мы используем при создании веб-сервисов для Headless Drupal, при использовании Drupal 8 в качестве бекэнда для мобильных приложений на React. Для экономии времени при создании новых сущностей можно использовать генераторы кода, о которых подробнее будет рассказано в следующих статьях.
Дополнительная информация о сущностях Drupal 8
Официальная документация
Модуль примеров и статья c примерами роутов, ссылок и форм для работы с сущностями из админки
Примеры программного создания различных сущностей
Веселая статья про сущности Drupal
Добавить комментарий