Blog

Własna encja w Drupalu 8

Entity API jest teraz w rdzeniu Drupala 8. Nie ma już prawie żadnej wymówki, żeby tworzyć tabele w bazie, które nie są zarazem encjami w Drupalu. Jeśli więc do Drupal developmentu podchodzisz serio - przeczytaj ten tekst!


Tworząc encję, otrzymujesz za darmo integrację z Views - możesz pozwolić na dodawanie pól do swoich encji oraz dostajesz UI do zarządzania nimi. Możesz również szukać encji przy pomocy Drupal::EntityQuery.

Ostatnio musiałem stworzyć prostą encję dla słownika finansowego i była to doskonała okazja, żeby podzielić się z wami tym, czego się nauczyłem.

Encja terminu w słowniku

Tworzona przez nas encja będzie przechowywała tłumaczenie słów z angielskiego na polski. Jest naprawdę prosta, bo zawiera tylko dwa główne pola:

  • pl - pole tekstowe na termin po polsku
  • en - pole tekstowe na termin po angielsku

Oprócz tego dodam jeszcze kilka innych pól, które warto dodać to prawie każdej encji::

  • id - unikalny identyfikator
  • uuid - Drupal 8 ma natywne wsparcie dla  "universally unique identifiers"
  • user_id - referencja do autora wpisu
  • created - timestamp z datą stworzenia wpisu
  • changed - timestamp z datą ostatniej edycji

Stwórzmy najpierw moduł 'dictionary'

W /sites/modules/custom stworzyłem folder 'dictionary' z następującymi plikami:

dictionary.info.yml

name: dictionary
type: module
description: Dictionary
core: 8.x
package: Application

 

<?php
/**
 * @file
 * Contains \Drupal\content_entity_example\Entity\ContentEntityExample.
 */

namespace Drupal\dictionary\Entity;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\user\UserInterface;
use Drupal\Core\Entity\EntityChangedTrait;

/**
 * Defines the ContentEntityExample entity.
 *
 * @ingroup dictionary
 *
 *
 * @ContentEntityType(
 *   id = "dictionary_term",
 *   label = @Translation("Dictionary Term entity"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\dictionary\Entity\Controller\TermListBuilder",
 *     "form" = {
 *       "add" = "Drupal\dictionary\Form\TermForm",
 *       "edit" = "Drupal\dictionary\Form\TermForm",
 *       "delete" = "Drupal\dictionary\Form\TermDeleteForm",
 *     },
 *     "access" = "Drupal\dictionary\TermAccessControlHandler",
 *   },
 *   list_cache_contexts = { "user" },
 *   base_table = "dictionary_term",
 *   admin_permission = "administer dictionary_term entity",
 *   entity_keys = {
 *     "id" = "id",
 *     "uuid" = "uuid",
 *     "user_id" = "user_id",
 *     "created" = "created",
 *     "changed" = "changed",
 *     "pl" = "pl",
 *     "en" = "en",
 *   },
 *   links = {
 *     "canonical" = "/dictionary_term/{dictionary_term}",
 *     "edit-form" = "/dictionary_term/{dictionary_term}/edit",
 *     "delete-form" = "/dictionary_term/{dictionary_term}/delete",
 *     "collection" = "/dictionary_term/list"
 *   },
 *   field_ui_base_route = "entity.dictionary.term_settings",
 * )
 */
class Term extends ContentEntityBase {

  use EntityChangedTrait;

  /**
   * {@inheritdoc}
   *
   * When a new entity instance is added, set the user_id entity reference to
   * the current user as the creator of the instance.
   */
  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
    parent::preCreate($storage_controller, $values);
    // Default author to current user.
    $values += array(
      'user_id' => \Drupal::currentUser()->id(),
    );
  }

  /**
   * {@inheritdoc}
   *
   * Define the field properties here.
   *
   * Field name, type and size determine the table structure.
   *
   * In addition, we can define how the field and its content can be manipulated
   * in the GUI. The behaviour of the widgets used can be determined here.
   */
  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 Term 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 Contact entity.'))
      ->setReadOnly(TRUE);

    // Name field for the contact.
    // We set display options for the view as well as the form.
    // Users with correct privileges can change the view and edit configuration.
    $fields['pl'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Polish'))
      ->setDescription(t('Polish version.'))
      ->setSettings(array(
        'default_value' => '',
        'max_length' => 255,
        'text_processing' => 0,
      ))
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'string',
        'weight' => -6,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string_textfield',
        'weight' => -6,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['en'] = BaseFieldDefinition::create('string')
      ->setLabel(t('English'))
      ->setDescription(t('English version.'))
      ->setSettings(array(
        'default_value' => '',
        'max_length' => 255,
        'text_processing' => 0,
      ))
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string_textfield',
        'weight' => -4,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    // Owner field of the contact.
    // Entity reference field, holds the reference to the user object.
    // The view shows the user name field of the user.
    // The form presents a auto complete field for the user name.
    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('User Name'))
      ->setDescription(t('The Name of the associated user.'))
      ->setSetting('target_type', 'user')
      ->setSetting('handler', 'default')
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'author',
        'weight' => -3,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'entity_reference_autocomplete',
        'settings' => array(
          'match_operator' => 'CONTAINS',
          'size' => 60,
          'placeholder' => '',
        ),
        'weight' => -3,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Created'))
      ->setDescription(t('The time that the entity was created.'));

    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setDescription(t('The time that the entity was last edited.'));

    return $fields;
  }

}

Klasa Term rozszerza ContentEntityBase, która jest podstawową klasą startową wszystkich dla encji z treścią w Drupalu 8. Warto zauważyć użycie annotacji, które są obowiązkowe. Jest tu zadeklarowanych wiele ważnych informacji. W szczególności::

  • id - unikalny identifykator encji w systemie
  • handlers - linki do wszystkich kontrolerów
  • base_table - nazwa podstawowej tabeli w bazie danych, w której będą trzymane dane o encji. Nie trzeba osobno deklarować schema (definicji tabeli w bazie danych). Jest ona tworzona na podstawie pól encji.

I to tyle. Encja jest gotowa. Cała pozostała praca to stworzenie widoków i formularzy do pracy z encją.

Aby to zrobić, stwórzmy routing i uprawnienia.

dictionary.routing.yml

# This file brings everything together. Very nifty!

# Route name can be used in several place (links, redirects, local actions etc.)
entity.dictionary_term.canonical:
  path: '/dictionary_term/{dictionary_term}'
  defaults:
  # Calls the view controller, defined in the annotation of the dictionary_term entity
    _entity_view: 'dictionary_term'
    _title: 'dictionary_term Content'
  requirements:
  # Calls the access controller of the entity, $operation 'view'
    _entity_access: 'dictionary_term.view'

entity.dictionary_term.collection:
  path: '/dictionary_term/list'
  defaults:
  # Calls the list controller, defined in the annotation of the dictionary_term entity.
    _entity_list: 'dictionary_term'
    _title: 'dictionary_term List'
  requirements:
  # Checks for permission directly.
    _permission: 'view dictionary_term entity'

entity.dictionary.term_add:
  path: '/dictionary_term/add'
  defaults:
  # Calls the form.add controller, defined in the dictionary_term entity.
    _entity_form: dictionary_term.add
    _title: 'Add dictionary_term'
  requirements:
    _entity_create_access: 'dictionary_term'

entity.dictionary_term.edit_form:
  path: '/dictionary_term/{dictionary_term}/edit'
  defaults:
  # Calls the form.edit controller, defined in the dictionary_term entity.
    _entity_form: dictionary_term.edit
    _title: 'Edit dictionary_term'
  requirements:
    _entity_access: 'dictionary_term.edit'

entity.dictionary_term.delete_form:
  path: '/dictionary_term/{dictionary_term}/delete'
  defaults:
    # Calls the form.delete controller, defined in the dictionary_term entity.
    _entity_form: dictionary_term.delete
    _title: 'Delete dictionary_term'
  requirements:
    _entity_access: 'dictionary_term.delete'

entity.dictionary.term_settings:
  path: 'admin/structure/dictionary_term_settings'
  defaults:
    _form: '\Drupal\dictionary\Form\TermSettingsForm'
    _title: 'dictionary_term Settings'
  requirements:
    _permission: 'administer dictionary_term entity'

dictionary.permissions.yml

'delete dictionary_term entity':
title: Delete term entity content.
'add dictionary_term entity':
title: Add term entity content
'view dictionary_term entity':
title: View term entity content
'edit dictionary_term entity':
title: Edit term entity content
'administer dictionary_term entity':
title: Administer term settings

 

Zazwyczaj encje mają przydatne lokalne linki do obsługi (taby do edycji).

dictionary.links.tasks.yml

# Define the 'local' links for the module

entity.dictionary_term.settings_tab:
route_name: dictionary.term_settings
title: Settings
base_route: dictionary.term_settings

entity.dictionary_term.view:
route_name: entity.dictionary_term.canonical
base_route: entity.dictionary_term.canonical
title: View

entity.dictionary_term.page_edit:
route_name: entity.dictionary_term.edit_form
base_route: entity.dictionary_term.canonical
title: Edit

entity.dictionary_term.delete_confirm:
route_name: entity.dictionary_term.delete_form
base_route: entity.dictionary_term.canonical
title: Delete
weight: 10

 

dictionary.links.action.yml

Link do dodawania nowego terminu na liście terminów.

dictionary._term_add:
# Which route will be called by the link
route_name: entity.dictionary.term_add
title: 'Add term'

# Where will the link appear, defined by route name.
appears_on:
- entity.dictionary_term.collection
- entity.dictionary_term.canonical

 

Teraz, kiedy wszystkie elementy menu są gotowe, stwórzmy stronę, która listuje nasze encje oraz dodajmy formularze tworzenia i edycji encji.

/src/Entity/Controller/TermListBuilder.php

W przypadku większości kontrolerów encji opieramy się na tych dostarczanych przez Drupala. Jednak kontroler widoku listy chcemy napisać sami, aby wyświetlał nam wygodne dla nas dane: słowo po angielsku ze słowem po polsku w tabelce.

<?php

/**
* @file
* Contains \Drupal\dictionaryEntity\Controller\TermListBuilder.
*/

namespace Drupal\dictionary\Entity\Controller;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides a list controller for dictionary_term entity.
*
* @ingroup dictionary
*/
class TermListBuilder extends EntityListBuilder {

  /**
  * The url generator.
  *
  * @var \Drupal\Core\Routing\UrlGeneratorInterface
  */
  protected $urlGenerator;

/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager')->getStorage($entity_type->id()),
$container->get('url_generator')
);
}

/**
* Constructs a new DictionaryTermListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type term.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, UrlGeneratorInterface $url_generator) {
parent::__construct($entity_type, $storage);
$this->urlGenerator = $url_generator;
}

/**
* {@inheritdoc}
*
* We override ::render() so that we can add our own content above the table.
* parent::render() is where EntityListBuilder creates the table using our
* buildHeader() and buildRow() implementations.
*/
public function render() {
$build['description'] = array(
'#markup' => $this->t('Content Entity Example implements a DictionaryTerms model. These are fieldable entities. You can manage the fields on the <a href="@adminlink">Term admin page</a>.', array(
'@adminlink' => $this->urlGenerator->generateFromRoute('entity.dictionary.term_settings'),
)),
);
$build['table'] = parent::render();
return $build;
}

/**
* {@inheritdoc}
*
* Building the header and content lines for the dictionary_term list.
*
* Calling the parent::buildHeader() adds a column for the possible actions
* and inserts the 'edit' and 'delete' links as defined for the entity type.
*/
public function buildHeader() {
$header['id'] = $this->t('TermID');
$header['pl'] = $this->t('Polish');
$header['en'] = $this->t('English');
return $header + parent::buildHeader();
}

/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/* @var $entity \Drupal\dictionary\Entity\Term */
$row['id'] = $entity->id();
$row['pl'] = $entity->pl->value;
$row['en'] = $entity->en->value;
return $row + parent::buildRow($entity);
}

}

Widać jak tworzymy nagłówek tabeli  (buildHeader) i wiersze z wynikami (buildRow).

 

add/edit form - src/Form/TermForm.php

<?php
/**
* @file
* Contains Drupal\dictionary\Form\TermForm.
*/

namespace Drupal\dictionary\Form;

use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;

/**
* Form controller for the content_entity_example entity edit forms.
*
* @ingroup content_entity_example
*/
class TermForm extends ContentEntityForm {

/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/* @var $entity \Drupal\dictionary\Entity\Term */
$form = parent::buildForm($form, $form_state);
return $form;
}

/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// Redirect to term list after save.
$form_state->setRedirect('entity.dictionary_term.collection');
$entity = $this->getEntity();
$entity->save();
}

}

 

delete form - src/Form/TermForm.php

<?php

/**
* @file
* Contains \Drupal\dictionary\Form\TermDeleteForm.
*/

namespace Drupal\dictionary\Form;

use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
* Provides a form for deleting a content_entity_example entity.
*
* @ingroup dictionary
*/
class TermDeleteForm extends ContentEntityConfirmFormBase {

/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete entity %name?', array('%name' => $this->entity->label()));
}

/**
* {@inheritdoc}
*
* If the delete command is canceled, return to the contact list.
*/
public function getCancelUrl() {
return new Url('entity.dictionary_term.collection');
}

/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}

/**
* {@inheritdoc}
*
* Delete the entity and log the event. logger() replaces the watchdog.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$entity = $this->getEntity();
$entity->delete();

$this->logger('dictionary')->notice('deleted %title.',
array(
'%title' => $this->entity->label(),
));
// Redirect to term list after delete.
$form_state->setRedirect('entity.dictionary_term.collection');
}

}

 

src/Form/TermSettingsForm.php

Ostatnim formularzem jest Settings form, który pozwala na dodanie dodatkowych ustawień dla encji. Nasz pozostaje pusty, bo nie potrzebujemy żadnych. Do tej strony dodaje się z reguły local tasks, które pozwalają na zarządzanie polami.

<?php
/**
 * @file
 * Contains \Drupal\dictionary\Form\TermSettingsForm.
 */

namespace Drupal\dictionary\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Class ContentEntityExampleSettingsForm.
 *
 * @package Drupal\dictionary\Form
 *
 * @ingroup dictionary
 */
class TermSettingsForm extends FormBase {
  /**
   * Returns a unique string identifying the form.
   *
   * @return string
   *   The unique string identifying the form.
   */
  public function getFormId() {
    return 'dictionary_term_settings';
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Empty implementation of the abstract submit class.
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['dictionary_term_settings']['#markup'] = 'Settings form for Dictionary Term. Manage field settings here.';
    return $form;
  }

}

I wszystko działa. Nasza encja jest gotowa.

Pełen kod do pobrania tutaj

 

3. Najlepsze praktyki zespołów programistycznych