Series Parts:

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.

simple_entity_collection_menu:
  title: 'Simple entity list'
  route_name: entity.simple.collection
  description: 'List of Simple entities'
  parent: system.admin_content

simple_type_entity_collection_menu:
  title: 'SimpleEntity Types'
  route_name: entity.simple_type.collection
  description: 'List of Simple entity types (bundles)'
  parent: system.admin_structure
entity collection menu item
Entity collection menu item
entity bundle management menu item
Entity 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:

/**
 * @ContentEntityType(
 *   id = "simple",
 *   handlers = {
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   links = {
 *     "canonical" = "/simple/{simple}",
 *     "add-page" = "/simple/add",
 *     "add-form" = "/simple/add/{simple_type}",
 *     "edit-form" = "/simple/{simple}/edit",
 *     "delete-form" = "/simple/{simple}/delete",
 *     "collection" = "/admin/content/simples",
 *   }
 * )
 */
Link Key Route Name Route URI
canonical entity.simple.canonical /simple/{simple}
add-page entity.simple.add_page /simple/add
add-form entity.simple.add_form /simple/add/{simple_type}
edit-form entity.simple.edit_form /simple/{simple}/edit
delete-form entity.simple.delete_form /simple/{simple}/delete
collection entity.simple.collection /admin/content/simples

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.

simple_entity_task_view:
  route_name: entity.simple.canonical
  base_route: entity.simple.canonical
  title: 'View'

simple_entity_task_edit:
  route_name: entity.simple.edit_form
  base_route: entity.simple.canonical
  title: 'Edit'

simple_entity_task_delete:
  route_name:  entity.simple.delete_form
  base_route:  entity.simple.canonical
  title: Delete
  weight: 10

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 on custom entity
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.

simple_entity_action_add:
  route_name: entity.simple.add_page
  title: 'Add Simple entity'
  appears_on:
    - entity.simple.collection

simple_type_entity_action_add:
  route_name: entity.simple_type.add_form
  title: 'Add Simple entity type (bundle)'
  appears_on:
    - entity.simple_type.collection
entity collection action
Entity collection action

Content Entity

The Content Entity has not changed much since the previous example, but there are some important differences. See if you can spot them.

<?php

namespace Drupal\simple\Entity;

use Drupal\Core\Entity\ContentEntityBase;

/**
 * Defines the Simple entity.
 *
 * @ContentEntityType(
 *   id = "simple",
 *   label = @Translation("Simple"),
 *   base_table = "simple",
 *   entity_keys = {
 *     "id" = "id",
 *     "bundle" = "bundle",
 *   },
 *   fieldable = TRUE,
 *   admin_permission = "administer simple types",
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\simple\SimpleListBuilder",
 *     "access" = "Drupal\Core\Entity\EntityAccessControlHandler",
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "form" = {
 *       "default" = "Drupal\simple\Form\SimpleEntityForm",
 *       "add" = "Drupal\simple\Form\SimpleEntityForm",
 *       "edit" = "Drupal\simple\Form\SimpleEntityForm",
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   links = {
 *     "canonical" = "/simple/{simple}",
 *     "add-page" = "/simple/add",
 *     "add-form" = "/simple/add/{simple_type}",
 *     "edit-form" = "/simple/{simple}/edit",
 *     "delete-form" = "/simple/{simple}/delete",
 *     "collection" = "/admin/content/simples",
 *   },
 *   bundle_entity_type = "simple_type",
 *   field_ui_base_route = "entity.simple_type.edit_form",
 * )
 */
class SimpleEntity extends ContentEntityBase {}

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.

<?php

namespace Drupal\simple;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;

/**
 * Class SimpleTypeListBuilder
 */
class SimpleListBuilder extends EntityListBuilder {

  /**
   * {@inheritdoc}
   */
  public function buildHeader(){
    $header['id'] = $this->t('Linked Entity Id');
    $header['content_entity_label'] = $this->t('Content Entity Label');
    $header['content_entity_id'] = $this->t('Content Entity Id');
    $header['bundle_label'] = $this->t('Config Entity (Bundle) Label');
    $header['bundle_id'] = $this->t('Config Entity (Bundle) Id');

    return $header + parent::buildHeader();
  }

  /**
   * {@inheritdoc}
   */
  public function buildRow(EntityInterface $entity) {
    $row['id'] = $entity->toLink($entity->id());
    $row['content_entity_label'] = $entity->getEntityType()->getLabel()->render();
    $row['content_entity_id'] = $entity->getEntityType()->id();
    $row['bundle_label'] = $entity->bundle->entity->label();
    $row['bundle_id'] = $entity->bundle();

    return $row + parent::buildRow($entity);
  }
}

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
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].

<?php

namespace Drupal\simple\Entity;

use Drupal\Core\Config\Entity\ConfigEntityBundleBase;

/**
 * Defines the Simple Type entity. A configuration entity used to manage
 * bundles for the Simple entity.
 *
 * @ConfigEntityType(
 *   id = "simple_type",
 *   label = @Translation("Simple Type"),
 *   bundle_of = "simple",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "uuid" = "uuid",
 *   },
 *   config_prefix = "simple_type",
 *   config_export = {
 *     "id",
 *     "label",
 *   },
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\simple\SimpleTypeListBuilder",
 *     "form" = {
 *       "default" = "Drupal\simple\Form\SimpleTypeEntityForm",
 *       "add" = "Drupal\simple\Form\SimpleTypeEntityForm",
 *       "edit" = "Drupal\simple\Form\SimpleTypeEntityForm",
 *       "delete" = "Drupal\Core\Entity\EntityDeleteForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   admin_permission = "administer simple types",
 *   links = {
 *     "canonical" = "/admin/structure/simple_type/{simple_type}",
 *     "add-form" = "/admin/structure/simple_type/add",
 *     "edit-form" = "/admin/structure/simple_type/{simple_type}/edit",
 *     "delete-form" = "/admin/structure/simple_type/{simple_type}/delete",
 *     "collection" = "/admin/structure/simple_type",
 *   }
 * )
 */
class SimpleTypeEntity extends ConfigEntityBundleBase {}

Config Entity 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);
  }
}
entity type list builder
Entity Type list builder

Config Entity Form

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!

Next – Part 4: Practical Content Entity with Bundles

References

About the Author

Jonathan Daggerhart

Long time Drupal and WordPress developer. I like to write modules and plugins, and I dabble in frontend and design.

Leave a Reply

Your email address will not be published. Required fields are marked *