Сущности (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
Добавить комментарий