FieldTypes, FieldWidgets en FieldFormatters
Overzicht
Drupal 8 wordt geleverd met een grote bibliotheek van basisklassen waarmee je met je eigen content kunt werken. Wanneer het gaat om contententiteiten, wil je velden gebruiken. Het is belangrijk velden te begrijpen, omdat dat precies de plek is waar je entiteiten hun data opslaan.
FieldTypes
Standaard veldtypes:
- boolean
- changed
- comment
- created
- datetime
- daterange
- decimal
- entity_reference
- file
- float
- image
- integer
- language
- link
- list_float
- list_integer
- list_string
- map
- password
- path
- string
- string_long
- telephone
- text
- text_long
- text_with_summary
- timestamp
- uri
- uuid
Aangepaste veldtypes
Telkens wanneer je data wilt representeren op een manier die Drupal niet standaard biedt, kun je een nieuw veldtype aanmaken voor je data.
Stel dat je een contententiteit hebt die gevoelige data bevat. De maker van deze content kan bepaalde gebruikers toegang geven tot het object met een wachtwoord, dat verschilt per gebruiker. In database-tabellen zou dat er zo uitzien:
| entity_id | uid | password | ----------------------------------- | 1 | 1 | 'helloworld' | | 1 | 2 | 'goodbye' |
Zoals je ziet heeft onze entiteit met ID 1 twee verschillende wachtwoorden voor twee verschillende gebruikers. Hoe kunnen we dit in Drupal implementeren zonder handmatig een tabel aan te maken? We maken een nieuw veldtype.
Aangezien Drupal veldlogica als Plugin implementeert, hebben we altijd een basisklasse waarvan we kunnen erven, zodat het in Drupal werkt. Voor een nieuw veldtype wil je de volgende mapstructuur maken in je module:
modules/custom/MODULENAME/src/Plugin/Field/FieldType
Het is een vrij lange padnaam en een beetje lastig, maar Drupal maakt het makkelijker om met alle functies te werken die in je modules kunnen samenleven.
Voor ons voorbeeld maken we een bestand EntityUserAccessField.php
namespace Drupal\MODULENAME\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* @FieldType(
* id = "entity_user_access",
* label = @Translation("Entity User Access"),
* description = @Translation("This field stores a reference to a user and a password for this user on the entity."),
* )
*/
class EntityUserAccessField extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
//ToDo: Implement this.
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
//ToDo: Implement this.
}
}
Zoals je ziet lijkt een veldtype erg op een contententiteit. In feite is er geen verschil tussen de twee, maar dat is een onderwerp voor een andere les ;)
Allereerst hebben we de annotatie voor ons veldtype:
- @FieldType: roept de FieldType-annotatieklasse uit de Drupal-bibliotheek aan
- id: dit is de machinenaam van ons veldtype, zodat we het opnieuw kunnen gebruiken. Zorg dat je geen gereserveerde namen uit PHP of dergelijke gebruikt.
- label: dit kan een door de gebruiker leesbare vertaling van de machinenaam zijn.
- description: als labels niet genoeg zijn, kun je ook een beschrijving toevoegen voor het veldtype.
Daarnaast breidt onze klasse FieldItemBase uit, waardoor we twee methodes moeten implementeren om dit veldtype correct te gebruiken:
- propertyDefinitions(): Deze methode lijkt op baseFieldDefinition van een contententiteit (maar het is niet hetzelfde!). We kunnen de data definiëren die verschijnt in entiteitsformulieren waar dit veldtype wordt gebruikt.
- schema(): Voor entiteiten is deze methode verouderd, maar in velden bestaat ze nog. Deze methode moet de weergave van velddata in de database implementeren. Dit kan verschillen van de properties.
Omdat het niet altijd duidelijk is wat er in deze methodes moet komen, voegen we voorbeeldcode toe:
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['uid'] = DataDefinition::create('integer')
->setLabel(t('User ID Reference'))
->setDescription(t('The ID of the referenced user.'))
->setSetting('unsigned', TRUE);
$properties['password'] = DataDefinition::create('string')
->setLabel(t('Password'))
->setDescription(t('A password saved in plain text. That is not safe dude!'));
$properties['created'] = DataDefinition::create('timestamp')
->setLabel(t('Created Time'))
->setDescription(t('The time that the entry was created'));
// ToDo: Add more Properties.
return $properties;
}
Het is ook mogelijk om de user ID op te slaan via DataReferenceDefinition, dit kan later worden bekeken.
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$columns = array(
'uid' => array(
'description' => 'The ID of the referenced user.',
'type' => 'int',
'unsigned' => TRUE,
),
'password' => array(
'description' => 'A plain text password.',
'type' => 'varchar',
'length' => 255,
),
'created' => array(
'description' => 'A timestamp of when this entry has been created.',
'type' => 'int',
),
// ToDo: Add more columns.
);
$schema = array(
'columns' => $columns,
'indexes' => array(),
'foreign keys' => array(),
);
return $schema;
}
Schema() is nodig zodat Drupal weet hoe onze data opgeslagen moet worden. De kolommen in schema moeten een subset zijn van de properties gedefinieerd in propertyDefinitions().
Nu hebben we een gloednieuw veldtype. Het heeft nog geen logica voor de verwerking van invoer, maar het kan al gebruikt worden in elk contentobject als veld. Als je wilt, kun je het zelfs gebruiken als baseField voor een entiteit:
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
// Some fields above.
$fields['entity_user_access'] = BaseFieldDefinition::create('entity_user_access')
->setLabel(t('Entity User Access'))
->setDescription(t('Specify passwords for any user that want to see this entity.'))
->setCardinality(-1); // Ensures that you can have more than just one member
// Even more fields below.
return $fields;
}
- BaseFieldDefinition::create(): je moet de machinenaam van het veldtype gebruiken in create()
- setCardinality(-1): Het aantal items bepaalt hoeveel veldwaarden een entiteit kan hebben. Bijvoorbeeld, als we 2 schrijven, kunnen slechts twee gebruikers toegang krijgen, bij 3 zijn dat 3 gebruikers, enzovoort. -1 betekent een onbeperkt aantal gebruikers.
FieldWidget
Als je aangepaste data hebt, heb je mogelijk een aangepaste weergave van die data nodig. Widgets worden gebruikt om te bepalen hoe gebruikers die data invoeren in formulieren. Bijvoorbeeld:
- als een formulier een geheel getal verwacht, maar de gebruiker alleen een selectievakje mag aanvinken
- als je autocomplete wilt voor je data
- als een wachtwoordinvoer via een speciale grafische interface gebeurt
enzovoort.
Een veldwidget in Drupal vind je onder
modules/custom/MODULENAME/src/Plugin/Field/FieldWidget
ook dit is een vrij lange padnaam. Op dit punt moet duidelijk zijn waarom Drupal deze stijl gebruikt voor het opsplitsen van .php-bestanden. Het is dan heel makkelijk te zien welke bestanden waar thuishoren.
We maken de veldwidget in EntityUserAccessWidget.php
<code>namespace Drupal\MODULENAME\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Plugin implementation of the 'entity_user_access_w' widget.
*
* @FieldWidget(
* id = "entity_user_access_w",
* label = @Translation("Entity User Access - Widget"),
* description = @Translation("Entity User Access - Widget"),
* field_types = {
* "entity_user_access",
* },
* multiple_values = TRUE,
* )
*/
class EntityUserAccessWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
// ToDo: Implement this.
}
}</code>
Zie je het? Drupal 8 gebruikt dit coderingspatroon steeds opnieuw als je functies wilt implementeren. Er is een annotatie en een basisklasse die je moet uitbreiden. Ja, Drupal kan dat gebruiken!
- @FieldWidget: definieert de annotatieklasse
- id: machinenaam van de widget
- field_types: array met machinenaam van veldtypes die deze widget kunnen gebruiken
- multiple_values: standaard FALSE. Indien TRUE kun je meerdere waarden indienen in het entiteitsformulier
Als je deze widget wilt gebruiken met je veldtype, moet je de annotatie van het veldtype als volgt aanpassen:
// ...
/**
* @FieldType(
* id = "entity_user_access",
* label = @Translation("Entity User Access"),
* description = @Translation("This field stores a reference to a user and a password for this user on the entity."),
* default_widget = "entity_user_access_w",
* )
*/
// ...
Ja, klaar! Nee, wacht, er gebeurt nog niets, want we moeten formElement() implementeren in onze widget.
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element['userlist'] = array(
'#type' => 'select',
'#title' => t('User'),
'#description' => t('Select group members from the list.'),
'#options' => array(
0 => t('Anonymous'),
1 => t('Admin'),
2 => t('foobar'),
// This should be implemented in a better way!
),
);
$element['passwordlist'] = array(
'#type' => 'password',
'#title' => t('Password'),
'#description' => t('Select a password for the user'),
);
// Standaardwaarde instellen voor alle velden hierboven
$childs = Element::children($element);
foreach ($childs as $child) {
$element[$child]['#default_value'] = isset($items[$delta]->{$child}) ? $items[$delta]->{$child} : NULL;
}
return $element;
}
Als je nu een formulier opent met deze widget, zie je minimaal twee invoervelden: één om een gebruiker te selecteren en één voor een wachtwoord. Wil je opslaglogica implementeren, dan moet je validatiemethodes implementeren in deze widget of in het entiteitsformulier. Zie de Drupal 8 Form API voor meer informatie.
Tegen deze tijd heb je het meeste werk gedaan voor een aangepast veld. Als je nog niet helemaal begrijpt wat er gebeurt, probeer de code uit of kijk naar core-modules voor meer voorbeelden.
FieldFormatters
Het laatste dat nog ontbreekt is de weergave van data in de zogenaamde view mode van een entiteit – trouwens, de widget is de form mode. Dit gebeurt meestal als je een entiteit oproept via yourdrupalpage.com/myentity/1/view
Omdat er hier niet veel te bespreken valt, gaan we direct naar de code. Onder modules/custom/MODULENAME/src/Plugin/Field/FieldFormatter maken we EntityUserAccessFormatter.php
namespace Drupal\MODULENAME\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the 'entity_user_access_f' formatter.
*
* @FieldFormatter(
* id = "entity_user_access_f",
* label = @Translation("Entity User Access - Formatter"),
* description = @Translation("Entity User Access - Formatter"),
* field_types = {
* "entity_user_access",
* }
* )
*/
class EntityUserAccessFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = array();
foreach ($items as $delta => $item) {
$elements[$delta] = array(
'uid' => array(
'#markup' => \Drupal\user\Entity\User::load($item->uid)->getUsername(),
),
// Add more content
);
}
return $elements;
}
}
Dit voorbeeld heeft een annotatie die sterk lijkt op die van de widget, dus daar hoeven we niet veel over te zeggen. ViewElements() toont gewoon de gebruikersnaam van de opgeslagen user ID in ons veldtype. Toegangslogica moet worden geïmplementeerd in de entiteit. Daarom zal deze implementatie alle gebruikersnamen tonen van gebruikers die een wachtwoord hebben ingesteld op de entiteit.