Picture
Ryan Loos Senior Drupal Developer
April 30, 2024


The “Drush generate” command is one of my favorite Drupal features. Scaffolding out boilerplate code with a few keystrokes feels great and it helps me avoid those tiny, hard-to-find human mistakes.

For most of my career, it just sat under the radar as another tool I could pull out for very specific tasks, like starting a new custom module or building a service file. That all changed a few months ago when I figured out how to harness the generator API within Drupal. What I found will help you create your own custom code generator.

The Task

It all happened as I was helping a client migrate their data out of Drupal. The plan was to expose the relevant Drupal data as JSON, so that it could be consumed by a third party API. In this case, JSON:API wasn't going to cut it because I needed really granular control over both the fields and values that I was exposing.

After some research I decided to build a custom plugin system in order to make the data as flexible as possible while also having some solid and consistent rules. As an example, here is the plugin I used to expose the News node:


<?php

namespace Drupal\custom_api\Plugin\SfgApi\Node;

use Drupal\custom_api\Plugin\SfgApi\ApiFieldHelperTrait;

/**
 * Plugin implementation of the custom_api.
 *
 * @CustomApi(
 *   id = "node_news",
 *   title = @Translation("Node news"),
 *   drupal_bundle = "news",
 *   destination_bundle = "News",
 *   entity_id = {},
 *   langcode = {},
 *   is_stub = {},
 * )
 */
class News extends CustomApiNodeBase {

  use ApiFieldHelperTrait;

  /**
   * {@inheritDoc}
   */
  public function setCustomData($entity) {
    return [
      'headline' => $entity->get('title')->value,
      'date' => $this->convertDateFromFormat('Y-m-d\TH:i:s', $entity->get('field_date')->value, 'Y-m-d'),
      'image' => $this->getReferencedEntity($entity->get('field_image')->referencedEntities(), FALSE, TRUE),
      'redirect_url' => $entity->get('field_direct_external_url')->uri ?: '',
      'abstract' => $entity->get('field_abstract')->value ?: '',
      'body' => $entity->get('body')->value ?: '',
      'news_type' => $this->editFieldValue($entity->get('field_news_type')->value, ['press release' => 'press_release']),
      'topics' => $this->getReferencedEntity($entity->get('field_topics')->referencedEntities()),
      'partner_agencies' => $this->getReferencedEntity($entity->get('field_departments')->referencedEntities()),
    ];
  }

}


There’s a lot going on under the hood here, so don’t worry about fully understanding this file. At its core this plugin does the following:

    •    It represents a specific bundle, with each plugin representing a different bundle.
    •    It contains some basic data for the bundle’s name and destination within the annotation.
    •    It allows its functions to extract data from the entity in a very customizable way.

Once I had this basic structure working, I started to think about expanding it to the other content I needed to export, specifically the other 70-plus types of nodes, paragraphs, media, all of which would need their own custom plugin. Well, never mind then.

Digging Around in Drush

I was happy with my plugin system, but I didn’t want to manually write the dozens of files it would eventually need. While thinking about the issue, I recalled that I had built the module in the first place by answering some questions in a “Drush generate” command. Could I do the same for my plugin? A readme buried in the Drush repo suggested I could. 

My Turn

At its most basic, the Drupal code generator asks the user some questions and then uses the answers provided to name and populate a Twig template and place that file in the correct directory. By following the aforementioned example and others in the codebase, I created my own command, the details of which I will explain. You can browse the full file here and the fully functional module here.

The first section is very similar to normal plugin annotation and gives the command a name, description, alias, et cetera. Crucially, it also provides a path for it to find the template file that it will be using. In this case, the template file is in the same directory. 


/**
 * Generates an custom API plugin.
 */
#[Generator(
    name: 'custom:api-plugin',
    description: 'Generates an custom API plugin',
    aliases: ['cap'],
    templatePath: __DIR__,
    type: GeneratorType::MODULE_COMPONENT,
)]

The next step is to define the class and extend the BaseGenerator, which provides the necessary functionality. Then you add the AutowireTrait, which is important for getting the command to actually register with Drush. Finally, you inject some dependencies that you’ll need for the actual code.


class ApiPluginGenerator extends BaseGenerator {

  use AutowireTrait;

  /**
   * Inject dependencies into the Generator.
   */
  public function __construct(
        protected EntityTypeBundleInfoInterface $entityTypeBundleInfo,
        protected EntityFieldManagerInterface $entityFieldManager,
    ) {
    parent::__construct();
  }

The pièce de résistance of this whole show is the generate function. To start, you instantiate an interviewer, which will allow you to ask the user questions and store their answers:
    1    Pick an entity type (node, paragraph, or media)
    2    Enter the machine name for the bundle
    3    Add a custom trait? Yes or no
    4    Try to generate the fields programmatically? Yes or no

(Note: I am only using basic questions and there are plenty more elaborate ones in the interviewer code.)

The answers then do a bit more processing and are added to an existing “$vars” argument. Then at the bottom you use the “addFile” function to generate the final file. Note that “addFile” requires two arguments, a path to create the file, and a Twig template to use. (It passes the “$vars” array to that template.) 


protected function generate(array &$vars, Assets $assets): void {
  $ir = $this->createInterviewer($vars);
  $vars['machine_name'] = 'custom_api';
  $vars['entity_type'] = $ir->choice('Plugin entity type', [
    'node' => 'Node',
    'paragraph' => 'Paragraph',
    'media' => 'Media',
  ], 'node', FALSE);
  $vars['bundle'] = $ir->ask('Bundle machine name');
  $vars['use_helper'] = $ir->confirm('Use helper trait?');
  $vars['generate_fields'] = $ir->confirm('Add all fields from the entity?');

  $vars['bundle_camelize'] = Utils::camelize($vars['bundle']);
  $vars['entity_type_ucfirst'] = ucfirst($vars['entity_type']);

  $bundle_list = $this->entityTypeBundleInfo->getBundleInfo($vars['entity_type']);
  // Check if the bundle exists for the specified entity type.
  $vars['bundle_exists'] = in_array($vars['bundle'], array_keys($bundle_list));

  if ($vars['generate_fields'] && $vars['bundle_exists']) {
    $fields = $this->entityFieldManager->getFieldDefinitions($vars['entity_type'], $vars['bundle']);

    $vars['entity_fields'] = [];
    foreach ($fields as $field_name => $field_definition) {
      $vars['entity_fields'][] = $field_name;
    }
  }

  $assets->addFile('src/Plugin/CustomApi/{entity_type_ucfirst}/{bundle_camelize}.php', 'PluginTemplate.twig');
}

Once it's properly namespaced and the module is enabled, you will be able to run “drush generate custom:api-plugin” and get the following, which generates a full plugin very similar to the example I showed you earlier.

code


Versioning Disclaimer

Before trying to create your own generators, you need to figure out which version of Drush you’re using. The example in the above code works for Drush 12x and later. If you’re using 11x, the command file will need to be registered with a “drush.services.yml” file, and will look a bit different. These differences are documented here. Also, you can find an example of the above command written for 11x here

Try This at Home

This custom code generator saved me so much time and drudgery, and now that I know it exists, I suspect I'll return to it in the future. Maybe next time I need to coordinate a big migration I can use this to create the migration files much faster. Hopefully, by seeing how easy and powerful this functionality is you've been inspired to harness it yourself.

Do you enjoy messing around with code and building custom solutions to knotty Drupal problems? If you found yourself nodding your head reading this post, you should definitely check out our careers page. At Chapter Three, we’re always looking for smart, creative people to join our small but powerful and close-knit team. You might be just the solution we’re looking for!