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