Skip to main content
Joachim's blog

Main navigation

  • Home
  • About
  • Hire me

Breadcrumb

  1. Home

Drupal Planet Posts

Speed up your PHPUnit Browser tests with this one trick

By joachim, Wed, 01/04/2026 - 08:14

It's true, no April fools. You can make your Browser tests run much quicker. How? By deleting them!

You will of course need to add a corresponding Kernel test - and that's the trick. Kernel tests run much faster than Browser tests.

But Browser tests make requests to the test site using an internal web browser, I hear you say, whereas Kernel tests make API calls directly. Kernel tests have their uses for testing APIs, but Browser tests are needed to test actual HTML output.

Aha! Kernel tests can now make HTTP requests.

This is subject to a number of caveats and limitations: there is no session, and forms can't be submitted. And functionality such as a current user, blocks on the page, and page caching will need additional setup.

And more generally, with Kernel tests, modules are enabled but not installed: you need to handle things like entity schemas, database tables, and install config yourself in the test. The benefit though is that you only set up the parts of the module that you need for your test.

So not all Browser tests are suitable for conversion. But a lot of them are. We're already working on converting tests in core, and as this feature has been backported to Drupal core 11.x, contrib modules can make use of it too.

The benefits to conversion are tests that run faster, so less time developing and less time waiting for CI pipelines to run, and a lower energy footprint and lower costs for drupal.org. And they're easier to debug too.

And if you haven't yet written any tests for your module, now is an excellent time to start!

Do you need help with writing PHPUnit tests, or getting started with test-driven development? I'm available for hire - contact me!

Tags

  • tests

New Module Builder documentation site

By joachim, Thu, 19/03/2026 - 12:45

Module Builder now has its own documentation site.

This covers the many options it offers developers for fine-tuning their module code, from dependency injection to plugin inheritance, entity base fields, form elements, permissions, library asset files, and more.

Meanwhile, the latest release of Module Builder adds a feature I've wanted to implement for a very long time: when a new form section is added to add a new component (such as a plugin, hook class, or entity type), the form scrolls up to the new section that's just been added with AJAX. This makes it much clearer to understand what's just been changed, and helps with navigating around Module Builder's forms.

Tags

  • module builder

Release more code: the technical stuff

By joachim, Tue, 24/02/2026 - 09:52

At LocalGov Drupal Dev Days in London earlier this month, the topic came up of releasing custom project code as contrib modules.

There were many people in the room who said they had custom code in their site codebase that they planned to release as contrib modules, but needed to find the time to get it ready. I heard people mention the work that they had left to do for this, and it sounded very familiar: generalise the functionality, remove client-specific code, remove client-specific strings.

This reminded me of a session I did at Drupal Camp London way back in 2014, on this very topic: releasing more code from your codebase, to lower the amount of custom code and share more with the community. Since then, I've gone on to release many more contrib modules, and the introduction of more powerful APIs and systems with Drupal 8 has added to what's possible, so I thought I'd revisit my thoughts on different ways to approach this. My presentation was on the 'why' as well as the 'how', but I'll assume you know that part already.

The first thing to say is that as with tests or accessibility, it's much easier to write contributable code from the start rather than rework it later.

Fundamentally though, whether to plan from the start or retrofit, the baic principle is that you want your code to be split into two layers: the contrib, and the custom. Think of it as a contrib cake with custom icing on top.

The tricky part is where to put the dividing line. It's not always clear how much of your functionality is generic and applicable to other use cases and other clients.

I always err on the side of putting too much in contrib, and offsetting the possibility that the contrib code is too specific with customisability.

But how do we actually slice it up?

Plugins

Plugins are one of the most powerful ways of switching behaviour in Drupal. Defining your own plugin type allows you to design exactly which parts of the code are handed over to the plugin, and in as many places as you want, by adding more methods to your plugin's interface.

If the methods in the plugin start to look unrelated, you can always add a second plugin type. And if the amount of boilerplate needed for a plugin type is offputting, Module Builder generates it all for you.

It's worth also considering the lesser-known sibling of attribute plugins, the YAML plugin. If you only want to change strings or parameters, then you can put all that into YAML instead of a whole class. (And YAML plugins do allow custom classes for oddball cases.)

With a plugin system, you need a way to set the plugin to use. There are two ways you could do this: if it's a single plugin that you select, use a plain config setting. If it's a pattern that you might want several of, use a config entity that holds the reference to the plugin. This requires a fair bit of boilerplate code, but there are examples in contrib that you can crib from, such as Flag and Action Link.

And remember that there are other systems that allow ways to select a plugin: field formatters and widgets, Views handlers for fields and filters and so on, paragraph behaviours, and more.

Twig templates

Twig templates are a great way to customise output from your module. You can change strings, rearrange elements, and add CSS classes for styling.

You'll need to define the theme hook using hook_theme(), and define the variables the template uses. Then, provide a neutral version of the template in the contrib module's /templates folder, and override it in your site's theme.

Form alteration

For forms, use hook_form_alter() to change the labels of elements and their order.

Or you can even add extra form elements, and handle their values in a custom submit handler.

If your alterations start to get too complex, consider using a plugin that you pass the form to for customization.

Overridden config

Simplest of all is to use config to override values, whether they are strings or parameters.

Define the config schema for the settings, add a default config to the module's config/install, and then override it in your project's config.

Other APIs

It's worth looking at existing APIs that allow a custom module or theme to alter functionality. For example, in core, field widgets can be altered with hook_field_widget_single_element_form_alter() and field formatters with hook_field_formatter_third_party_settings_form(). And all sorts of unspeakable things can be done to Views with field, filter, argument, and sort handlers, and display extender plugins.

Shortcuts

If you're short on time and resources to work on splitting your cake up, there are some shortcuts you can take. It's what I call the 'code and run' method of releasing code: the contrib module is incomplete, but released in the hope that the next person who finds it useful will pick it up and move it forward.

  • Skimp on UI: If they're settings that you don't need to override in your project, you could even make the values constants somewhere. The main thing is to make it easy to find all occurrences of a value, so that a future contributor can replace them with a value from config.
  • Skimp on features: Leave space for other cases you can envisage, but don't need right now.

My opinion on this is that releasing some code, even if it's half-baked, is better than not releasing code at all, as long as you clearly explain on the project page that the module has things missing or incomplete, and leave a trail in the code in the form of comments and placeholders.

Do you need help with preparing custom code to be released as a contrib project? It's a great way to get more presence for you or your organisation. I'm available for hire - contact me!

Tags

  • contributing code
  • LocalGov Drupal

Converting hooks to OO methods made easy

By joachim, Fri, 23/01/2026 - 11:49

Rector is a really powerful tool for making refactoring changes to your codebase. It's easy to use, but it's not obvious, and a lot of the documentation and articles about it are outdated or incomplete. For instance, when you go to the project page (https://www.drupal.org/project/rector) there's no clear indication of how to install it!

More and more of the code changes needed to keep your modules up to date with Drupal core are being written as Rector rules. I wrote recently about converting plugins to PHP attributes; the other big change in Drupal at the moment is hooks changing from procedural functions to class methods.

Here's the steps I took to convert the hooks in the Computed Field module:

  1. Install Rector in your project. As mentioned earlier, finding the installation instructions is not obvious: they're in the github project:
composer require --dev palantirnet/drupal-rector
cp vendor/palantirnet/drupal-rector/rector.php .

This puts a rector.php file in your project root. What to do with this isn't immediately obvious either, but fortunately, in the PR for OO hook conversion there is sample code. The key part is this:

  $rectorConfig->rule(\DrupalRector\Rector\Convert\HookConvertRector::class);

You can then run Rector on your code. Remember to commit any existing changes to git first: this Rector rule changes a lot, and it's good to be able to revert it cleanly if necessary.

vendor/bin/rector process path/to/my_module

This does the conversion: hook implementation code is copied to methods in new Hook classes, and the existing hook implementations are reduced to legacy wrappers.

However, the code is all formatted to ugly PHP PSR standards. Import statements in .module file for use inside hook code will also remain. So we turn to PHPCS, which can re-format the code correctly and clean up the imports. I chose to target just the .module file and the Hook classes:

vendor/bin/phpcbf --standard=Drupal --extensions=php,module path/to/my_module/src/Hook
vendor/bin/phpcbf --standard=Drupal --extensions=php,module path/to/my_module/my_module.module

At this point, you should run your tests to confirm everything works, but the conversion should be complete.

You can of course now choose to do further refactoring on your hooks class, such as splitting it into multiple classes for clarity, moving helper functions into the class, or combining multiple hooks.

Tags

  • Rector
  • deprecation
  • phpcs

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

Commit your Composer-managed files to version control

By joachim, Tue, 20/04/2021 - 08:46

According to the Composer documentation, you shouldn't commit to version control any of the files in packages that are managed by Composer. For a Drupal project, that means the following folders are not in your git repository:

  • vendor
  • web/core
  • web/modules/contrib

Bluntly, I think this is the wrong thing to do. Doing the exact opposite saves you time, effort, heartache, and energy.

Let's start by comparing the two workflows.

Composer-recommended workflow: the wrong way

  1. Alice, the developer, updates some packages in her local dev.
  2. She commits only the composer.json and composer.lock files.
  3. Bob pulls changes, and runs composer install to update his local dev.
  4. When the project is deployed, the server needs to run composer install as part of the deployment process.

Git-managed workflow: the right way

  1. Alice, the developer, updates some packages in her local dev.
  2. She commits all the new and changed files.
  3. Bob pulls changes.
  4. When the project is deployed... it's deployed. All the files necessary to run the app are in git, and so updating the server's git clone gives it all the files.

The first reason this workflow is better should jump out: developers save time. Git is much faster and pulling files from a repository than Composer is at resolving, downloading, and installing packages. Switching git branches is fast, even when a lot of files change. Having to run Composer because one branch has package updates on it and the other doesn't, or has different ones, is a waste of developer time.

The second reason is related: save time in deployment. That's not just developer time, that's time your production server is down.

The third reason is more philosophical: Composer is not a file management tool. It's a dependency management tool. Let Composer do what it does well, which is figuring out which versions of which packages should be installed. Let git handle what it does well, which is synchronizing different copies of a codebase. Purists may point out that git is a version control tool, not a file management tool: that's a valid point, but doesn't diminish my argument: commit all files, and then use rsync or whatever to get the files from a git clone to the server.

Related to this: don't make your webserver do package management work; that's not its job. And don't make Bob repeat the work Alice already did.

Finally, committing files to git puts them under version control. That's merely stating the obvious, but putting files under version control has many benefits.

  • IDEs typically ignore git-ignored files for things like searching and code analysis. On my IDE at least (VSCode), disabling that means it then wants to include things like the uploaded files folder.

  • Similarly, search tools such as ag and ack take .gitignore files into account, and having to override that robs you of useful behaviour.

  • Finally, files under version control are easier to debug and tinker with. I for one often go digging in core or contrib modules, trying to figure out why something isn't working the way it's supposed to, or trying to fix a bug. To do that, I put in dump statements; sometimes tons of dump statements. (I can never get xdebug to work. Dump statements are quick and easy.) With code under version control, cleaning up all that mess is trivial: git can discard all changes. Without version control, I'm hunting through files for all the debug changes I made, or bouncing on CMD-Z like it's still the dark ages.

So, put all your files in version control. And remember the adage: if it's not under version control, it doesn't exist.

Tags

  • Composer

Pagination

  • Current page 1
  • Page 2
  • Page 3
  • Page 4
  • Page 5
  • Page 6
  • Page 7
  • Next page
  • Last page
Joachim&#039;s blog

Frequent tags

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