– Все объекты Drupal 8 - Entity.
– Но ведь материалы, пользователи, таксономия, комментарии – они же уже были Entity еще в 7-ке.
– Все - значит все.
Как так?
Поля, блоки, меню, стили изображений, роли, вьюхи, фиды, языки, форматы… Стоп! Как это всё можно одной гребёнкой, ведь это совсем разные вещи. А дело в том, что Entity теперь тоже не так прост. Т.е. не так конкретен. Т.е. настолько абстрактен (а потому и вездесущ), что теперь еще сложнее сказать, что это. Но Барт Финстра (xano), который вроде как в этом понимает, говорит следующее:
Entites are self-contained units of complex data
Сущности – автономные (независимые) составляющие данных
Сущности – сути вещей
Ближе к телу
Entity делится на две категории: Контент и Конфигурацию (19 стр.). А описание возможностей идет через интерфейсы, которых море. Вот, например, как определяется нода (отсюда):
А вот общая структура, где эту ноду еще попробуй найди (нащелкал в PhpStorm Diagrams)
Главные выводы:
- теперь не во всякий Entity можно пихать поля;
- обычно контент хранится в базе, а конфиги в файлах;
- если делаешь контент, то оберегай его от конфигов, их реально больше;
- без кода грустно.
У вас есть Entity? Дайте два
Примеров реализации собственных Entity валом. Даже в Examples есть content_entity_example и config_entity_example. По аналогии сделаем еще один, который будет такой же бесполезный, но короче. Встречайте ego.
<?php
namespace Drupal\ego\Entity;
use
Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the EgoContent entity.
*
* @ContentEntityType(
* id = "ego_content",
* label = Translation("Ego Content entity"),
* base_table = "ego",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid"
* },
* )
*/
class EgoContent extends ContentEntityBase {
public static function
baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['id'] = BaseFieldDefinition::create('integer')
->setLabel(t('ID'))
->setDescription(t('Ego ID'));
$fields['uuid'] = BaseFieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('Ego UUID'));
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('Ego Name'))
->setSettings(array(
'max_length' => 100,
));
return
$fields;
}
}?>
CRUD макдак
Распиаренный CRUD проверим с помощью не менее распиаренной системы тестирования /admin/config/development/testing
<?php
namespace Drupal\ego\Tests\Entity;
use
Drupal\ego\Entity\EgoContent;
use Drupal\examples\Tests\ExamplesTestBase;
/**
* Tests of the Ego Content
*
* group ego
*/
class EgoContentTest extends ExamplesTestBase{
public static $modules = array('ego', 'block');
public function
testEgoContent() {
$storage = \Drupal::entityTypeManager()->getStorage('ego_content');
// C - create
$ego = EgoContent::create();
$this->assertNull($ego->id());
$this->assertTrue($ego->isNew());
$ego->save();
$this->assertNotNull($ego->id());
$this->assertFalse($ego->isNew());
$ego2 = $storage->create(array('name'=>"GOD"));
$this->assertEqual($ego2->name->value, "GOD");
$ego2->save();
// R - read
$id = $ego->id();
$ego3 = $storage->load($id);
$ego4 = EgoContent::load($id);
$this->assertEqual($ego->uuid->value, $ego3->uuid->value);
$this->assertEqual($ego->uuid->value, $ego4->uuid->value);
// U - update
$ego->name = "Lalala";
$this->assertEqual($ego->name->value, "Lalala");
$ego->save();
// D - delete
$id = $ego->id();
$id2 = $ego2->id();
$ids = array($id, $id2);
$ego->delete();
$storage->delete(array($ego2));
$result = $storage->loadMultiple($ids);
$this->assertTrue(empty($result));
}
}
?>
Хотя, 30 с. на выполнение многовато, но в зеленый красит исправно.
Да, всё примитивно, но это не обучалка по созданию крутых Entity, а развлекательная статья. И дай бог, чтобы ты её и так доскролил. А что поинтересней можно глянуть здесь:
- Configuration Entities in Drupal 8 (upchuk);
- Drupal 8: Программное создание сущностей (Niklan);
- Entites in Drupal 8 (10-11 стр. показана прикрутка Formatter, Widget, UI Form & View);
- Entity API 8.0 (fago);
- Creating a Content Entity Type in Drupal 8 with DRUPAL CONSOLE (Kwok Wai);
- Drupal.org (Links, Content, Config);
- В модулях друпала пройтись по папкам Entity.
Поля
Сказать, что Entity повлияло на филды – ничего не сказать. Теперь их просто не узнать - вычислительные свойства, методы, все как в лучших домах.. Но о домах чуть позже.
<?php
// Drupal 7
$node->body[$langcode][0]['value'];
// Drupal 8
$node->body->value;
$node->tags[2]->target_id;
$ru_node = $node->getTranslation('ru');
$ru_node->language() == 'ru';
$ru_node->title->value = "Прывет!";
$node->field_link->url;
$node->field->getPropertyDefinitions();
$node->hasField('super_field');
$entity = $node->field_name->getEntity();
$node->body->getType();?>
Клёво! Что? В 7-ке есть Entity Metadata Wrapper, который тоже так умеет? Ладно, но вот сейчас будет точно бомба.
EntityFieldQuery (7) vs (8) EntityQuery
Задача: среди опубликованных материалов product и movies выбрать те, у которых в body есть слово discount, и отсортировать по убыванию.
Drupal 7:
<?php
$query = new EntityFieldQuery();
$query
->entityCondition('entity_type', 'node')
->entityCondition('bundle', array('product', 'movies'))
->propertyCondition('status', 1)
->fieldCondition('body', 'value', 'discount', 'CONTAINS')
->propertyOrderBy('created', 'DESC');
$result = $query->execute();
if (!empty($result['node'])) {
$nodes = node_load_multiple(array_keys($result['node']));
}
?>
Drupal 8:
<?php
$storage = \Drupal::entityTypeManager()->getStorage('node');
$query = $storage->getQuery();
$query
->Condition('type', array('product', 'movies'))
->Condition('status', 1)
->Condition('body', 'value', 'discount', 'CONTAINS')
->OrderBy('created', 'DESC');
$result = $query->execute();
$nodes = $storage->loadMultiple($result);
?>
Один Condition работает за троих (field, property, entity Condition)?! В чем секрет его успеха? Просто у него хороший менеджер. А работает он только за одного (угадай кого).
Еще пример conditions:
<?php
$ids = \Drupal::entityQuery('node')
->condition('title', 'About', 'STARTS_WITH')
->condition('created', 637200000, '>')
->execute();
?>
Пример AND и OR группировки условий:
<?php
$query = \Drupal::entityQuery('node')
->condition('status', 1)
->condition('changed', REQUEST_TIME, '<');
$group = $query->orConditionGroup()
->condition('title', 'cat', 'CONTAINS')
->condition('field_tags.entity.name', 'cats');
$nids = $query->condition($group)->execute();?>
Пример агрегации:
<?php
$query = Drupal::entityQueryAggregate('node');
$result = $query
->groupBy('type')
->aggregate('nid', 'COUNT')
->execute();?>
Пример выборки из блоков:
<?php
$ids = \Drupal::entityQuery('block')
->condition('plugin', 'aggregator_feed_block')
->condition('settings.feed', array_keys($entities))
->execute();
?>
Эй, где мои field_tables?!
Теперь филды еще и лежат не сами по себе, а исключительно с типом entity для которого созданы. Поэтому поля, созданные в одних entity уже не получится использовать в других (но между bundle-ами одного entity пока можно). По этому поводу краткий перевод статьи Франческо Плацело (plach). Только перевод этой статьи изначально и предполагался, но потом меня понесло
***********************************
Ну да, теперь поля прикреплены типо к entity. Благодаря этому не будет лишней суеты, когда в запросе есть условие на несколько полей. А то, что теперь их нельзя использовать для разных entity, как по мне, даже хорошо. Вечно накалывался, когда пользовался этим.
Более того, Entity сейчас вообще хранятся как попало. Что-то в базе, что-то в файлах, что-то опять в базе, но сериализованно в blob. И это сделано специально для того, чтобы все пользовались Entity Query API. Эта классная штука, учитывающая кучу нюансов, в том числе кэширование и, главное, реализацию хранилища. Например, если это SQL, то entity reference связи превратятся в JOIN-ы. Короче, только отсутствие у хранилища реализации Entity Query API спасет вас от жесткого баттхерта, если вы сделаете запрос напрямую. И то, при этом нужно ограничиться только получением id, а остальное уже через специальные методы загрузки.
Ладно, в общем-то я хотел рассказать о схеме хранения.
Везде расставлены слухачи событий изменения, добавления или удаления полей к entity. Так что, если при этом не задеваются никакие данные, процесс обновления сработает быстро и четко (иначе уж сами разруливайте).
Вот 4 основных модели организации таблиц:
1. Простой entity:
| entity_id | uuid | bundle_name | label | … |
2. С поддержкой мультиязычности (Translatable):
| entity_id | bundle_name | langcode | default_langcode | label | … |
3. С поддержкой редакций (Revisionable):
| entity_id | revision_id | label | revision_timestamp | revision_uid | revision_log | … |
4. С поддержкой мультиязычности и редакций:
| entity_id | revision_id | bundle_name | langcode | default_langcode | label | … |
| entity_id | revision_id | langcode | revision_timestamp | revision_uid | revision_log |
| entity_id | revision_id | langcode | default_langcode | label | … |
К чему я все это вам втюхиваю? А к тому, что теперь можно легко расширять таблицы. Вот пример из жизни:
Нужно выводить всех пользователей, которые хоть что-нибудь опубликовали, при этом указывать количество публикаций и заголовок последней для каждого из них. Ну, типо трекера активности.
У меня с активностью не задалось, но если ты более успешен, то такой запрос будет давать неслабую нагрузку.
Типичное решение - денормализация данных. Т.е. добавим к таблице User еще два поля, для хранения количества записей и заголовка последней из них.
<?php
function active_users_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];
if ($entity_type->id() == 'user') {
$fields['last_created_node'] = BaseFieldDefinition::create('entity_reference')
->setLabel('Last created node')
->setRevisionable(TRUE)
->setSetting('target_type', 'node')
->setSetting('handler', 'default');
$fields['node_count'] = BaseFieldDefinition::create('integer')
->setLabel('Number of created nodes')
->setRevisionable(TRUE)
->setDefaultValue(0);
}
return $fields;
}
?>
Поддержка редакций указана из-за того, что она есть у User, а значит и все её поля должны это делать. А вот если её не будет, то и флаг редакции поля просто проигнорируется.
Модуль уже можно подключать. Но созданные поля сами себя не заполнят, поэтому добавим специально обученный сервис:
<?php
public function onNodeCreated(NodeInterface $node) {
$user = $node->getOwner();
$user->last_created_node = $node;
$user->node_count = $this->getNodeCount($user);
$user->save();
}
protected function
getNodeCount(UserInterface $user) {
$result = $this->nodeStorage->getAggregateQuery()
->aggregate('nid', 'COUNT')
->condition('uid', $user->id())
->execute();
return
$result[0]['nid_count'];
}
public function
onNodeDeleted(NodeInterface $node) {
$user = $node->getOwner();
// ох уж этот итальянец, все предусмотрел!
if ($user->last_created_node->target_id == $node->id()) {
$user->last_created_node = $this->getLastCreatedNode($user);
}
$user->node_count = $this->getNodeCount($user);
$user->save();
}
protected function
getLastCreatedNode(UserInterface $user) {
$result = $this->nodeStorage->getQuery()
->condition('uid', $user->id())
->sort('created', 'DESC')
->range(0, 1)
->execute();
return
reset($result);
}
?>
Отличненько, теперь еще подкинем метод получения id активных пользователей.
<?php
public function getActiveUsers() {
$ids = $this->userStorage->getQuery()
->condition('status', 1)
->condition('node_count', 0, '>')
->condition('last_created_node.entity.status', 1)
->sort('login', 'DESC')
->execute();
return
User::loadMultiple($ids);
}?>
Заметили, как Query API разобрался с зависимостями между таблицами User и Node и даже не вспотел?
Осталось только воспользоваться всем этим. У меня в друпал-кругах уже определенная репутация, поэтому пришлось делать через контроллер:
<?php
public function view() {
$rows = [];
foreach ($this->manager->getActiveUsers() as $user) {
$rows[]['data'] = [
String::checkPlain($user->label()),
intval($user->node_count->value),
String::checkPlain($user->last_created_node->entity->label()),
];
}
return [
'#theme' => 'table',
'#header' => [$this->t('User'), $this->t('Node count'), $this->t('Last created node')],
'#rows' => $rows,
];
}
?>
Какой смысл в друпале без общих филдов, я ухожу
Постой (я с тобой)! Если использование общего поля для разных entity является принципиальным, то можно это устроить с помощью трюка с псевдо-полями:
<?php
use Drupal\node\Entity\NodeType;
use \Drupal\Core\Entity\EntityInterface;
use \Drupal\Core\Entity\Display\EntityViewDisplayInterface;
/**
* Implements hook_entity_extra_field_info().
*/
function my_module_entity_extra_field_info() {
$extra = array();
foreach (
NodeType::loadMultiple() as $bundle) {
$extra['node'][$bundle->Id()]['display']['my_own_pseudo_field'] = array(
'label' => t('My own field'),
'description' => t('This is my own pseudo-field'),
'weight' => 100,
'visible' => TRUE,
);
}
return
$extra;
}
/**
* Implements hook_ENTITY_TYPE_view().
*/
function my_module_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode, $langcode) {
if ($display->getComponent('my_own_pseudo_field')) {
$build['my_own_pseudo_field'] = [
'#type' => 'markup',
'#markup' => 'This is my custom content',
];
}
}?>
Вложение | Размер |
---|---|
drupal8_entity_poster.jpg | 169.23 КБ |
drupal8_schema_node.png | 76 КБ |
drupal8_entity_schema.png | 156.88 КБ |
active-users-list-small.png | 36.4 КБ |
Комментарии
Это все хорошо, но только вот работать стало гораздо медленнее:) И еще неприятный сюрприз - при удалении поля из типа материала (я подозреваю, что из любой другой сущности тоже), сносятся также все вьюхи, в которых это поле выводилось, что само по себе бред сумасшедшего. Удивило также, что нет возможности сменить в настройках текстового поля его формат с plain text на использование фильтров, т.е. стала еще более жесткая структура, чем в семерке.