With Drupal 8 comes the promise of OOP and more straight-forward code reuse. This improvement shines most brightly with the new plugin system, and, in particular, with Field plugins.
What if a field type does almost what you want? Say we want to reference entities, but also associate a quantity with what we reference. A real world example might be a deck builder for a trading card game like Magic: The Gathering or the DragonBall Z TCG. We want to reference a card from a deck entity and put in the quantity at the same time.
That seems like a better user experience than adding the same card three different times.
There are several ways we could implement this. Something like Field Collection could provide this functionality, but this would create a whole new entity for our association. That seems like overkill.
We could also use an additional text field that paired up with our entity reference field. If we are referencing only one entity, this isn’t a bad solution. But what if we need to reference over 20 different entities using a multi-value field? This would be a pain to maintain and render. Accidentally drag one of your values out of order, and the integrity of your data is lost.
But what if we could just add an extra text input field to the core entity reference field? And without creating everything from scratch? Turns out, with Drupal 8, we can. In this article, we’ll walk through extending the core Entity Reference field type with a quantity textfield. Hopefully, this example will also open up other possibilities for you.
We’ll need to extend three different types of plugins:
- FieldType: defines properties and backend storage for the field.
- FieldWidget: what the admin sees when putting data into a field.
- FieldFormatter: how the field data is rendered on the front end for theming.
First, we need to define a new FieldType plugin, but one that extends the core entity reference type. I’m going to go ahead and assume you have a custom module to put the following code in.
/**
* @FieldType(
* id = "entity_reference_quantity",
* label = @Translation("Entity reference quantity"),
* description = @Translation("An entity field containing an entity reference with a quantity."),
* category = @Translation("Reference"),
* default_widget = "entity_reference_autocomplete",
* default_formatter = "entity_reference_label",
* list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList",
* )
*/
class EntityReferenceQuantity extends EntityReferenceItem {
}
Our plugin annotation is almost the same as the class we are extending, except for the id, label, and description properties. Now, if you clear your cache and try adding a field, you’ll see this new field type. Congratulations!
Of course, it doesn’t really do anything special yet. So let’s continue.
To add a quantity, we need to override two methods. The first is EntityReferenceItem::propertyDefinitions()
. This describes the data that this field will contain. We return an array that has the ‘quantity’ key defined, its value being an instance of DataDefinition.
While typed data is out of the scope of this article, you can view the types defined by core and an overview of the API on Drupal.org.
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$quantity_definition = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Quantity'))
->setRequired(TRUE);
$properties['quantity'] = $quantity_definition;
return $properties;
}
The only constraint we will add to our quantity DataDefinition is to make it required, but we could add other constraints, like minimum or maximum values, using the addConstraint() method. An example of that would be something like ->addConstraint('Range', ['min' => 1]);
Constraints are also outside the scope of this article, but you can read more about them and the Entity Validation API here.
The only other method required to override is schema(), which tells Drupal how this new data will be stored, regardless of the entity storage type. The column name needs to match our property name.
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$schema = parent::schema($field_definition);
$schema['columns']['quantity'] = array(
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
);
return $schema;
}
You might also want to look at overriding the EntityReferenceItem::generateSampleValue()
method, but it is not required.
Now, we need to define a custom widget, which will be the form for this field. It needs to be aware of our quantity requirement. Otherwise, we’ll have some confused users getting yelled at for required data that had no corresponding form field.
This calls for another plugin, but like the Field Type, we can just extend the already existing EntityReferenceAutocompleteWidget.
/**
* @FieldWidget(
* id = "entity_reference_autocomplete_quantity",
* label = @Translation("Autocomplete w/Quantity"),
* description = @Translation("An autocomplete text field with an associated quantity."),
* field_types = {
* "entity_reference_quantity"
* }
* )
*/
class EntityReferenceAutocompleteQuantity extends EntityReferenceAutocompleteWidget {
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$widget = parent::formElement($items, $delta, $element, $form, $form_state);
$widget['quantity'] = array(
'#title' => $this->t('Quantity'),
'#type' => 'number',
'#default_value' => isset($items[$delta]) ? $items[$delta]->quantity : 1,
'#min' => 1,
'#weight' => 10,
);
return $widget;
}
}
Our annotation is almost completely different this time. Pay special attention to the field_types property, because that will allow this widget to be used on our new field type. But we only need to override one method.
So now, users can enter and save a quantity associated with an entity reference… but we still need some way to render this new information to the page. The default field formatters for entity references don’t take into account our wonderful new quantity data.
So let’s create our own formatter. Again, we’ll create a new plugin, and again, we just need to extend an already existing formatter. Here, we have a lot of options to base our own formatter on, but we’ll just use the EntityReferenceLabelFormatter in this article for simplicity. It would be a good idea to provide different formatters for a good site building experience.
The following code adds a suffix to the default implementation, and again we only have to override one method. An entity with a label of “My Cool Entity” and a quantity of “3” will be displayed as “My Cool Entity x3”.
/**
* @FieldFormatter(
* id = "entity_reference_quantity_view",
* label = @Translation("Entity label and quantity"),
* description = @Translation("Display the referenced entities’ label with their quantities."),
* field_types = {
* "entity_reference_quantity"
* }
* )
*/
class EntityReferenceQuantityFormatter extends EntityReferenceLabelFormatter {
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = parent::viewElements($items, $langcode);
$values = $items->getValue();
foreach ($elements as $delta => $entity) {
$elements[$delta]['#suffix'] = ' x' . $values[$delta]['quantity'];
}
return $elements;
}
}
Again, you’ll want to pay close attention to the field_types property in the annotation.
After you clear your caches, you should see this new formatter as an option on all entity reference fields.
Where could we go from here? Lots of places, but I wanted to draw attention to one more area of our field type declaration, in case you need more fine-tuned customization. In the annotation for our FieldType, notice this line:
// list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList",
That List class can be anything that implements the FieldItemListInterface. This is the class that will store the list of values for any given instance of your field type.
In the methods we overrode in our widget and formatter classes, you’ll see $items is passed in as a parameter. That will be an instance of whatever you put as your list_class. Customizing that class is another way to tailor your field type specific to your needs. You could add additional helper methods like EntityReferenceFieldItemList does with EntityReferenceFieldItemList::referencedEntities()
. Or you could add additional constraints that would apply to the whole list of values. For example, you might not want to allow more than 60 cards to be referenced, or limit certain cards to just 2 copies.
Whatever you choose to do, however, you don’t need to reinvent the wheel each time. And that’s a beautiful thing.