Drupal

Custom content types with Drupal Console

How to create a Drupal module that implements multiple custom content entity types using Drupal Console
Mehr erfahren

Setup and theme a custom Drupal content entity

How to create a Drupal module that implements multiple custom content entity types using Drupal Console
Datum
10.Mai 2020

Since I first met Drupal Console, I would never want to work on larger Drupal projects without this lovely little helper again. I can't image writing all those lines of code again and again manually, whenever I create a custom module, entity or whatever. 

Still there are a few steps I keep repeating after I used Drupal Console to generate a custom entity type. Most important,

  • how to "activate" the twig template and page callbacks for a custom Drupal entity,
  • placing the new entity's menu items in the correct submenus in the admin menu,
  • show the default theme instead of the administration theme when edting or adding a custom entity
  • and how to define nice URLs (like entity/1/edit) for add and edit forms and the entity page. 

 

This story will talk you through those modifications and explain how to set up a custom module that implements two dependend custom entity types. We are going to

  1. Create a custom module with Drupal Console
  2. Add multiple custom entity types using Drupal Console
  3. Alter the custom entity title/name field to allow up to 255 characters
  4. Define a twig template for the custom entity
  5. Reorganize the custom entities' menu items
  6. Rename links (URL aliases) for the custom entity's page, add and edit forms and
  7. Use the standard theme when a custom entity is added or edited.

 

 

1. create a custom module with Drupal Console

Use drupal console to generate a new module that implements our custom entity types.

drupal generate:module

Follow the on screen instructions and choose what suits your needs and setup:

Enter the new module name:
> WET Inventory

Enter the module machine name [wet_inventory]:
>

Enter the module Path [modules/custom]:
> modules/custom

Enter module description [My Awesome Module]:
> Some descriptions goes here ...

Enter package name [Custom]:
> lowfidelity

Enter Drupal Core version [8.x]:
>

Do you want to generate a .module file? (yes/no) [yes]:
>

Define module as feature (yes/no) [no]:
>

Do you want to add a composer.json file to your module? (yes/no) [yes]:
> no

Would you like to add module dependencies? (yes/no) [no]:
> yes

Module dependencies separated by commas (i.e. context, panels):
> some_module,another_module

Do you want to generate a unit test class? (yes/no) [yes]:
> no

Do you want to generate a themeable template? (yes/no) [yes]:
> no

Do you want proceed with the operation? (yes/no) [yes]:
>

Generated or updated files
Generation path: /work/www/wet/public_html/web
1 - /modules/custom/wet_inventory/wet_inventory.info.yml
2 - /modules/custom/wet_inventory/wet_inventory.module

Generated lines: 32

 

 

2. add multiple custom entity types using Drupal Console

This module will implement inventory tracking as part of a larger application. The module creates a "parent entity" that holds a list of child entities. If you are familiar with Drupal Commerce it can be compared to the way the shopping cart basically works. An inventory entity holds a list of products and the amount of articles that are on stock at a given date.  You won't have to think about that functionality too much, it's just to give you an idea, what's the use case in this example.

Now use Drupal Console to add a custom entity to the module we just created:

drupal generate:entity:content

Then follow the on screen instructions:

 Enter the module name [address]:
 > wet_inventory

 Enter the class of your new content entity [DefaultEntity]:
 > InventoryEntity

 Enter the machine name of your new content entity [inventory_entity]:
 > 

 Enter the label of your new content entity [Inventory entity]:
 > Inventory

 Enter the base-path for the content entity routes [/admin/structure]:
 > 

 Do you want this (content) entity to have bundles? (yes/no) [no]:
 > 

 Is your entity translatable? (yes/no) [yes]:
 > 

 Is your entity revisionable? (yes/no) [yes]:
 > no

 Do you want this (content) entity to have forms? (yes/no) [yes]:
 > 

 Do you want this (content) entity to have an owner? (yes/no) [yes]:
 > 

Generated or updated files
 Generation path: /work/www/wet/public_html/web
 1 - modules/wet/wet_inventory/wet_inventory.permissions.yml
 2 - modules/wet/wet_inventory/src/InventoryEntityAccessControlHandler.php
 3 - modules/wet/wet_inventory/src/InventoryEntityTranslationHandler.php
 4 - modules/wet/wet_inventory/src/Entity/InventoryEntityInterface.php
 5 - modules/wet/wet_inventory/src/Entity/InventoryEntity.php
 6 - modules/wet/wet_inventory/src/Entity/InventoryEntityViewsData.php
 7 - modules/wet/wet_inventory/src/InventoryEntityListBuilder.php
 8 - modules/wet/wet_inventory/src/InventoryEntityHtmlRouteProvider.php
 9 - modules/wet/wet_inventory/wet_inventory.links.menu.yml
 10 - modules/wet/wet_inventory/wet_inventory.links.task.yml
 11 - modules/wet/wet_inventory/wet_inventory.links.action.yml
 12 - modules/wet/wet_inventory/src/Form/InventoryEntitySettingsForm.php
 13 - modules/wet/wet_inventory/src/Form/InventoryEntityForm.php
 14 - modules/wet/wet_inventory/src/Form/InventoryEntityDeleteForm.php
 15 - modules/wet/wet_inventory/templates/inventory_entity.html.twig
 16 - modules/wet/wet_inventory/inventory_entity.page.inc

                                                                                                                        
 Generated lines: 698  

This will add a bunch of new files implementing the custom "parent entity". Then repeat this step to add the inventory line items, that - in my use case - will be referenced from our parent entity.

drupal generate:entity:content

Once again, follow the on screen information to setup your second custom entity type.

 Enter the module name [address]:
 > wet_inventory

 Enter the class of your new content entity [DefaultEntity]:
 > InventoryLineEntity

 Enter the machine name of your new content entity [inventory_line_entity]:
 > 

 Enter the label of your new content entity [Inventory line entity]:
 > Inventory line

 Enter the base-path for the content entity routes [/admin/structure]:
 > 

 Do you want this (content) entity to have bundles? (yes/no) [no]:
 > 

 Is your entity translatable? (yes/no) [yes]:
 >   

 Is your entity revisionable? (yes/no) [yes]:
 > no

 Do you want this (content) entity to have forms? (yes/no) [yes]:
 > 

 Do you want this (content) entity to have an owner? (yes/no) [yes]:
 > 

Generated or updated files
 Generation path: /work/www/wet/public_html/web
 1 - modules/wet/wet_inventory/wet_inventory.permissions.yml
 2 - modules/wet/wet_inventory/src/InventoryLineEntityAccessControlHandler.php
 3 - modules/wet/wet_inventory/src/InventoryLineEntityTranslationHandler.php
 4 - modules/wet/wet_inventory/src/Entity/InventoryLineEntityInterface.php
 5 - modules/wet/wet_inventory/src/Entity/InventoryLineEntity.php
 6 - modules/wet/wet_inventory/src/Entity/InventoryLineEntityViewsData.php
 7 - modules/wet/wet_inventory/src/InventoryLineEntityListBuilder.php
 8 - modules/wet/wet_inventory/src/InventoryLineEntityHtmlRouteProvider.php
 9 - modules/wet/wet_inventory/wet_inventory.links.menu.yml
 10 - modules/wet/wet_inventory/wet_inventory.links.task.yml
 11 - modules/wet/wet_inventory/wet_inventory.links.action.yml
 12 - modules/wet/wet_inventory/src/Form/InventoryLineEntitySettingsForm.php
 13 - modules/wet/wet_inventory/src/Form/InventoryLineEntityForm.php
 14 - modules/wet/wet_inventory/src/Form/InventoryLineEntityDeleteForm.php
 15 - modules/wet/wet_inventory/templates/inventory_line_entity.html.twig
 16 - modules/wet/wet_inventory/inventory_line_entity.page.inc

                                                                                                                        
 Generated lines: 698  

 

At this point you already have a new module and two custom entity types that will be available, when enabling the module. But don't enable the module yet, please.

Before that we'll make a tiny modification to the custom entities' code. If you do your modifications to the name field or add additional fields to your entity AFTER enabling the module you'll either have to uninstall and reenable the module for your changes to take effect, or you'll have to install the Devel Entity Updates module in order to update your database schema. Drush used to ship with entup function, but that was meanwhile removed.

 

 

3. alter the custom entity title/name field
to allow up to 255 characters

When using Drupal Console to generate a custom entity, it will set the max_length of the title field to 50 characters. Actually console doesn't create a Ttile field, but a Name field instead. But that's just a matter of naming that field. I usually change that to 255 characters or something like that. This is not necessary, if 50 characters is enough for your entity titles but in my use case I automatically created entity names containing the inventory date and the product name for each inventory line.
If you also want to be able to create longer titles, you'll find the code in wet_inventory/src/Entity/InventoryEntity.php and wet_inventory/src/Entity/InventoryLineEntity.php at around line 170:

$fields['name'] = BaseFieldDefinition::create('string')
  ->setLabel(t('Name'))
  ->setDescription(t('The name of the Inventory line entity.'))
  ->setSettings([
    'max_length' => 50, // Change this to the required max title length.
    'text_processing' => 0,
  ])
  ->setDefaultValue('')
  ->setDisplayOptions('view', [
    'label' => 'above',
    'type' => 'string',
    'weight' => -4,
  ])
  ->setDisplayOptions('form', [
    'type' => 'string_textfield',
    'weight' => -4,
  ])
  ->setDisplayConfigurable('form', TRUE)
  ->setDisplayConfigurable('view', TRUE)
  ->setRequired(TRUE);

 

Now go ahead, enable the new module and - just to be sure - rebuild your caches. 

drush en wet_inventory
drush cr

 

 

4. define a twig template for the custom entity

When you generate a custom content type using Drupal Console you have to select a module to which the custom entity will be added. A twig template file for each custom entity will be created in the module's templates directory. Also a page.inc file that implements the custom entity's page callback is generated in the module dir. The page.inc file is the place, where the variables, that are available in the twig template, are defined.

In our example we end up with two page callback files

  • modules/wet/wet_inventory/inventory_entity.page.inc
  • modules/wet/wet_inventory/inventory_line_entity.page.inc

and two twig templates

  • modules/wet/wet_inventory/templates/inventory-entity.html.twig
  • modules/wet/wet_inventory/templates/inventory-line-entity.html.twig

Those files are a nice starting point, but they have two problems:

  1. The custom entity's page.inc file is not included, so the template_preprocess_hook is never fired and
  2. the file name of the twig templates is incorrect, if a custom entity's name contains an underscore.

So let's fix that!

 

IMPLEMENT HOOK_THEME

Implement hook_theme in your .module file. In our example modules/wet/wet_inventory/wet_inventory.module:

/**
 * Implements hook_theme().
 *
 * Register theme implementations for the two entity types that are included in the wet_orders module.
 * See API docs at https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/function/hook_theme/8.2.x
 */
function wet_inventory_theme() {

  $theme = [

    // Define page callback and theme for the Inventory entity
    'inventory_entity' => [
      'render element' => 'elements',
      // The file implementing the page callback
      'file' => 'inventory_entity.page.inc',
      // Define the twig template file name. Note .html.twig will be added and underscores are converted to dashes.
      // so inventory_entity will point to inventory-entity.html.twig.
      # 'template' => 'inventory_entity', // This is the default setting, only change if twig template file name differs.
    ],

    // Define page callback and theme for the Inventory Line entity
    'inventory_line_entity' => [
      'render element' => 'elements',
      'file' => 'inventory_line_entity.page.inc',
      # 'template' => 'inventory_line_entity',
    ],
  ];

  return $theme;
}

At this point the hook_preprocess hook in our page.inc file, template_preprocess_inventory_entity(...) will be properly called, but we'll still probably end up with a WSOD.

 

CORRECT THE TWIG TEMPLATE FILENAME

If you clear your caches now (drush cr) and reload an inventory_entity page (ie. /admin/structure/inventory_entity/1) you will end up with a WSOD and an error message like

Twig\Error\LoaderError: Template "modules/wet/wet_inventory/templates/inventory_entity.html.twig" is not defined. in Twig\Loader\ChainLoader->getCacheKey() (line 142 of /work/www/wet/public_html/vendor/twig/twig/src/Loader/ChainLoader.php).

If the custom entity system name contains an underscore, like inventory_line_entity, the correct filename for the twig template would be inventory-line-entity.html.twig. Drupal Console creates a nice template but uses underscores instead of dashes, ie. inventory_line_entity.html.twig in the filename. Simply rename the file and replace all underscores with dashes.

 

 

5. reorganize the custom entities' menu items

Each entity type will supply a list that shows all of your custom entities and a button to create a new entity. Also a settings form that allows adding new fields or customizing view and form modes for your entity is available. More or less like the list and settings form of standard node bundles you're used to. The problem is, all those links will be added to admin/structure. If you're working on a project with a couple of custom entity types this will quickly clutter up your admin menu and the Structure page.

For our newly created entities we'll find four new links on the admin/structure page and - even worse - in the administration menu. We'll have to clean up that mess. 

Custom Drupal entities admin menu routes

Let's create a parent menu item "WET Inventory" and move both settings forms menu entries there. The two pages that list inventory and inventory line entities will get moved to the admin/content page, where the other content lists live. Also create a parent menu entry "WET Inventory" there.

 Create a new route for the "WET Inventory" parent menu entries by adding the file modules/wet/wet_inventory/wet_inventory.routing.yml. If a user clicks on the parent menu entry just open /admin/structure.

# Parent menu item for Inventory entitites.
# If a user clicks on the parent menu entry just open /admin/structure.
wet.admin_inventory:
  path: '/admin/structure'
  defaults:
    _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
    _title: 'WET Inventory'
  #requirements:
    #_permission: 'some permissions here'


# Parent menu item for Inventory LINE entities.
# If a user clicks on the parent menu entry just open /admin/structure.
wet.content_inventory:
  path: '/admin/content'
  defaults:
    _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
    _title: 'WET Inventory'
  #requirements:
    #_permission: 'some permissions here'

 

Then head over to modules/wet/wet_inventory/wet_inventory.links.menu.yml, add definitions for the two new parent menu entries and push the other menu entries into them.

# Inventory STRUCTURE parent menu item. See .routing.yml.
wet.admin_inventory:
  title: 'WET Inventory'
  route_name: wet.admin_inventory
  parent: system.admin_structure
  description: 'Administer and configure WET Inventory Items.'
  weight: 100

# Inventory CONTENT parent menu item. See .routing.yml.
wet.content_inventory:
  title: 'WET Inventory'
  route_name: wet.content_inventory
  parent: system.admin_content
  description: 'Administer and configure WET Inventory Items.'
  weight: 100

# Inventory menu items definition
entity.inventory_entity.collection:
  title: 'WET Inventory'
  route_name: entity.inventory_entity.collection
  description: 'List Inventory entities'
  # Move this to the admin content menu/page.
  parent: wet.content_inventory
  weight: 100

inventory_entity.admin.structure.settings:
  title: 'WET Inventory settings'
  description: 'Configure Inventory entities'
  route_name: inventory_entity.settings
  # Move this to our parent menu item WET Inventory.
  parent: wet.admin_inventory
  weight: 100

# Inventory line menu items definition
entity.inventory_line_entity.collection:
  title: 'WET Inventory Lines'
  route_name: entity.inventory_line_entity.collection
  description: 'List Inventory line entities'
  # Move this to the admin content menu/page.
  parent: wet.content_inventory
  weight: 100

inventory_line_entity.admin.structure.settings:
  title: 'WET Inventory Line'
  description: 'Configure Inventory line entities'
  route_name: inventory_line_entity.settings
  # Move this to our parent menu item WET Inventory.
  parent: wet.admin_inventory
  weight: 100

After updating routes you usually want to call drush cc router or drush cr to refresh your caches.

 

Custom drupal entities and routes


 

 

6. rename links (URL aliases) for the custom entity's page, add and edit forms

If you used the default base route settings when creating your custom entity types, the paths to your content type will look something like that:

  • /admin/structure/inventory_entity/add
  • /admin/structure/inventory_entity/{inventory_entity}  or
  • /admin/structure/inventory_entity/{inventory_entity}/edit and so on.

Let's change that to something short and meaningful like 

  • /inventory/add
  • /inventory/13
  • /inventory/23/edit .

You can find those links defined in the defintion comments in your InventoryEntity.php and InventoryLineEntity.php at around line 50. We only move the link to the list of entities to /admin/content. 

 *   },
 *   links = {
 *     "canonical" = "/inventory/{inventory_entity}",
 *     "add-form" = "/inventory/add",
 *     "edit-form" = "/inventory/{inventory_entity}/edit",
 *     "delete-form" = "/inventory/{inventory_entity}/delete",
 *     "collection" = "/admin/content/inventory_entity",
 *   },
 *   field_ui_base_route = "inventory_entity.settings"
 * )
 */

 

 

7. show the standard theme when a custom entity is added or edited

If you configured your site to use the default theme when you add or edit a new node, you might have noticed that custom entities created with Drupal Console always show the administration theme when adding or editing an entity. They don't seem to care too much about your settings. To change that add a line in the Html Route Provider of your new entitites. In this example modules/wet/wet_inventory/src/InventoryEntityHtmlRouteProvider.php and modules/wet/wet_inventory/src/InventoryLineEntityHtmlRouteProvider.php at around line 15. Don't extend AdminHtmlRouteProvider but instead select the DefaultHtmlRouteProvider. The AdminHtmlRouteProvider always sets the _admin_route option to TRUE.

Before you proceed: If you didn't customize the links to the edit or add forms (see previous section) you'll have to do that first. Make sure the ADD NEW link points to /inventory/add instead of /admin/content/inventory_entity/add

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider; // <-- Look here
use Symfony\Component\Routing\Route;

/**
 * Provides routes for Inventory line entities.
 *
 * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
 * @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
 */
class InventoryLineEntityHtmlRouteProvider extends DefaultHtmlRouteProvider { // <-- Look here

  /**
   * {@inheritdoc}
   */
  public function getRoutes(EntityTypeInterface $entity_type) {
    $collection = parent::getRoutes($entity_type);

As we still want the settings forms to be displayed using the administration theme, we don't touch the getSettingsFormRoute function's:

->setRequirement('_permission', $entity_type->getAdminPermission()) 
->setOption('_admin_route', TRUE);

 

 

Some further reading

hook_theme API reference

Jim Conte's Themeing A Custom Content Entity

The discussion Template preprocessing isn't triggered for custom entities in the Drupal issue queue

Chapter 9.11. Drupal 8 Entity API. Create custom Entity type. Generate Entity type using Drupal Console at drupalbook.org. 

 

 

 


 

 

 

Credits
CoV Holidays 2020
We are all mad here
Zurück zur Übersicht