Skip to main content
Joachim's blog

Main navigation

  • Home
  • About

Blog

The big plugin attribute change-over made easy

By joachim, Tue, 02/09/2025 - 13:17

Attributes are here and they're great. I hated annotations; they were a necessary evil, but putting working code into comments just felt wrong.

But the conversion work is still ongoing: many contrib modules (and perhaps custom ones too) need to convert their plugin types to being attribute-based. There is time to do this: code in \Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations announces that attributes on plugins are required from Drupal 12, with the old-style annotation allowed to remain alongside it for backwards-compatibility until Drupal 13.

But the sooner you convert plugin types to attributes, the sooner you benefit from imported classes in definition values, the ability to put comments within the annotation, cleaner translation of labels, and use of PHP constants in attribute keys or values. And IDE autocompletion of the plugin definition keys, if you're not using Module Builder to generate your plugins (why, though? why?).

With Drupal Rector you can convert individual plugins from annotation to attribute, but to convert a plugin type, you need more than that.

Module Builder can help with this with its Adoption feature. This adds items to your module config based on the existing code, which allows you to alter or add to the code definition before re-generating it. (This feature is particularly useful for adding further injected services to an existing class such as a plugin or a form.)

Release 4.5.4 of Drupal Code Builder (the library that powers Module Builder) added adoption of plugin types, and this opens the door to converting a plugin type from annotation to attribute. The steps are are follows:

  1. Go to Administration › Configuration › Development › Module Builder.
  2. If your module is not already defined in Module Builder, click 'Adopt existing module', then find your module in the list and click 'Adopt module and adopt components'. If you already have the module in Module Builder, go to your module's 'Adopt' tab.
  3. On the adopt components form, select the plugin types you want to convert.
  4. Click 'Adopt components'.
  5. Go to the 'Plugins' tab of your module. Your plugin types should be in the form.
  6. Change the discovery type of each one to 'Attribute plugin'. The values will carry across (this is actually a problem with FormAPI that I've never managed to figure out, but it's actually quite handy).
  7. Go to the Generate tab.
  8. Select the files to write. For each plugin type, you'll want the plugin manager, and the new attribute class.

You should verify the new attribute class. Properties will have been adapted from the annotation class but they may not all be correct. In particular, default values for optional properties aren't yet handled by Module Builder, so you'll need to add those.

Once you've tidied up the attribute class, your plugin type is now attribute-enabled. (You should run the tests that cover plugins just to make sure everything is fine.)

The plugins of this type in your module are still using annotations, so now we turn to Rector.

There is full documentation on converting plugins but the gist of it is this:

$rectorConfig->ruleWithConfiguration(\DrupalRector\Drupal10\Rector\Deprecation\AnnotationToAttributeRector::class, [
  new \DrupalRector\Drupal10\Rector\ValueObject\AnnotationToAttributeConfiguration(
    '10.0.0',
    '10.0.0',
    'MyPluginType',
    'Drupal\my_module\Attribute\MyPluginType',
  ),
]);

Run rector on just your plugin folders to make it go quicker, since you know where the plugin files are:

vendor/bin/rector process path/to/module/src/Plugin/MyPluginType

That converts all the annotations, but unfortunately, it doesn't format them properly: the whole thing ends up all on one line. And PHPCS and PHPCBF don't know how to format an attribute either.

But regular expressions come to the rescue, and fortunately the syntax of attributes is fairly distinct.

In your IDE, a search of '(?=\b\w+: )' replaced with '\n ' (note two spaces, for the indent) will put each property onto its own line. It won't handle array values though. And beware: it will catch the use of colons in text strings! This is an instance where using a GUI for git makes quick work: stage the fixes to the attributes, discard everything else.

That leaves just the terminal closing bracket, which you can do by hand, or cook up a regex if you have a lot of plugin classes.

Tags

  • module builder
  • plugins
  • Rector
  • deprecation
  • regex

Drupal on cPanel: Confusion, Pain, and Never-Ending Lousiness

By joachim, Wed, 27/08/2025 - 20:18

My site is back up, after a brief interlude: I moved hosts, because Gandi doubled their hosting costs. I'd left it quite late to renew, because up until now it's just been a case of ticking a box and paying money, so I didn't have much time to shop around.

I chose Krystal, because they use green energy and their procedure for transferring my domain seemed simple (there was a German hosting firm I was recommended too, but that one said something about filling in a form in a PDF and it looked horrendous).

I was warned about their use of cPanel, but I went ahead anyway, and now I can say that yes, the warning was justified.

CPanel is a mish-mash of different things, none of which join up properly. So for instance, it has a feature for creating git repositories on the server and using them to deploy your code, but that bit doesn't tell you to set up ssh keys first. You have to know. Then on the git repository management page, the URL for cloning the repository is wrong. When I opened a support ticket, I got a quick response with the correct URL, but when I suggested the information in cPanel could be fixed, I was told that the URL it shows can't be changed.

It's like someone took a wheelbarrow full of random tools and tipped it all out in front of you. Nothing works together, nothing is co-ordinated.

One of them is Softaculous, a tool which promises to install any of a bunch of web apps. I tried getting it to install Drupal before I copied over my own codebase, just in case it knew some things I didn't. It installed it with the legacy folder structure: Drupal in the project root, rather than in a /web folder. I know that technically works, but I didn't feel like totally restructuring my codebase for that.

So the first real Drupal problem was the web root. It was set up as /public_html, and the documentation for git deployment said it would deploy into that folder. I opened a support ticket to ask about changing the web root to /public_html/web, for Drupal's folder structure, and I was told that it was impossible on my cheap hosting plan; get a VPS they said. But that's overkill for this site, which gets very little traffic.

That left me with two options: restructure my codebase to follow the legacy Drupal code structure, with the web root in the project root, or add some sort of redirect. The latter is fairly easy to do with an .htaccess file in the project root, once you know how. I found this in an old forum post:

# Redirect to the actual webroot.
# Because cpanel is crap.
# @see https://www.drupal.org/hosting-support/2021-09-21/unable-to-set-document-root-as-public_htmlweb
RewriteEngine on
RewriteCond %{HTTP_HOST} ^(www.)?noreiko.com$
RewriteCond %{REQUEST_URI} !^/www/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /www/$1
RewriteCond %{HTTP_HOST} ^(www.)?noreiko.com$
RewriteRule ^(/)?$ www/index.html [L]

That worked with a simple test of a plain HTML page, and so I deployed my site. Hurrah! Except no: form submissions weren't working: submitting a form took me to an error page complaining about the redirect being external.

Fortunately, once again I found a fix, this time in a core issue. This code goes in a settings.php file. In my case, I put it in a settings.prod.php file, since I don't need it to run on my local development copy of the site.

// Needed for cPanel crapness.
// See https://www.drupal.org/project/drupal/issues/2612160#comment-11767977
if (isset($GLOBALS['request']) && '/htdocs/index.php' === $GLOBALS['request']->server->get('SCRIPT_NAME')) {
  $GLOBALS['request']->server->set('SCRIPT_NAME', '/index.php');
}
elseif (isset($GLOBALS['request']) && '/htdocs/update.php' === $GLOBALS['request']->server->get('SCRIPT_NAME')) {
  $GLOBALS['request']->server->set('SCRIPT_NAME', '/update.php');
}

The same fix needs to be done for update.php as well. There's probably a neat way to cover both cases in one go, and also any other PHP entry points (though I think statistics.php is gone?) but I'd run out of energy by then.

Drupal runs fine in a subfolder on my laptop, where I still use MAMP and run each local site in its on own folder in the web root. (Although Tome, the Drupal static site generator doesn't handle that properly, which is why I abandoned the idea of moving my site to github pages). So it's probably something caused by the Apache redirect I'd put it; but once I got it working I didn't experiment further.

So, my site is up and running again, as you can see.

But no thanks to the proprietary software that runs the hosting.

Tags

  • hosting

Changing your mind about dependency injection

By joachim, Thu, 24/10/2024 - 11:25

When I start writing a class that has a dependency injection, I have a clear idea about which services it needs. I generate it -- the plugin, form, controller, or service -- and specify those services.

Then nearly always, unless it's something really very simple, I find that no matter how much I thought about it and planned it, I need to add more services. Maybe remove some too.

Fortunately, because Module Builder saves the configuration of the module code you've generated, it's easy to go back to it and edit it to add more services:

  1. Edit your module in Module Builder
  2. Add to the injected services for your component
  3. Ensure your code file is committed to version control
  4. Generate the code, and write the updated version of the code file
  5. Add and commit the new DI code, while discarding the changes that remove your code. (I find it helps to use a git GUI for things like this, though git add -p works too.)

But I tend to find that I make this mistake several times as the class develops, and so I adopt the approach of using the \Drupal::service() function to get my services, and only when I'm fairly confident I'm not going to need to make any more changes to DI, I update the injected services in one go, converting all the service calls to use the service properties.

I was asked yesterday at Drupal Drinks about how to do that, and it occurred to me that there's a way of doing this so after you've updated the dependency injection with Module Builder, it's a simple find and replace to update your code.

If you write your code like this whenever you need a service:

$service_entityTypeManager = \Drupal::service('entity_type.manager');
$stuff = $service_entityTypeManager->doSomething();

Then you need to do only two find and replace operations to convert this to DI:

  1. Replace '^.+Drupal::service.+\n' with ''. This removes all the lines where you get the service from the Drupal class.
  2. Replace '\$service_(\w+)' with '$this->$1'. This replaces all the service variables with the class property.

Up until now I'd been calling the service variables something like $entityTypeManager so that I could easily change that to $this->entityTypeManager manually, but prefixing the variable name with a camel case 'service_' gives you something to find with a regular expression.

If you want to be really fancy, you can use a regular expression like '(?<=::service..)[\w.]+' (using dots to avoid having to escape the open bracket and the quote mark) to find all the services that you need to add to the class's dependency injection.

Something like this:

$ ag -G MyClass.php '(?<=::service..)[\w.]+' -o --nonumbers --nofilename | sort | uniq | tr "\n" ", "

will give you a list of service names that you can copy-paste into the Module Builder form. This is probably overkill for something you can do pretty quickly with the search in a text editor or IDE, but it's a nice illustration of the power of unix tools: ag has options to output just the found text, then sort and uniq eliminate duplicates, and finally tr turns it into a comma-separated list.

Tags

  • dependency injection
  • Drupal Code Builder
  • module builder

Refactoring with Rector

By joachim, Fri, 03/05/2024 - 20:47

Rector is a tool for making changes to PHP code, which powers tools that assist with upgrading deprecated code in Drupal. When I recently made some refactoring changes in Drupal Code Builder, which were too complex to do with search and replace regexes, it seemed like a good opportunity to experiment with Rector, and learn a bit more about it.

Besides, I'm an inveterate condiment-passer: I tend to prefer spending longer on a generalisation of a problem than on the problem itself, and the more dull the problem and the more interesting the generalisation, the more the probability increases.

So faced with a refactoring from this return from the getFileInfo() method:

    return [
      'path' => '',
      'filename' => $this->component_data['filename'],
      'body' => $body,
      'merged' =>$merged,
    ];

to this:

    return new CodeFile(
      body_pieces: $body,
      merged: $merged,
    );

which was going to be tedious as hell to do in a ton of files, obviously, I elected to spend time fiddling with Rector.

The first thing I'll say is that the same sort of approach as I use with migrations works well: work with a small indicative sample, and iterate small changes. With a migration, I will find a small number of source rows which represent different variations (or if there is too much variation, I'll iterate the iteration multiple times). I'll run the migration with just those sources, examine the result, make refinements to the migration, then roll back and repeat.

With Rector, you can specify just a single class in the code that registers the rule to RectorConfig in the rector.php file, so I picked a class which had very little code, as the dump() output of an entire PHP file's PhpParser analysis is enormous.

You then use the rule class's getNodeTypes() method to declare which node types you are interested in. Here, I made a mis-step at first. I wanted to replace Array_ nodes, but only in the getFileInfo() method. So in my first attempt, I specified ClassMethod nodes, and then in refactor() I wrote code to drill down into them to get the array Array_ nodes. This went well until I tried returning a new replacement node, and then Rector complained, and I realised the obvious thing I'd skipped over: the refactor() method expects you to return a node to replace the found node. So my approach was completely wrong.

I rewrote getNodeTypes() to search for Array_ nodes: those represent the creation of an array value. This felt more dangerous: arrays are defined in lots of places in my code! And I haven't been able to see a way to determine the parentage of a node: there do not appear to be pointers that go back up the PhpParser syntax tree (it would be handy, but would make the dump() output even worse to read!). Fortunately, the combination of array keys was unique in DrupalCodeBuilder, or at least I hoped it was fairly unique. So I wrote code to get a list of the array's keys, and then compare it to what was expected:

        foreach ($node->items as $item) {
            $seen_array_keys[] = $item->key->value;
        }
        if (array_intersect(static::EXPECTED_MINIMUM_ARRAY_KEYS, $seen_array_keys) != static::EXPECTED_MINIMUM_ARRAY_KEYS) {
            return NULL;
        }

Returning NULL from refactor() means we aren't interested in this node and don't want to change it.

With the arrays that made it through the filter, I needed to make a new node that's a class instantiation, to replace the array, passing the same values to the new statement as the array keys (mostly).

Rector's list of commonly used PhpParser nodes was really useful here.

A new statement node is made thus:

use PhpParser\Node\Name;
use PhpParser\Node\Expr\New_;

        $class = new Name('\DrupalCodeBuilder\File\CodeFile');
        return new New_($class);

This doesn't have any parameters yet, but running Rector on this with my sample set showed me it was working properly. Rector has a dry run option for development, which shows you what would change but doesn't write anything to files, so you can run it over and over again. What's confusing is that it also has a cache; until I worked this out I was repeatedly confused by some runs having no effect and no output. I have honestly no idea what the point is of caching something that's designed to make changes, but there is an option to disable it. So the command to run is: $ vendor/bin/rector --dry-run --clear-cache. Over and over again.

Once that worked, I needed to convert array items to constructor parameters. Fortunately, the value from the array items work for parameters too:

use PhpParser\Node\Arg;

        foreach ($node->items as $array_item) {
                $construct_argument = new Arg(
                   $array_item->value,
                );

That gave me the values. But I wanted named parameters for my constructor, partly because they're cool and mostly because the CodeFile class's __construct() has optional parameters, and using names makes that simpler.

Inspecting the Arg class's own constructor showed how to do this:

use PhpParser\Node\Arg;
use PhpParser\Node\Identifier;

                $construct_argument = new Arg(
                    value: $array_item->value,
                    name: new Identifier($key),
                );

Using named parameters here too to make the code clearer to read!

It's also possible to copy over any inline comments that are above one node to a new node:

            // Preserve comments.
            $construct_argument->setAttribute('comments', $array_item->getComments());

The constructor parameters are passed as a parameter to the New_ class:

        return new New_($class, $new_call_args);

Once this was all working, I decided to do some more refactoring in the CodeFile class in DrupalCodeBuilder. The changes I was making with Rector made it more apparent that in a lot of cases, I was passing empty values. Also, the $body parameter wasn't well-named, as it's an array of pieces, so could do with a more descriptive name such as $body_pieces.

Changes like this are really easy to do (though by this point, I had made a git repository for my Rector rule, so I could make further enhancements without breaking what I'd got working already).

        foreach ($node->items as $array_item) {
            $key = $array_item->key->value;

            // Rename the 'body' key.
            if ($key == 'body') {
                $key = 'body_pieces';
            }

And that's my Rector rule done.

Although it's taken me far more time than changing each file by hand, it's been far more interesting, and I've learned a lot about how Rector works, which will be useful to me in the future. I can definitely see how it's a very useful tool even for refactoring a small codebase such as DrupalCodeBuilder, where a rule is only going to be used once. It might even prompt me to undertake some minor refactoring tasks I've been putting off because of how tedious they'll be.

What I've not figured out is how to extract namespaces from full class names to an import statement, or how to put line breaks in the new statement. I'm hoping that a pass through with PHP_CodeSniffer and Drupal Coder's rules will fix those. If not, there's always good old regexes!

Tags

  • refactoring
  • Rector
  • PhpParser
  • Drupal Code Builder

Defining bundle fields in code

By joachim, Mon, 31/01/2022 - 17:14

Fields in Drupal 9 can be defined in code, or they can be defined in configuration. Both techniques have their uses and advantages. Typically code fields apply to all bundles of the entity type, as so-called base fields, while config fields apply only to a single bundle.

But there is a way to have code fields which apply only to specific bundles: these are called bundle fields. These are particularly useful for adding computed fields to specific bundles of an entity (more on computed fields in a future blog post).

The API for these is a bit clunky, as it wasn't finalised for Drupal 8, and it may in fact be completely changed in the future. If that causes you apprehension at implementing bundle fields in your code, it's worth mentioning that the last patch on that issue was in 2017: it's a complex problem without many applications, so accordingly doesn't get much attention.

Bundle fields need to be defined in two places: the field storage, and field definition itself. This is similar to how config fields work; it's actually the same as base fields, but the BaseFieldDefinition hides this from you as it does double duty. (There is an issue to remove the need to define the storage, but again, not much happening there.)

You also need a field definition class. The entity contrib module provides a BundleFieldDefinition class, but if you don't want to install that module just for a single class, copy-pasting that class to your custom module is fine too.

(There is a class in core FieldDefinition, which was added for this purpose, however, that requires a storage class to work alongside it, and the issue to provide that has not yet been fixed.)

Once you have that class, how you define the field depends on the entity you're adding it to.

Bundle fields on your own entity type

In your own entity type, you define the entity class. You can therefore implement the bundleFieldDefinitions() method in that class:

  // In MyEntity class:
  /**
   * {@inheritdoc}
   */
  public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
    $definitions = [];

    if ($bundle == 'mybundle') {
      $definitions['myfield'] = BundleFieldDefinition::create('entity_reference')
        // This is essential: the array key is NOT used as the field machine name,
        // so it MUST be specified here!
        ->setName('myfield')
        // These two are essential: they define the entity type and the bundle
        // that the field is on.
        ->setTargetEntityTypeId($entity_type->id())
        ->setTargetBundle($bundle)
        // Further define the field as you would with a base field.
        ->setLabel(t('Label'))
        ->setDescription(t('Description.'));
    }

    return $definitions;
  }

That defines the field. Because these are bundle fields, the storage must be defined separately in hook_entity_field_storage_info(). However, we can take advantage of the BundleFieldDefinition class doing double duty as a storage and a field, and piggy-back on the definition in the entity class:

/**
 * Implements hook_entity_field_storage_info().
 */
function mymodule_entity_field_storage_info(EntityTypeInterface $entity_type) {
  // Entity bundle fields need to declare their storage separately. However, we
  // can piggyback on the field definition itself, since the
  // BundleFieldDefinition class does double duty as field and storage
  // definition since it extends from BaseFieldDefinition.
  // We cheat on the parameters we pass in, as the entity's
  // bundleFieldDefinitions() method expects a list of base field definitions,
  // but we have none here to give. For our purposes, bundleFieldDefinitions()
  // does not need them.
  if ($entity_type->id() == 'myentity') {
    return MyEntity::bundleFieldDefinitions($entity_type, 'mybundle', []);
  }
}

The documentation for hook_entity_bundle_field_info() suggests you do it the other way round, and define the storage first, then derive the field from the storage, but doing it in the way shown above means that the code for your field is in your entity class where it's alongside the base field definition, and more easily discoverable.

Bundle fields on someone else's entity type

For an entity type you don't control, you could switch the entity class to a custom subclass and implement bundleFieldDefinitions() in that, but it's simpler to use hook_entity_bundle_field_info().

/**
 * Implements hook_entity_bundle_field_info().
 */
function mymodule_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
  if ($entity_type->id() == 'myentity' && $bundle == 'mybundle') {
    $fields = [];
    $fields['myfield'] = BundleFieldDefinition::create('list_integer')
      // This is essential: the array key is NOT used as the field machine name,
      // so it MUST be specified here!
      ->setName('myfield')
      // These two are essential: they define the entity type and the bundle
      // that the field is on.
      ->setTargetEntityTypeId($entity_type->id())
      ->setTargetBundle($bundle)
      // Further define the field as you would with a base field.
      ->setLabel(t('Label'))
      ->setDescription(t('Description.'));

    return $fields;
  }
}

/**
 * Implements hook_entity_field_storage_info().
 */
function mymodule_entity_field_storage_info(EntityTypeInterface $entity_type) {
  if ($entity_type->id() == 'myentity') {
    return mymodule_entity_bundle_field_info($entity_type, 'mybundle', []);
  }
}

We use the same piggybacking trick here. In the case of altering someone else's entity type, it makes just as much sense to define the storage and derive the field, but following the same pattern as the custom entity keeps the two methods matching.

As you can see, this is an area of Drupal's entity system which is unfortunately incomplete, and liable to change in the future. Nonetheless, the usefulness of bundle fields means that it's worth using them. Just be sure to document your code so future developers (who might be you!) know that it's area of the code that may need maintainance when the API is updated. And keep an eye on the Drupal core change records!

Tags

  • core
  • Entity API
  • Field API

Pagination

  • Current page 1
  • Page 2
  • Page 3
  • Page 4
  • Page 5
  • Page 6
  • Page 7
  • Page 8
  • Page 9
  • …
  • Next page
  • Last page
Blog

Frequent tags

  • Drupal Code Builder (9)
  • git (7)
  • module builder (6)
  • 6.x (5)
  • drupal commerce (4)
  • development (3)
  • Entity API (3)
  • Field API (3)
  • patching (3)
  • Composer (3)
  • Drush (3)
  • contributing code (2)
  • maintaining projects (2)
  • code style (2)
  • contrib module (2)
  • drupal.org (2)
  • debugging (2)
  • tests (2)
  • multisite (2)
  • issue queue (2)
  • Drupal core (2)
  • core (2)
  • modules (2)
  • roadmap (2)
  • 7.x (2)
  • developer tools (2)
  • Rector (2)
  • wtf (2)
Powered by Drupal