Custom entity types with drush in Drupal 9

Drupal as a framework: How to create custom entity type boilerplate code with drush and customize it

Posted on October 25, 2021 · 15 mins read

Motivation

Drupal CMS used to be all about publishing content. It still is, that’s what it does better, but since Drupal 7 was released it can be used very efficiently as a multi-purpose tool just as the other PHP frameworks out there.

But a supposed multi-purpose tool does not manage only pages and articles. Among other functionalities, it lets the developer define classes with arbitrary fields and arbitrary custom logic. This precious superpower is easily achieved in Drupal with a code generator that allows us to set up boilerplate code for custom classes, what in Drupal world is named entity types, letting us work with Drupal as a framework.

The base command for automatic code creation in Drupal is drush generate (drush gen). My intent in this post is to explain some small changes which can be done to the code generated by drush gen module-content-entity not to complicate its results but to simplify them and start with simpler classes to begin our work. Specifically:

But why? Advantages of custom entity types against nodes and when to use them

Some years ago, on the first steps of this path of working with Drupal as a Framework the developer used to create Drupal content types (node types), add fields to them (somebody remembers CCK?) and do some functionality-removal work on them. Things like …

  • Hide from search results.
  • Hide the canonical entity view page.
  • Hide some unwanted default fields. Mostly title, status, published and body.

All this hiding and removing was the price you had to pay to have a more or less fully-featured CMS for free just with core and a couple of contrib modules, and then you could build very specific functionality besides or on top of it. In Drupal 9, custom entity types let us avoid these issues and work with the exact objects and functionalities we need.

On the other side at a data-base storage level, fields added via UI in Drupal aren’t added to the entity base table but in an additional table. Its values are retrieved via JOIN queries. Drupal takes care of setting adequate keys and indexes, and cache has greatly improved the overall scalability of the systems, but it doesn’t look like the most efficient solution for data storage.

With custom entity types, if fields are added in code, they are stored in the same entity base table when possible, and this makes our system more prepared for scaling from the beginning.

How custom entity types was done in Drupal 7 - 8

  • Drupal 7 entity type API was a bit overwhelming, so a lot of people let the ECK module do the hard work. Working with this module was not as comfortable as with frameworks but was getting near to it.
  • Drupal 8 simplified things a lot, Drupal console appeared as an alternative to drush to do all kinds of boilerplate code generation but if it is not abandoned now, it’s near to it. This blog post at OpenSense Labs is a great explanation of how the Drupal console is or was used.
  • Drupal docs also explain how to create a custom content entity type in Drupal 8/9 here and here. The code we will get with the entity type generator is very similar to what is written in those articles, but the generator will do the hard work for us.

Starting from scratch

So, let’s go to work !!

A test Drupal 9 site, with a new Project custom entity type can be created just with following commands:

composer create-project drupal/recommended-project d9
cd d9
composer require drush/drush
vendor/bin/drush site-install standard --db-url="mysql://devel@localhost/d9" --site-name=Drupal 9 Test Site
vendor/bin/drush upwd admin --password="mypass"
sudo chown -R www-data:www-data web/sites/default/files
vendor/bin/drush gen content-entity

A new module will be created with some default configurations and functionalities depending on our answers to the last command. In this case, I created a Project content entity type in a Project module. At the end of the article, you can get all the code for this module.

All the functionalities created by drush gen content-entity command can be extended or removed once the module is enabled except entity fields, which is a bit more tricky to modify afterwards. So, that’s why at this point is advisable to inspect the static method Project::baseFieldDefinitions() where entity fields are declared and add whatever new fields our entity type needs.

Explaining how to set up fields of different types is not the purpose of this article but you’ll find some examples in the last section Simplify the process of adding fields to our class .

Set edit form as the canonical path

It’s quite common that the entity view page as in node/[nid] or project/[id] is not needed. Usually when the entity type is internal to the system or only available to administrators. Of course, we can hide it ensuring no links nor redirections point to it, restrict permissions or ensure generated HTML doesn’t display the information we don’t want, but it looks cleaner to actually remove it, doesn’t it?

In these cases, a practical implementation would be to set the entity edit form as the canonical path. The first option is to alter the autogenerated routes for our entity implementing a Route Subscriber but in this case, I will do it in a more general and reusable way. The boilerplate code generated by drush has set the core class AdminHtmlRouteProvide as responsible for generating our class routes and callbacks so we will declare a new EditFormAsCanonicalHtmlRouteProvider in our custom module:

src/Entity/routing/EditFormAsCanonicalHtmlRouteProvider.php:

namespace Drupal\project\Routing;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;

/**
 * Provides HTML routes for entities with administrative add/edit/delete pages.
 *
 * Use this class if the add/edit/delete form routes should use the
 * administrative theme.
 *
 * @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider.
 */
class EditFormAsCanonicalHtmlRouteProvider extends AdminHtmlRouteProvider {

  /**
   * Gets the canonical route.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   *
   * @return \Symfony\Component\Routing\Route|null
   *   The generated route, if available.
   */
  protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
    return $this->getEditFormRoute($entity_type);
  }

}

And set the newly created route provider in the Project class:

 *     "route_provider" = {
 *       "html" = "Drupal\project\Routing\EditFormAsCanonicalHtmlRouteProvider",
 *     }

We now have View and Edit tabs when accessing our content:

We just need to remove one of them. In the GitHub project referenced at the end of the article, I have chosen to remove the entity.project.edit_form tab and change the other tab’s title:

entity.project.view:
title: Edit
route_name: entity.project.canonical
base_route: entity.project.canonical

Replace the default collection page with a view

The auto-generated code for our entity includes a simple administration page for our entities at admin/content/project path. If this page is really used, in the long term we will require it to have filters, sorting and other whizzes. We will replace it with an administration view directly from the beginning.

This view should be of type Page and keep the same path admin/content/project as the original administration page. Don’t forget to set adequate permissions to it !!

Once created, our view will automatically replace the original page as intended. There’s no need to do any special configuration.

Refactor the generated code in a base class

When you need to create multiple of these custom entity type classes, it’s convenient to keep the entity type class slim and refactor common methods into a shared parent class which I will name CustomContentEntityBase. With this approach, all methods originally in the generated entity class should be moved to this new base class and the following code added to it:

abstract class CustomContentEntityBase extends ContentEntityBase {

  // All code previously contained in Project class
  // (..)
  
  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
  
    // All code previously baseFieldDefinitions method
    // (..)
    
    // And at the end of it ...
    get_called_class()::addCustomFields($fields);
    return $fields;
  }
  
  /**
   * Define entity custom fields and add them to the $fields array.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $fields
   */
  abstract protected static function addCustomFields(&$fields);
}

And then our entity class only requires to implement this addCustomFields method and any specific methods we want to add to it. We also need to keep into it the intact annotation.

src/Entity/Project.php:

/**
 * Keep all annotations here !!!
 */
class Project extends CustomContentEntityBase {

  /**
   * {@inheritdoc}
   */
  protected static function addCustomFields(&$fields) {

    // Define custom entity fields making use of BaseFieldDefinition::create() method

    // For example ...
    $fields['maintainers'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Maintainers'))
      ->setDescription(t('User ID of the project maintainers.'))
      ->setSetting('target_type', 'user')
      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
      ->setDisplayOptions('form', [
        'type' => 'entity_reference_autocomplete',
        'settings' => [
          'match_operator' => 'CONTAINS',
          'size' => 60,
          'placeholder' => '',
        ],
        'weight' => 15,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'entity_reference_label',
        'weight' => 15,
      ])
      ->setDisplayConfigurable('view', TRUE);
  }
}

Simplify the process of adding fields to our class

In the previous section, you can see that adding a single field to an entity is quite a daunting task. When doing it, you’ll find yourself copy-pasting many blocks of code and the class ends up with a big addCustomFields method not very practical.

On the other side, in the creation of a field, some of those method calls are usually repeated. That’s why I have built a small helper class which makes code in entity type class a bit more compact. You can find it on the project’s GitHub. I have mostly used it on Drupal 8 so maybe a method here and there needs to be adapted to Drupal 9. Anyway, here’s how the previous addCustomFields method would end up making use of that class:

/**
 * {@inheritdoc}
 */
protected static function addCustomFields(&$fields) {

  // Define custom entity fields making use of BaseFieldDefinition::create() method

  // For example ...
  $fields['maintainers'] = FieldDefinitionHelper::createReference()
    ->setLabel('Maintainers')
    ->setDescription(t('User ID of the project maintainers.'))
    ->setSetting('target_type', 'user')
    ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
}

Module repo

You can view the full code example in the project repository on GitHub.


Comment on this post