Note: In this example the Content Entity has been renamed to “Simple” and the Config Entity has been renamed to “Simple Type”. This is because each example is actually its own module in the Github repo.
The Plan
Provide navigation for entity management.
Provide useful messages when managing entities.
Provide useful collections (lists) of entities.
The Code
Let’s creating some simple YAML files that will map the routes provided by the route_provider annotation to links within the Drupal UI.
Menu Links
This first of these is MODULENAME.links.menu.yml. This file adds links to Drupal’s core administrative menu system.
Entity collection menu itemEntity Bundle management menu item
Looks simple enough, but there is a mystery here. Where do all these route_names come from? The answer is in the Content Entity’s handlers[route_provider][html] annotation. Let’s look at how that works.
Route Provider & Links Annotations
The route_provider = {} works in conjunction with the links = {} annotations to dynamically create routes for your entities. For each of your links = {}, the route_provider creates a route named in this pattern: entity.{{ your_entity_id }}.{{ machine_safe_link_key }}.
For example, consider the following abbreviated example and compare the links and their resulting route names:
The alternative to using a route_provider for your entity is that you would have to manually define all the routes yourself within a MODULENAME.routing.yml file. So as you can see, route providers are really helpful!
Moving on…
Tab Links on Entity (Tasks)
Task links give us the familiar View/Edit/Delete tabs at the top of an entity, just like you see on Nodes.
After understanding the route_provider, this should look familiar. route_names are created by the route_provider, and base_routes are the route name these tasks should be attached to.
Tabs added to custom entity.
“Add New” Entity Links (Actions)
Action links provide the helpful “Add New” buttons at the top of a list of entities. By now this should look pretty straight forward to you.
That’s right, we have changed some of the handlers to use new custom classes that we will create. Specifically the list_builder and the default/add/edit forms. Good catch! Let’s see what those new classes look like.
Content Entity Form
The Content Entity’s new Form will look a lot like the previous Config Entity Form. All we need to do is extend the ContentEntityForm and override the save() method.
<?php
namespace Drupal\simple\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Class SimpleEntityForm
*/
class SimpleEntityForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$entity = &$this->entity;
$message_params = [
'%entity_label' => $entity->id(),
'%content_entity_label' => $entity->getEntityType()->getLabel()->render(),
'%bundle_label' => $entity->bundle->entity->label(),
];
$status = parent::save($form, $form_state);
switch ($status) {
case SAVED_NEW:
drupal_set_message($this->t('Created the %bundle_label - %content_entity_label entity: %entity_label.', $message_params ));
break;
default:
drupal_set_message($this->t('Saved the %bundle_label - %content_entity_label entity: %entity_label.', $message_params));
}
$content_entity_id = $entity->getEntityType()->id();
$form_state->setRedirect("entity.{$content_entity_id}.canonical", [$content_entity_id => $entity->id()]);
}
}
In the override for the save method, we provide some simple messages to inform the user what has just occurred, and a redirect that takes them back to viewing the entity they just created or updated. Easy peasy.
Content Entity List Builder
List Builders are used when displaying an Entity’s collection route. They should provide useful information about each entity, as well as common operations that can be performed on the entity.
To create a new List Builder, extend the EntityListBuilder class and override the buildHeader() and buildRow() methods. Return a keyed array of the header labels or row data respectively.
The new List Builder for our Content Entity will show all the information we currently have about the entities, which at this point isn’t very much.
Entity List Builder
Config Entity
Like the Content Entity, the Config Entity (which manages our Bundles) has not changed very much from the previous examples. The only change is a new custom class for handlers[list_builder].
The List Builder for our Config Entity is even more simple than that of our Content Entity. The same principles apply to creating it, but in the case of the Config Entity we have very little data to show per row.
<?php
namespace Drupal\simple;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
/**
* Class SimpleTypeListBuilder
*/
class SimpleTypeListBuilder extends EntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader(){
$header['label'] = $this->t('Label');
$header['id'] = $this->t('Machine name');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['id'] = $entity->id();
return $row + parent::buildRow($entity);
}
}
Since the purpose of this new version is to provide a better admin experience, I’ve modified the Config Entity Form to have an additional “Save and manage fields” button that when clicked will redirect the user to the “Manage Fields” tab for the entity.
<?php
namespace Drupal\simple\Form;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field_ui\FieldUI;
class SimpleTypeEntityForm extends BundleEntityFormBase {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$entity_type = $this->entity;
$content_entity_id = $entity_type->getEntityType()->getBundleOf();
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $entity_type->label(),
'#description' => $this->t("Label for the %content_entity_id entity type (bundle).", ['%content_entity_id' => $content_entity_id]),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $entity_type->id(),
'#machine_name' => [
'exists' => '\Drupal\simple\Entity\SimpleTypeEntity::load',
],
'#disabled' => !$entity_type->isNew(),
];
return $this->protectBundleIdElement($form);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
if (\Drupal::moduleHandler()->moduleExists('field_ui') && $this->getEntity()->isNew()) {
$actions['save_continue'] = $actions['submit'];
$actions['save_continue']['#value'] = $this->t('Save and manage fields');
$actions['save_continue']['#submit'][] = [$this, 'redirectToFieldUi'];
}
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$entity_type = $this->entity;
$status = $entity_type->save();
$message_params = [
'%label' => $entity_type->label(),
'%content_entity_id' => $entity_type->getEntityType()->getBundleOf(),
];
switch ($status) {
case SAVED_NEW:
drupal_set_message($this->t('Created the %label %content_entity_id entity type.', $message_params));
break;
default:
drupal_set_message($this->t('Saved the %label %content_entity_id entity type.', $message_params));
}
$form_state->setRedirectUrl($entity_type->toUrl('collection'));
}
/**
* Form submission handler to redirect to Manage fields page of Field UI.
*
* @param array $form
* @param FormStateInterface $form_state
*/
public function redirectToFieldUi(array $form, FormStateInterface $form_state) {
$route_info = FieldUI::getOverviewRouteInfo($this->entity->getEntityType()->getBundleOf(), $this->entity->id());
if ($form_state->getTriggeringElement()['#parents'][0] === 'save_continue' && $route_info) {
$form_state->setRedirectUrl($route_info);
}
}
}
The actions() method is overriding that of the parent class and adding a new “Save and manage fields” button. That button has a #submit handler on it that will call this class’s custom redirectToFieldUi() method.
Results
That’s it! Improving our most_simple entity by creating a better admin experience was mostly about providing navigation, useful messages, and entity information on the collections route. Our resulting simple entity is not too shabby.
🤠Problems: Not many! Sure, there is plenty of room for improving this example and making it much more practical, but as far as it goes we have addressed the major issues one might have when working with these entities. Huzzah!
One thought on “Drupal 8 Content Entity Bundles – Part 3: Simple Content Entity with Bundles”
Nikolay
I appreciate your time you’ve spent to write this super nice article. I’ve opened so many things to myself, which I couldn’t find anywhere else. This is a high quality content. Thank you!
One thought on “Drupal 8 Content Entity Bundles – Part 3: Simple Content Entity with Bundles”
Nikolay
I appreciate your time you’ve spent to write this super nice article. I’ve opened so many things to myself, which I couldn’t find anywhere else. This is a high quality content. Thank you!