Drupal 8. Entity, мы с тобой одной крови

Пнд, 15/02/2016 - 21:29

Drupal 8. Entity, мы с тобой одной крови

– Все объекты Drupal 8 - Entity.
– Но ведь материалы, пользователи, таксономия, комментарии – они же уже были Entity еще в 7-ке.
– Все - значит все.

Как так?

Поля, блоки, меню, стили изображений, роли, вьюхи, фиды, языки, форматы… Стоп! Как это всё можно одной гребёнкой, ведь это совсем разные вещи. А дело в том, что Entity теперь тоже не так прост. Т.е. не так конкретен. Т.е. настолько абстрактен (а потому и вездесущ), что теперь еще сложнее сказать, что это. Но Барт Финстра (xano), который вроде как в этом понимает, говорит следующее:
Entites are self-contained units of complex data
Сущности – автономные (независимые) составляющие данных
Сущности – сути вещей

Ближе к телу

Entity делится на две категории: Контент и Конфигурацию (19 стр.). А описание возможностей идет через интерфейсы, которых море. Вот, например, как определяется нода (отсюда):
drupal 8, node, schema
А вот общая структура, где эту ноду еще попробуй найди (нащелкал в PhpStorm Diagrams)
drupal8, entity, schema

Главные выводы:

  • теперь не во всякий 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

<?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, а развлекательная статья. И дай бог, чтобы ты её и так доскролил. А что поинтересней можно глянуть здесь:

Поля

Сказать, что 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 | uuid | bundle_name | langcode |
| entity_id | bundle_name | langcode | default_langcode | label ||

3. С поддержкой редакций (Revisionable):

| entity_id | revision_id | uuid | bundle_name | label ||
| entity_id | revision_id | label | revision_timestamp | revision_uid | revision_log ||

4. С поддержкой мультиязычности и редакций:

| entity_id | revision_id | uuid | bundle_name | langcode |
| 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 ||

К чему я все это вам втюхиваю? А к тому, что теперь можно легко расширять таблицы. Вот пример из жизни:
Нужно выводить всех пользователей, которые хоть что-нибудь опубликовали, при этом указывать количество публикаций и заголовок последней для каждого из них. Ну, типо трекера активности.
active user tracker
У меня с активностью не задалось, но если ты более успешен, то такой запрос будет давать неслабую нагрузку.

Типичное решение - денормализация данных. Т.е. добавим к таблице 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(01)
    ->
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 &$buildEntityInterface $entityEntityViewDisplayInterface $display$view_mode$langcode) {
  if (
$display->getComponent('my_own_pseudo_field')) {
    
$build['my_own_pseudo_field'] = [
      
'#type' => 'markup',
      
'#markup' => 'This is my custom content',
    ];
  }
}
?>

8 Спасибо

Комментарии

Аватар пользователя Director-cemetery
4 months 2 недели назад Director-cemetery #

Спасибо за статью, скоро темы о Восьмёрке станут всё более и более популярны. Понравилось чувство юмора, остальное мне, как повелителю мыши понятно, но как бы отчасти.
Хочется увидеть видео об инсталле 8-ки, плюс штук 10 видео, о наиболее часто возникающих проблемах.

Наподобие как на семёрке - "Как войти на сайт?" и "Поставил СКедитор, почему нет редактора?"

Ещё хочется видеть на сайте реально раздел, посвящённый только Восьмёрке. Чтоб новичок зашел, набрался ума и не задавал глупых вопросов (отверстал кэш, всё было хорошо, что произошло потом?) , с умными мы как нибудь разберёмся.

И ещё хочется обратиться к программерам - просьба, начинайте умные речи краткими вводными. Ибо вроде инфа есть, а о чём речь, то ли о гравитации, то ли о чёрных дырах - неясно.

2 Спасибо
Аватар пользователя Graytone
4 months 2 недели назад Graytone #

Хорошая статья. Она мне напомнила лет много назад, лет 14. В коммандировке зимой под сочами, когда было обледенение, рухнули в горах лэп, света нет, холодно и перевалы перекрыты и оттуда не уехать в ближайшие несколько дней. Но у нас был САГ и много бензина, и надо было сварить буржуйку. Но с ним что то случилось и он не хотел работать, наверное ему тоже было холодно. Водитель из адыгеи достал пакетик запрещенного в России ☺☺☺ и предложил посмотреть на это дело с другой стороны. Вечером мы уже топили баню от буржуйки)

3 Спасибо
Аватар пользователя vaplas
4 months 2 недели назад vaplas #

Поверишь или нет, но на 1/4 я - адыг☺

0 Спасибо
Аватар пользователя gor
4 months 2 недели назад gor #

Спасибо! На главной!

1 Спасибо
Аватар пользователя Mirocow
3 months 4 недели назад Mirocow #

Спасибо за познавательную статью

0 Спасибо
Аватар пользователя sergeybelya
3 months 4 недели назад sergeybelya #

Это все хорошо, но только вот работать стало гораздо медленнее:) И еще неприятный сюрприз - при удалении поля из типа материала (я подозреваю, что из любой другой сущности тоже), сносятся также все вьюхи, в которых это поле выводилось, что само по себе бред сумасшедшего. Удивило также, что нет возможности сменить в настройках текстового поля его формат с plain text на использование фильтров, т.е. стала еще более жесткая структура, чем в семерке.

0 Спасибо