Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough
Nov 29 2021
Nov 29

No doubt that by now you are familiar with the Drupal display and form modes. The former existed in previous versions of Drupal while the latter came in Drupal 8. As a matter of fact, you can check out this older article of mine in which I talk about custom form modes and how we can define and make entity types use them. If, of course, you are unfamiliar with the topic.

Today, however, I am going to tell you about a new hook introduced in Drupal 9.2 which allows us to swap out the form mode dynamically when the entity form is being rendered. Which, I have to admit, is something pretty cool. Many times in the past I had to alter forms and hide fields based various criteria such as access or context.

Let me paint you a picture with my imagination brush. Just to get an idea when this hook would shine. Imagine you have users on your site with a role called Customer. Admins can create/edit these users and they should be able to control all the fields on the user account. Which can include even things like remote_id. However, the users themselves, when they edit their own account, should not see all these fields but only a subset. For example, just the email and password. Maybe the timezone. Whatever. What you need in this case is to have a different form mode for when the user edits their own account and the default one can stay for when the admin edits the account.

We can now easily achieve this with hook_entity_form_mode_alter(). Before this new hook, a workaround for this kind of use case could be achieved with the more verbose hook_entity_form_display_alter(). This allows to hide or show various components (fields) from the form display depending on various conditions. But now, we can simply configure our form mode and switch it out entirely.

And it’s all very simple. Assuming you have created your User role called Customer and a form mode for the User account also called customer, we can implement the hook simply like this:

function module_name_entity_form_mode_alter(&$form_mode, Drupal\Core\Entity\EntityInterface $entity) {
  if ($entity instanceof \Drupal\user\UserInterface && $entity->hasRole('customer') && \Drupal::currentUser()->id() === $entity->id()) {
    $form_mode = 'customer';
  }
}

The hook receives the current form mode by reference so we can change it and the entity whose form is being built. All we have to do is check if we are looking at a User form and that the current user is the same as the user being edited. And we’re done. Customer users will now only see the intended subset of fields when editing their accounts. And when you add a new field, adjusting who gets to see it will be a matter of configuration instead of actual development work.

Hope this helps.

Nov 22 2021
Nov 22

The Drupal commerce ecosystem, and especially architecture, is both rich and flexible enough to allow us to create some really powerful things. Much comes out of the box that, with a few bits of tweaking here and there, makes our web-shop ready for shipping. Boy…do I love a good pun.

But even more, the architecture that heavily relies on plugins makes it so that when that is not enough, we can quickly write our own bits of integrating code to deliver those missing pieces. So, about such an example I want to talk about today, namely, how we can create a promotion that, when buyers are eligible, will ensure the shipping price does not go over a specific amount. Imagine the following use cases:

  • you have a shipping service with a flat rate in your country but varying rates in other countries, or
  • you have a temporary offer by which you want to knock down all shipping costs to a certain amount, or
  • you have coupons that entitle some users to a given shipping rate.

Of course, the main premise here is that your shipping costs are variable and depend on the things people buy (such as weight and stuff). Because otherwise I can immediately refer you back up to all the things Drupal commerce can do out of the box. Namely, creating an offer type that reduces the shipping rate by either a percentage amount or by a fixed amount:

Today, however, what we are interested in is an offer type that ensures that the shipping cost doesn’t go higher than a certain value. Something like this:

So, if you check in your current Drupal commerce installation, the offer type from the screenshot above doesn’t exist yet. But, it’s easy to create. So let’s get to it.

First, a little bit of theory.

The promotion offer plugins implement PromotionOfferInterface with its most important method: apply(). The latter is responsible for taking an entity and applying the offer onto it. Shocker. I highly recommend you check out the code for all the existing ones and find inspiration from there. Always the best source to learn from.

Typically, promotion offers are applied to something. For example, shipment costs, the order total as a whole, individual order item prices, etc. So for this reason, we also have interfaces and base classes for the plugins that deal specifically with a type of offer “target”. For example, we have OrderPromotionOfferInterface and it’s corresponding OrderPromotionOfferBase that deal with orders. We also have similar for order items, some others, but finally, what we care about is the ShipmentPromotionOfferInterface and ShipmentPromotionOfferBase, which we will be using.

The reason for these base classes is to provide some specific logic that deals with the type of offer “target”. For instance, when dealing with shipping, we can condition the plugin to a given shipping method. So the base class takes care of the form and that logic already for us. All the plugin has to do is handle the actual offer application and potentially the configuration form for it (as needed).

OK, now that we have a bit of background on where in the code we need to look for inspiration, let’s create our own plugin called ShipmentFixedAmount, which will extend from ShipmentPromotionOfferBase. And we start with the plugin annotation:

/**
 * Sets the shipment amount to a fixed value.
 *
 * @CommercePromotionOffer(
 *   id = "shipment_fixed_amount",
 *   label = @Translation("Maximum amount on the shipment"),
 *   entity_type = "commerce_order"
 * )
 */
class ShipmentFixedAmount extends ShipmentPromotionOfferBase {

Nothing major here, except maybe the confusing entity_type key where we specify the type of entity we want passed to the apply() method of our plugin. We won’t actually deal with the latter because the parent class will do so. But we do want the commerce_order entity there. Instead, we have an applyToShipment() method called by the parent class and which gives us the shipment entity to apply the offer onto. Check out the ShipmentPromotionOfferBase::apply() method to better understand how this works.

Next up, we need a configuration form whereby the user can specify what is the amount to max out the shipping cost at. Drupal commerce comes with some traits that could help with this, but in our case don’t. But you should check out some of them for future reference, as you may be able to use them directly: FixedAmountOffTrait, PercentageOffTrait and the like. But alas, our form will have some different wording so we need to recreate it. Not a biggie, and here are the first few methods of our plugin, specifically that deal with this configuration of the amount by the site manager:

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
        'amount' => NULL,
      ] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);

    $amount = $this->configuration['amount'];
    // A bug in the plugin_select form element causes $amount to be incomplete.
    if (isset($amount) && !isset($amount['number'], $amount['currency_code'])) {
      $amount = NULL;
    }

    $form['amount'] = [
      '#type' => 'commerce_price',
      '#title' => $this->t('Maximum amount'),
      '#default_value' => $amount,
      '#required' => TRUE,
      '#weight' => -1,
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);

    if (!$form_state->getErrors()) {
      $values = $form_state->getValue($form['#parents']);
      $this->configuration['amount'] = $values['amount'];
    }
  }

  /**
   * Gets the offer amount.
   *
   * @return \Drupal\commerce_price\Price|null
   *   The amount, or NULL if unknown.
   */
  protected function getAmount() {
    if (!empty($this->configuration['amount'])) {
      $amount = $this->configuration['amount'];
      return new Price($amount['number'], $amount['currency_code']);
    }
  }

Apart from getAmount() which is a simple helper, the rest are implementations of the configurable plugin interface with which we should be a bit familiar already. And you can see that it’s very similar to FixedAmountOffTrait.

Finally, for the actual business logic, we need to implement the applyToShipment() method to apply our offer to the shipment found in the order:

  /**
   * {@inheritdoc}
   */
  public function applyToShipment(ShipmentInterface $shipment, PromotionInterface $promotion) {
    $amount = $this->getAmount();
    if ($amount->getCurrencyCode() != $shipment->getAmount()->getCurrencyCode()) {
      return;
    }

    $current_amount = $shipment->getAdjustedAmount();
    if ($current_amount->lessThanOrEqual($amount)) {
      // If it's already the same amount, do nothing.
      return;
    }

    // Calculate the amount that needs to be subtracted.
    $to_subtract = $current_amount->subtract($amount);

    $shipment->addAdjustment(new Adjustment([
      'type' => 'shipping_promotion',
      'label' => $promotion->getDisplayName() ?: $this->t('Discount'),
      'amount' => $to_subtract->multiply('-1'),
      'source_id' => $promotion->id(),
      'included' => $this->isDisplayInclusive(),
    ]));
  }

So what is going on here?

As you can see we get an instance of the $shipment object onto which we need to set an Adjustment. This is the Commerce way of applying changes to amounts so that they can then be calculated together later (and broken back out if needed).

First we check that the currency is the same as the one on the shipment. We don’t want to deal with currency conversions here. We return if not. Then we quickly check if the actual shipment amount is not already smaller or the same as the one we configured. If it is, again, we return without doing anything. Nothing left to do right?

And finally, we perform the big operation: we determine the difference between the shipping amount and the configured one, so that we know how much we need to subtract. And we do so by adding an adjustment to the shipping (in the negative, hence the $to_subtract->multiply('-1')). And that’s it.

After a good ol’ fashion cache clear, you can create a Promotion using this new offer type, just like in the screenshot above. And when users are eligible to this promotion, they will see a discount on their order equal to the difference between the real shipping amount of their order and the maximum we want to limit this amount to via the promotion. So if the shipping normally costs 10 EUR, but you configured the offer to max out the cost at 7 EUR, users will see a discount of 3 EUR on their order checkout. Which effectively gives them a total cost of 7 EUR on their order.

Phew, long story, but I bet you’ll write this code up in 5 minutes now that you know how and understand where to look.

Hope this helps.

Nov 04 2021
Nov 04

In this article I am going to show you a neat little trick by which we can override the static metatags of a given View with those that come from a potential dynamic filter.

Imagine the following: you have a View that lists Article nodes. And these nodes can be tagged with terms from the Tags vocabulary. Moreover, this View has an exposed filter on these tags so the user can filter by them. The end URLs look something like this:

Using the Metatag module, you configured the View to have a title, description and whatever else you needed. However, when filtering the View, the resulting page would benefit from more specific metatags, potentially coming from the tags right? For example, the title to no longer be Articles (which makes sense when you list all of them), but instead, be Articles tagged with #Tag. Makes sense right? Or News from Africa. You get the point and I had a similar need.

My solution for this problem was to alter the metatags of the View and defer to the metatags of the Tags taxonomy terms if there was a filter applied. So how did I do this?

All I had to do is implement hook_metatag_route_entity() and tell the Metatag module to look at another entity when trying to determine the metatags of a given route. So something like this:

/**
 * Implements hook_metatag_route_entity().
 */
function module_name_metatag_route_entity(\Drupal\Core\Routing\RouteMatchInterface $route_match) {
  $route = $route_match->getRouteName();
  if ($route !== 'view.articles.display_machine_name') {
    return;
  }

  $tag_id = \Drupal::request()->query->get('tag');
  if (!$tag_id) {
    return;
  }
  $term = Term::load($tag_id);
  if ($term instanceof TermInterface && $term->bundle() === 'tags') {
    return $term;
  }
}

And that’s it. This hook is invoked when Metatag tries to determine the entity to use on a given route when generating the metatags. So all I had to do is check that the route is in fact that of my View and that I have a tag ID in the URL query (the View was being filtered). If so, I loaded the taxonomy term and returned it. Metatag did the rest.

Do note that depending on your module name, you’ll need to alter the order of the implementations of this hook to run before that of Metatag Views in order to take effect.

With this in place, the metatags on the View are now being inherited from the Tags taxonomy terms if there is an exposed filter applied on the View with the machine name tag. Otherwise, it isn’t doing anything and the metatags of the View are being used as expected.

Finally, in order for this solution to be useful, there is a critical assumption: the Tags taxonomy terms don’t really need/have a detail page and are only used to tag Articles in this context. Of course, this is quite unlikely but this example can be applied also with much more specific taxonomy vocabularies which are created with a single purpose in mind: to categorise one type of content and maybe act as a filter on a View.

Hope this helps.

Jan 07 2021
Jan 07

Below is an extract from my book Drupal 9 Module Development from the early Chapter 5 (out of 18) on Menu Links. I introduce the menu system from a rather theoretical point of view, and catalogue the different types of menu links we have in Drupal.

Before we get our hands dirty with menus and menu links, let's talk a bit about the general architecture behind the menu system. To this end, I want to see its main components, what some of its key players are and what classes you should be looking at. As always, no great developer has ever relied solely on a book or documentation to figure out complex systems.

Menus

Menus are configuration entities represented by the following class: Drupal\system\ Entity\Menu. I previously mentioned that we have something called configuration entities in Drupal, which we explore in detail later in this book. However, for now, it's enough to understand that menus can be created through the UI and become an exportable configuration. Additionally, this exported configuration can also be included inside a module so that it gets imported when the module is first installed. This way, a module can ship with its own menus. We will see how this latter aspect works when we talk about the different kinds of storage in Drupal. For now, we will work with the menus that come with Drupal core.

Each menu can have multiple menu links, structured hierarchically in a tree with a maximum depth of 9 links. The ordering of the menu links can be done easily through the UI or via the weighting of the menu links, if defined in code.

Menu links

At their most basic level, menu links are YAML-based plugins. To this end, regular menu links are defined inside a module_ name.links.menu.yml file and can be altered by other modules by implementing hook_menu_links_discovered_alter(). When I say regular, I mean those links that go into menus. We will see shortly that there are also a few other types.

There are a number of important classes you should check out in this architecture though: MenuLinkManager (the plugin manager) and MenuLinkBase (the menu link plugins base class that implements MenuLinkInterface).

Menu links can, however, also be content entities. The links created via the UI are stored as entities because they are considered content. The way this works is that for each created MenuLinkContent entity, a plugin derivative is created. We are getting dangerously close to advanced topics that are too early to cover. But in a nutshell, via these derivatives, it's as if a new menu link plugin is created for each MenuLinkContent entity, making the latter behave as any other menu link plugin. This is a very powerful system in Drupal.

Menu links have a number of properties, among which is a path or route. When created via the UI, the path can be external or internal or can reference an existing resource (such as a user or piece of content). When created programmatically, you'll typically use a route.

Multiple types of menu links

The menu links we've been talking about so far are the links that show up in menus. There are also a few different kinds of links that show up elsewhere but are still considered menu links and work similarly.

Local tasks

Local tasks, otherwise known as tabs, are grouped links that usually show up above the main content of a page (depending on the region where the tabs block is placed). They are usually used to group together related links that have to deal with the current page. For example, on an entity page, such as the node detail page, you can have two tabs—one for viewing the node and one for editing it (and maybe one for deleting it); in other words, local tasks:

Local tasks take access rules into account, so if the current user does not have access to the route of a given tab, the link is not rendered. Moreover, if that means only one link in the set remains accessible, that link doesn't get rendered as there is no point. So, for tabs, a minimum of two links are needed for them to show up.

Modules can define local task links inside a module_name.links.task.yml file, whereas other modules can alter them by implementing hook_menu_local_tasks_ alter().

Local actions

Local actions are links that relate to a given route and are typically used for operations. For example, on a list page, you might have a local action link to create a new list item, which will take you to the relevant form page.

Modules can define local action links inside a module_name.links.action.yml file, whereas other modules can alter them by implementing hook_menu_local_ actions_alter().

Contextual links

Contextual links are used by the Contextual module to provide handy links next to
a given component (a render array). You probably encountered this when hovering over a block, for example, and getting that little icon with a dropdown that has the Configure block link.

Contextual links are tied to render arrays. In fact, any render array can show a group of contextual links that have previously been defined.
Modules can define contextual links inside a module_name.links.contextual.ymlfile, whereas other modules can alter them by implementing hook_contextual_links_ alter().

For more on the menu system and to see how the twist unfolds, do check out my book, Drupal 9 Module Development.

Thanks for the support.

Nov 30 2020
Nov 30

In this short article I want to introduce you to a new module we recently released on Drupal.org, namely Multi-value form element.

This small module provides a form element that allows you to easily define multi-value elements in your custom forms. Much like what field widgets provide with the Add another item Ajax button.

So how does it work? Easy, really. All you have to do is define a form element of the '#type' => 'multivalue' with one or more children, defined like you normally would. So for example:

$form['names'] = [
  '#type' => 'multivalue',
  '#title' => $this->t('Names'),
  'name' => [
    '#type' => 'textfield',
    '#title' => $this->t('Name'),
  ],
];

Would give you this:

drupal multi value form elements example

And you can also use multiple form element children if you want:

$form['contacts'] = [
  '#type' => 'multivalue',
  '#title' => $this->t('Contacts'),
  'name' => [
    '#type' => 'textfield',
    '#title' => $this->t('Name'),
  ],
  'mail' => [
    '#type' => 'email',
    '#title' => $this->t('E-mail'),
  ],
];

So as you can see, no big deal to use. But all the complex Ajax logic of adding extra values is out of your hands now and can easily build nice forms.

Check out some more examples of how to use this element and what options it has above the Drupal\multivalue_form_element\Element\MultiValue class.

This module is sponsored by the European Commission as part of the OpenEuropa initiative and all the work my colleagues and myself are doing there.

Nov 24 2020
Nov 24

Maybe you have banged your head against the wall trying to figure out why if you add an Ajax button (or any other element) inside a table, it just doesn’t work. I have.

I was building a complex form that needed to render some table rows, nicely formatted and have some operations buttons to the right to edit/delete the rows. All this via Ajax. You know when you estimate things and you go like: yeah, simple form, we render table, add buttons, Ajax, replace with text fields, Save, done. Right? Wrong. You render the table, put the Ajax buttons in the last column and BAM! Hours later, you wanna punch someone. When Drupal renders tables, it doesn’t process the #ajax definition if you pass an element in the column data key.

Well, here’s a neat little trick to help you out in this case: #pre_render.

What we can do is add our buttons outside the table and use a #pre_render callback to move the buttons back into the table where we want them. Because by that time, the form is processed and Drupal doesn’t really care where the buttons are. As long as everything else is correct as well.

So here’s what a very basic buildForm() method can look like. Remember, it doesn’t do anything just ensures we can get our Ajax callback triggered.

/**
 * {@inheritdoc}
 */
public function buildForm(array $form, FormStateInterface $form_state) {
  $form['#id'] = $form['#id'] ?? Html::getId('test');

  $rows = [];

  $row = [
    $this->t('Row label'),
    []
  ];

  $rows[] = $row;

  $form['buttons'] = [
    [
      '#type' => 'button',
      '#value' => $this->t('Edit'),
      '#submit' => [
        [$this, 'editButtonSubmit'],
      ],
      '#executes_submit_callback' => TRUE,
      // Hardcoding for now as we have only one row.
      '#edit' => 0,
      '#ajax' => [
        'callback' => [$this, 'ajaxCallback'],
        'wrapper' => $form['#id'],
      ]
    ],
  ];

  $form['table'] = [
    '#type' => 'table',
    '#rows' => $rows,
    '#header' => [$this->t('Title'), $this->t('Operations')],
  ];

  $form['#pre_render'] = [
    [$this, 'preRenderForm'],
  ];

  return $form;
}

First, we ensure we have an ID on our form so we have something to replace via Ajax. Then we create a row with two columns: a simple text and an empty column (where the button should go, in fact).

Outside the form, we create a series of buttons (1 in this case), matching literally the rows in the table. So here I hardcode the crap out of things but you’d probably loop the same loop as for generating the rows. On top of the regular Ajax shizzle, we also add a submit callback just so we can properly capture which button gets pressed. This is so that on form rebuild, we can do something with it (up to you to do that).

Finally, we have the table element and a general form pre_render callback defined.

And here are the two referenced callback methods:

/**
 * {@inheritdoc}
 */
public function editButtonSubmit(array &$form, FormStateInterface $form_state) {
  $element = $form_state->getTriggeringElement();
  $form_state->set('edit', $element['#edit']);
  $form_state->setRebuild();
}

/**
 * Prerender callback for the form.
 *
 * Moves the buttons into the table.
 *
 * @param array $form
 *   The form.
 *
 * @return array
 *   The form.
 */
public function preRenderForm(array $form) {
  foreach (Element::children($form['buttons']) as $child) {
    // The 1 is the cell number where we insert the button.
    $form['table']['#rows'][$child][1] = [
      'data' => $form['buttons'][$child]
    ];
    unset($form['buttons'][$child]);
  }

  return $form;
}

First we have the submit callback which stores information about the button that was pressed, as well as rebuilds the form. This allows us to manipulate the form however we want in the rebuild. And second, we have a very simple loop of the declared buttons which we move into the table. And that’s it.

Of course, our form should implement Drupal\Core\Security\TrustedCallbackInterface and its method trustedCallbacks() so Drupal knows our pre_render callback is secure:

/**
 * {@inheritdoc}
 */
public static function trustedCallbacks() {
  return ['preRenderForm'];
}

And that’s pretty much it. Now the Edit button will trigger the Ajax, rebuild the form and you are able to repurpose the row to show something else: perhaps a textfield to change the hardcoded label we did? Up to you.

Hope this helps.

Jun 15 2020
Jun 15

Today I want to introduce a new contrib module called Composite Reference. Why is it called like this? Because it’s meant to be used for strengthening the “bond” between entities that are meant to live and die together.

In many cases we use entity references to entities that are not reusable (or are not meant to be). They are just more complex storage vehicles for data that belongs to another entity (for the sake of explanation, we can call this the parent). So when the parent is deleted, it stands to reason the referenced entity (the child) is also deleted because it is not supposed to exist outside of the context of its parent. And this is a type of composite relation: the two belong together as a unit. Granted, not all parent-child relations are or have to be composite. But some can and I simply used them as an example.

So what does the module do? Apart from the fancy name, it does nothing more than make an entity reference (or entity reference revisions) field configurable to become composite. And when the relation is composite, the referenced entity gets deleted when the referencing one is deleted. And to prevent all sorts of chaos and misuse, the deletion is prevented if the referenced entity is referenced by yet another entity (making it by definition NOT composite). This should not happen though as you would mark relations as composite only in the cases in which the referenced entities are not reusable.

And that is pretty much it. You can read the project README for more info on how to use the module.

This project was written and is maintained as part of the OpenEuropa Initiative of the European Commission.

Apr 07 2020
Apr 07

In order to really understand how entity data is modelled, we need to understand the TypedData API. Unfortunately, this API still remains quite a mystery for many. But you're in luck because, in this section, we're going to get to the bottom of it.

Why TypedData?

It helps to understand things better if we first talk about why there was the need for this API. It all has to do with the way PHP as a language is, compared to others, and that is, loosely typed. This means that in PHP it is very difficult to use native language constructs to rely on the type of certain data or understand more about that data.

The difference between the string "1" and integer 1 is a very common example. We are often afraid of using the === sign to compare them because we never know what they actually come back as from the database or wherever. So, we either use == (which is not really good) or forcefully cast them to the same type and hope PHP will be able to get it right.

In PHP 7, we have type hinting for scalar values in function parameters which is good, but still not enough. Scalar values alone are not going to cut it if you think of the difference between 1495875076 and 2495877076. The first is a timestamp while the second is an integer. Even more importantly, the first has meaning while the second one does not. At least seemingly. Maybe I want it to have some meaning because it is the specific formatting for the IDs in my package tracking app.

Drupal was not exempt from the problems this loosely typed nature of PHP can create. Drupal 7 developers know very well what it meant to deal with field values in this way. But not anymore because we now have the TypedData API in Drupal.

What is TypedData?

The TypedData API is a low-level and generic API that essentially does two things from which a lot of power and flexibility is derived.

First, it wraps "values" of any kind of complexity. More importantly, it forms "values". This can be a simple scalar value to a multidimensional map of related values of different types that together are considered one value. Let's take, for example, a New York license plate: 405-307. This is a simple string but we "wrap" it with TypedData to give it meaning. In other words, we know programmatically that it is a license plate and not just a random PHP string. But wait, that plate number can be found in other states as well (possibly, I have no idea). So, in order to better define a plate, we need also a state code: NY. This is another simple string wrapped with TypedData to give it meaning—a state code. Together, they can become a slightly more complex piece of TypedData: US license plate, which has its own meaning.

Second, as you can probably infer, it gives meaning to the data that it wraps. If we continue our previous example, the US license plate TypedData now has plenty of meaning. So, we can programmatically ask it what it is and all sorts of other things about it, such as what is the state code for that plate. And the API facilitates this interaction with the data.

As I mentioned, from this flexibility, a lot of power can be built on top. Things like data validation are very important in Drupal and rely on TypedData. As we will see later in this chapter, validation happens at the TypedData level using constraints on the underlying data.

Check out the book for a getting a deeper understanding on how this API is used to model the entity system in Drupal.

Mar 30 2020
Mar 30

Automated testing is a process by which we rely on special software to continuously run pre-defined tests that verify the integrity of our application. To this end, automated tests are a collection of steps that cover the functionality of an application and compare triggered outcomes to expected ones.

Manual testing is a great way to ensure that a piece of written functionality works as expected. The main problem encountered by most adopters of this strategy, especially those who use it exclusively, is regression. Once a piece of functionality is tested, the only way they can guarantee regressions (or bugs) were not introduced by another piece of functionality is by retesting it. And as the application grows, this becomes impossible to handle. This is where automated tests come in.

Automated testing uses special software that has an API that allows us to automate the steps involved in testing the functionality. This means that we can rely on machines to run these tests as many times as we want, and the only thing stopping us from having a fully-working application is the lack of proper test coverage with well-defined tests.

There's a lot of different software available for performing such tests and it's usually geared toward specific types of testing. For example, Behat is a powerful PHP-based open source behavior testing framework that allows the scripting of tests that mirror quite closely what a manual tester would do—interact with the application through the browser and test its behavior. There are other testing frameworks that go much lower in the level of their testing target. For example, the PHP industry standard tool, PHPUnit, is widely used for performing unit tests. This type of testing focuses on the actual code at the lowest possible level; it tests that class methods work properly by verifying their output after providing them with different input. A strong argument in favor of this kind of testing is that it encourages better code architecture, which can be (partly) measured by the ease with which unit testing can be written for it.

We also have functional or integration tests which fall somewhere in between the two examples. These go higher than code level and enlist application subsystems in order to test more comprehensive sets of functionality, without necessarily considering browser behavior and user interaction.

It is not difficult to agree that a well-tested application features a combination of the different testing methodologies. For example, testing the individual architectural units of an application does not guarantee that the entire subsystem works, just as testing only the subsystem does not guarantee that its individual components will work properly under all circumstances. Also, the same is true for certain subsystems that depend on user interaction—these require test coverage as well.

Testing methodologies in Drupal 8

Like many other development aspects, automated testing has been greatly improved in Drupal 8. In the previous version, the testing framework was a custom one built specifically for testing Drupal applications—Simpletest. Its main testing capability focused on functional testing with a strong emphasis on user interaction with a pseudo-browser. However, it was quite strong and allowed a wide range of functionality to be tested.

Drupal 8 development started with Simpletest as well. However, with the adoption of PHPUnit, Drupal is moving away from it and is in the process of deprecating it. To replace it, there is a host of different types of tests—all run by PHPUnit—that can cover more testing methodologies. So let's see what these are.

Drupal 8 comes with the following types of testing:

  • Simpletest: exists for legacy reasons but no longer used to create new tests. This will be removed in Drupal 9.
  • Unit: low-level class testing with minimal dependencies (usually mocked).
  • Kernel: functional testing with the kernel bootstrapped, access to the database and only a few loaded modules.
  • Functional: functional testing with a bootstrapped Drupal instance, a few installed modules and using a Mink-based browser emulator (Goutte driver).
  • FunctionalJavaScript: functional testing like the previous, using the Selenium driver for Mink that allows for testing JavaScript powered functionality.

Apart from Simpletest, all of these test suites are built on top of PHPUnit and are, consequently, run by it. Based on the namespace the test classes reside in, as well as the directory placement, Drupal can discover these tests and know what type they are.

PHPUnit

Drupal 8 uses PHPUnit as the testing framework for all types of tests. In this section, we will see how we can work with it to run tests.

On your development environment (or wherever you want to run the tests), make sure you have the composer dependencies installed with the --dev flag. This will include PHPUnit. Keep in mind not to ever do this on your production environment as you can compromise the security of your application.

Although Drupal has a UI for running tests, PHPUnit is not well integrated with this. So, it's recommended that we run them using the command line instead. Actually, it's very easy to do so. To run the entire test suite (of a certain type), we have to navigate to the Drupal core folder:

cd core

And run the following command:

../vendor/bin/phpunit --testsuite=unit

This command goes back a folder through the vendor directory and uses the installed phpunit executable (make sure the command finds its way to the vendor folder in your installation). As an option, in the previous example, we specified that we only want to run unit tests. Omitting that would run all types of tests. However, for most of the others, there will be some configuration needed, as we will see in the respective sections.

If we want to run a specific test, we can pass it as an argument to the phpunit command (the path to the file):

../vendor/bin/phpunit tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php

In this example, we run a Drupal core test that tests the UrlGenerator class.

Alternatively, we can run multiple tests that belong to the same group (we will see how tests are added to a group soon):

../vendor/bin/phpunit --group=Routing

This runs all the tests from the Routing group which actually contains the UrlGeneratorTest we saw earlier. We can run tests from multiple groups if we separate them by a comma.

Also, to check what the available groups are, we can run the following command:

../vendor/bin/phpunit --list-groups

This will list all the groups that have been registered with PHPUnit.

Finally, we can also run a specific method found inside a test by using the --filter argument:

../vendor/bin/phpunit --filter=testAliasGenerationUsingInterfaceConstants

This is one of the test methods from the same UrlGeneratorTest we saw before and is the only one that would run.

Registering tests

There are certain commonalities between the various test suite types regarding what we need to do in order for Drupal (and PHPUnit) to be able to discover and run them.

First, we have the directory placement where the test classes should go in. The pattern is this: tests/src/suite_type, where suite_type is a name of the test suite type this test should be. And it can be one of the following:

  • Unit
  • Kernel
  • Functional
  • FunctionalJavascript

So, for example, unit tests would go inside the tests/src/Unit folder of our module. Second, the test classes need to respect a namespace structure as well:

namespace Drupal\Tests\[module_name]\[suite_type]

This is also pretty straightforward to understand.

Third, there is a certain metadata that we need to have in the test class PHPDoc. Every class must have a summary line describing what the test class is for. Only classes that use the @coversDefaultClass attribute can omit the summary line. Moreover, all test classes must have the @group PHPDoc annotation indicating the group they are part of. This is how PHPUnit can run tests that belong to certain groups only.

So now that we know how to register and run tests, let's order the book and start by looking at unit tests first.

Mar 11 2020
Mar 11

In this short article I wanted to draw your attention to a neat little feature introduced in Drupal 8.8 related to migrations. And I mean the ability to force entity validation whenever Migrate saves destination entities.

As we already know, entities can be validated using their typed data wrappers like so:

$violations = $entity->validate();

This calls the general validation over the entity and all its fields. Very handy.

Something that you may or may not know is that when we create and save an entity programatically, this validation is not run by default. So for example:

$entity = Node::create([
  'type' => 'page',
  'title' => 'My title',
]);

$entity->save();

If we had some validation on the title field, it would not run and potentially bad data would be saved in the field. Sometimes this is fine, other times it’s bad and in many times it’s critical as things can break spectacularly. So always good to run validation.

When it comes to the Migrate entity destination, there was no validation being run. But with Drupal 8.8, we have a destination plugin option that indicates we want to run the validation when saving the entity. Like so:

destination:
  plugin: 'entity:node'
  validate: true

This will ensure the nodes are validated before being saved by the migration.

Moreover, apart from the configuration on the migration plugin, you can also control this from the entity level if the entity type is defined by you. You can do this by implementing the Drupal\Core\Entity\FieldableEntityInterface::isValidationRequired() method in the entity class. Do note, however, that most entity types do not implement it, nor is this method checked before doing regular entity saves. I expect its use will be extended but so far it is only used within the migration context.

Hope this helps.

Jan 27 2020
Jan 27

Using migrate in Drupal is a very powerful way to bring data into a Drupal application. I talked and wrote extensively on this matter here and elsewhere. Most of my examples use the CSV source plugin to illustrate migrations from CSV-formatted data. And if you are familiar with this source plugin, you know you have “configure” it by specifying all the file’s column names. Kind of like this (a very simple YAML array):

  column_names:
    0:
      id: 'Unique Id'
    1:
      column one: 'What this column is about'
    2:
      column two: 'Another column'

I wrote many many migrations from CSV files, of various sizes, but it took me years to finally utter the following out loud:

Can’t I just generate these stupid column names automatically instead of manually writing them every time?

As you can imagine, there can be files with 30 columns. And a given migration effort can even contain 20 migration files. So, a pain. I’m not the sharpest tool in the shed but finally my laziness got the best of me and decided to write a Drush command I want to share with you today. Also, I have not written anything in such a long time and I feel proper shame.

So what I wanted was simple: a command that I can run, point it to a file and it would print me the column names I just paste into the migration file. No fuss, no muss. Or is it the other way around?

So this is what I came up with.

First, in the module’s composer file, we have to add an extra bit to inform Drush about the services file used for Drush commands. Apparently this will be mandatory in Drush 10.

    "extra": {
        "drush": {
            "services": {
                "drush.services.yml": "^9"
            }
        }
    }

Then, we have the actual drush.services.yml file where we declare the command service:

services:
  my_module.commands:
    class: Drupal\my_module\Commands\MigrationCommands
    tags:
      - { name: drush.command }

It’s a simple tagged service that says that it should be treated by Drush as a command class that can contain multiple commands.

And finally, the interesting bit, the command class:

getConfig()->cwd() . DIRECTORY_SEPARATOR . $file, 'r');
    $spl->next();
    $headers = $spl->fgetcsv();

    $source_headers = [];
    foreach ($headers as $header) {
      $source_headers[] = [$header => $header];
    }

    $yml = Yaml::encode($source_headers);
    $this->output()->write($yml);
  }

}

What happens here is very simple. We first read the file whose path is the first and only mandatory argument of the command. This path needs to be relative from where the Drush command is called from because we concatenate it with that location using $this->getConfig()->cwd(). Then we take the values from the first row of the CSV (the header) and we build an array that is in the format expected by the CSV source plugin. Finally, we output a YAML-encoded version of that array.

Do note, however, that the column description is just the column name again since we don’t have data for that. So if you wanna add descriptions, you’ll have to add them manually in the migration file. Run the command, copy and paste and bill your client less.

Hope this helps. Can’t believe I’ve been writing CSV based migrations since like the beginning and I just came up with this thing now.

Jun 13 2019
Jun 13

In this article we are going to explore some of the powers of the Drupal 8 migration system, namely the migration “templates” that allow us to build dynamic migrations. And by templates I don’t mean Twig templates but plugin definitions that get enhanced by a deriver to make individual migrations for each of the things that we need in the application. For example, as we will explore, each language.

The term “template” I inherit from the early days of Drupal 8 when migrations were config entities and core had migration (config) templates in place for Drupal to Drupal migrations. But I like to use this term to represent also the deriver-based migrations because it kinda makes sense. It’s a personal choice so feel free to ignore it if you don’t agree.

Before going into the details of how the dynamic migrations works, let’s cover a few of the more basic things about migrations in Drupal 8.

What is a migration?

The very first thing we should talk about is what actually is a migration. The simple answer to this question is: a plugin. Each migration is a YAML-based plugin that actually brings together all the other plugins the migration system needs to run an actual logical migration. And if you don’t know what a plugin is, they are swappable bits of functionality that are meant to perform a similar task, depending on their type. They are all over core and by now there are plenty of resources to read more about the plugin system, so I won’t go into it here.

Migration plugins, unlike most others such as blocks and field types, are defined in YAML files inside the module’s migrations folder. But just like all other plugin types, they map to a plugin class, in this case Drupal\migrate\Plugin\Migration.

The more important thing to know about migrations, however, is the logical structure they follow. And by this I mean that each migration is made up of a source, multiple processors and a destination. Make sense right? You need to get some data (the source reads and interprets its format), prepare it for its new destination (the processors alter or transform the data) and finally save it in the destination (which has a specific format and behaviour). And to make all this happen, we have plugins again:

  • Source plugins
  • Process plugins
  • Destination plugins

Source plugins are responsible for reading and iterating over the raw data being imported. And this can be in many formats: SQL tables, CSV files, JSON files, URL endpoint, etc. And for each of these we have a Drupal\migrate\Plugin\MigrateSourceInterface plugin. For average migrations, you’ll probably pick an existing source plugin, point it to your data and you are good to go. You can of course create your own if needed.

Destination plugins (Drupal\migrate\Plugin\MigrateDestinationInterface) are closely tied to the site being migrated into. And since we are in Drupal 8, these relate to what we can migrate to: entities, config, things like this. You will very rarely have to implement your own, and typically you will use an entity based destination.

In between these two, we have the process plugins (Drupal\migrate\Plugin\MigrateProcessInterface), which are admittedly the most fun. There are many of them already available in core and contrib, and their role is to take data values and prepare them for the destination. And the cool thing is that they are chainable so you can really get creative with your data. We will see in a bit how these are used in practice.

The migration plugin is therefore a basic definition of how these other 3 kinds of plugins should be used. You get some meta, source, process, destination and dependency information and you are good to go. But how?

That’s where the last main bit comes into play: the Drupal\migrate\MigrateExecutable. This guy is responsible for taking a migration plugin and “running” it. Meaning that it can make it import the data or roll it back. And some other adjacent things that have to do with this process.

Migrate ecosystem

Apart from the Drupal core setup, there are few notable contrib modules that any site doing migrations will/should use.

One of these is Migrate Plus. This module provides some additional helpful process plugins, the migration group configuration entity type for grouping migrations and a URL-based source plugin which comes with a couple of its own plugin types: Drupal\migrate_plus\DataFetcherPluginInterface (retrieve the data from a given protocol like a URL or file) and Drupal\migrate_plus\DataParserPluginInterface (interpret the retrieved data in various formats like JSON, XML, SOAP, etc). Really powerful stuff over here.

Another one is Migrate Tools. This one essentially provides the Drush commands for running the migrations. To do so, it provides its own migration executable that extends the core one to add all the necessary goodies. So in this respect, it’s a critical module if you wanna actually run migrations. It also makes an attempt at providing a UI but I guess more of that will come in the future.

The last one I will mention is Migrate Source CSV. This one provides a source plugin for CSV files. CSV is quite a popular data source format for migrations so you might end up using this quite a lot.

Going forward we will use all 3 of these modules.

Basic migration

After this admittedly long intro, let’s see how one of these migrations looks like. I will create one in my advanced_migrations module which you can also check out from Github. But first, let’s see the source data we are working with. To keep things simple, I have this CSV file containing product categories:

id,label_en,label_ro
B,Beverages,Bauturi
BA,Alcohols,Alcoolice
BAB,Beers,Beri
BAW,Wines,Vinuri
BJ,Juices,Sucuri
BJF,Fruit juices,Sucuri de fructe
F,Fresh food,Alimente proaspete

And we want to import these as taxonomy terms in the categories vocabulary. For now we will stick with the English label only. We will see after how to get them translated as well with the corresponding Romanian labels.

As mentioned before, the YAML file goes in the migrations folder and can be named advanced_migrations.migration.categories.yml. The naming is pretty straightforward to understand so let’s see the file contents:

id: categories
label: Categories
migration_group: advanced_migrations
source:
  plugin: csv
  path: 'modules/custom/advanced_migrations/data/categories.csv'
  header_row_count: 1
  ids:
    - id
  column_names:
    0:
      id: 'Unique Id'
    1:
      label_en: 'Label EN'
    2:
      label_ro: 'Label RO'
destination:
  plugin: entity:taxonomy_term
process:
  vid:
    plugin: default_value
    default_value: categories
  name: label_en

It’s this simple. We start with some meta information such as the ID and label, as well as the migration group it should belong to. Then we have the definitions for the 3 plugin types we spoke about earlier:

Source

Under the source key we specify the ID of the source plugin to use and any source specific definition. In this case we point it to our CSV file, and kind of “explain” it how to understand the CSV file. Do check out the Drupal\migrate_source_csv\Plugin\migrate\source\CSV plugin if you don’t understand the definition.

Destination

Under the destination key we simply tell the migration what to save the data as. Easy peasy.

Process

Under the process key we do the mapping between our data source and the destination specific “fields” (in this case actual Drupal entity fields). And in this mapping we employ process plugins to get the data across and maybe alter it.

In our example we migrate one field (the category name) and for this we use the Drupal\migrate\Plugin\migrate\process\Get process plugin which is assumed unless one is actually specified. All it does is copies the raw data as it is without making any change. It’s the very most basic and simple process plugin. And since we are creating taxonomy terms, we need to specify a vocabulary which we don’t necessarily have to take from the source. In this case we don’t actually because we want to import all the term into the categories vocabulary. So we can use the Drupal\migrate\Plugin\migrate\process\DefaultValue plugin to specify what value should be saved in that field for each term we create.

And that’s it. Clearing the cache, we can now see our migration using Drush:

drush migrate:status

This will list our one migration and we can run it as well:

drush migrate:import categories

Bingo bango we have categories. Roll them back if you want with:

drush migrate:rollback categories

Dynamic migration

Now that we have the categories imported in English, let’s see how we can import their translations as well. And for this we will use a dynamic migration using a “template” and a plugin deriver. But first, what are plugin derivatives?

Plugin derivatives

The Drupal plugin system is an incredibly powerful way of structuring and leveraging functionality. You have a task in the application that needs to be done and can be done in multiple ways? Bam! Have a plugin type and define one or more plugins to handle that task in the way they see fit within the boundaries of that subsystem.

And although this is powerful, plugin derivatives are what really makes this an awesome thing. Derivatives are essentially instances of the same plugin but with some differences. And the best thing about them is that they are not defined entirely statically but they are “born” dynamically. Meaning that a plugin can be defined to do something and a deriver will make as many derivatives of that plugin as needed. Let’s see some examples from core to better understand the concept.

Menu links:

Menu links are plugins that are defined in YAML files and which map to the Drupal\Core\Menu\MenuLinkDefault class for their behaviour. However, we also have the Menu Link Content module which allows us to define menu links in the UI. So how does that work? Using derivatives.

The menu links created in the UI are actual content entities. And the Drupal\menu_link_content\Plugin\Deriver\MenuLinkContentDeriver creates as many derivatives of the menu link plugin as there are menu link content entities in the system. Each of these derivatives behave almost the same as the ones defined in code but contain some differences specific to what has been defined in the UI by the user. For example the URL (route) of the menu link is not taken from a YAML file definition but from the user-entered value.

Menu blocks:

Keeping with the menu system, another common example of derivatives is the menu blocks. Drupal defines a Drupal\system\Plugin\Block\SystemMenuBlock block plugin that renders a menu. But on its own, it doesn’t do much. That’s where the Drupal\system\Plugin\Derivative\SystemMenuBlock deriver comes into play and creates a plugin derivate for all the menus on the site. In doing so, augments the plugin definitions with the info about the menu to render. And like this we have a block we can place for each menu on the site.

Migration deriver

Now that we know what plugin derivatives are and how they work, let’s see how we can apply this to our migration to import the category translations. But why we would actually use a deriver for this? We could simply copy the migration into another one and just use the Romanian label as the term name no? Well yes…but no.

Our data is now in 2 languages. It could be 23 languages. Or it could be 16. Using a deriver we can make a migration derivative for each available language dynamically and simply change the data field to use for each. Let’s see how we can make this happen.

The first thing we need to do is create another migration that will act as the “template”. In other words, the static parts of the migration which will be the same for each derivative. And as such, it will be like the SystemMenuBlock one in that it won’t be useful on its own.

Let’s call it advanced_migrations.migration.category_translations.yml:

id: category_translations
label: Category translations
migration_group: advanced_migrations
deriver: Drupal\advanced_migrations\CategoriesLanguageDeriver
source:
  plugin: csv
  path: 'modules/custom/advanced_migrations/data/categories.csv'
  header_row_count: 1
  ids:
    - id
  column_names:
    0:
      id: 'Unique Id'
    1:
      label_en: 'Label EN'
    2:
      label_ro: 'Label RO'
destination:
  plugin: entity:taxonomy_term
  translations: true
process:
  vid:
    plugin: default_value
    default_value: categories
  tid:
    plugin: migration_lookup
    source: id
    migration: categories
  content_translation_source:
    plugin: default_value
    default_value: 'en'

migration_dependencies:
  required:
    - categories

Much of it is like the previous migration. There are some important changes though:

  • We use the deriver key to define the deriver class. This will be the class that creates the individual derivative definitions.
  • We configure the destination plugin to accept entity translations. This is needed to ensure we are saving translations and not source entities. Check out Drupal\migrate\Plugin\migrate\destination\EntityContentBase for more info.
  • Unlike the previous migration, we define also a process mapping for the taxonomy term ID (tid). And we use the migration_lookup process plugin to map the IDs to the ones from the original migration. We do this to ensure that our migrated entity translations are associated to the correct source entities. Check out Drupal\migrate\Plugin\migrate\process\MigrationLookup for how this plugin works.
  • Specific to the destination type (content entities) we need to import a default value also in the content_translation_source if we want the resulting entity translation to be correct. And we just default this to English because that was the default language the original migration imported in. This is the source language in the translation set.
  • Finally, because we need to lookup in the original migration, we also define a migration dependency on the original migration. So that the original gets run, followed by all the translation ones.

You’ll notice another important difference: the term name is missing from the mapping. That will be handled in the deriver based on the actual language of the derivative because this is not something we can determine statically at this stage. So let’s see that now.

In our main module namespace we can create this very simple deriver (which we referenced in the migration above):

namespace Drupal\advanced_migrations;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Deriver for the category translations.
 */
class CategoriesLanguageDeriver extends DeriverBase implements ContainerDeriverInterface {

  /**
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * CategoriesLanguageDeriver constructor.
   *
   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
   */
  public function __construct(LanguageManagerInterface $languageManager) {
    $this->languageManager = $languageManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('language_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $languages = $this->languageManager->getLanguages();
    foreach ($languages as $language) {
      // We skip EN as that is the original language.
      if ($language->getId() === 'en') {
        continue;
      }

      $derivative = $this->getDerivativeValues($base_plugin_definition, $language);
      $this->derivatives[$language->getId()] = $derivative;
    }

    return $this->derivatives;
  }

  /**
   * Creates a derivative definition for each available language.
   *
   * @param array $base_plugin_definition
   * @param LanguageInterface $language
   *
   * @return array
   */
  protected function getDerivativeValues(array $base_plugin_definition, LanguageInterface $language) {
    $base_plugin_definition['process']['name'] = [
      'plugin' => 'skip_on_empty',
      'method' => 'row',
      'source' => 'label_' . $language->getId(),
    ];

    $base_plugin_definition['process']['langcode'] = [
      'plugin' => 'default_value',
      'default_value' => $language->getId(),
    ];

    return $base_plugin_definition;
  }

}

All plugin derivers extend the Drupal\Component\Plugin\Derivative\DeriverBase and have only one method to implement: getDerivativeDefinitions(). And to make our class container aware, we implement the deriver specific ContainerDeriverInterface that provides us with the create() method.

The getDerivativeDefinitions() receives an array which contains the base plugin definition. So essentially our entire YAML migration file turned into an array. And it needs to return an array of derivative definitions keyed by their derivative IDs. And it’s up to us to say what these are. In our case, we simply load all the available languages on the site and create a derivative for each. And the definition of each derivative needs to be a “version” of the base one. And we are free to do what we want with it as long as it still remains correct. So for our purposes, we add two process mappings (the ones we need to determine dynamically):

  • The taxonomy term name. But instead of the simple Get plugin, we use the Drupal\migrate\Plugin\migrate\process\SkipOnEmpty one because we don’t want to create a translation at all for this record if the source column label_[langcode] is missing. Makes sense right? Data is never perfect.
  • The translation langcode which defaults to the current derivative language.

And with this we should be ready. We can clear the cache and inspect our migrations again. We should see a new one with the ID category_translations:ro (the base plugin ID + the derivative ID). And we can now run this migration as well and we’ll have our term translations imported.

Other examples

I think dynamic migrations are extremely powerful in certain cases. Importing translations is an extremely common thing to do and this is a nice way of doing it. But there are other examples as well. For instance, importing Commerce products. You’ll create a migration for the products and one for the product variations. But a product can have multiple variations depending on the actual product specification. For example, the product can have 3 prices depending on 3 delivery options. So you can dynamically create the product variation migrations for each of the delivery option. Or whatever the use case may be.

Conclusion

As we saw, the Drupal 8 migration system is extremely powerful and flexible. It allows us to concoct all sorts of creative ways to read, clean and save our external data into Drupal. But the reason this system is so powerful is because it rests on the lower-level plugin API which is meant to be used for building such systems. So migrate is one of them. But there are others. And the good news is that you can build complex applications that leverage something like the plugin API for extremely creative solutions. But for now, you learned how to get your translations imported which is a big necessity.

May 13 2019
May 13

I’ve been working on a Drupal 7 to 8 migration of content and I encountered in the source body fields a bunch of tables which had a class that styled them in a certain way. One of the requirements was clearly to port the style but improve it using the Bootstrap tables styles and responsiveness. What to do, what to do.

In the source file I was encountering something like this:

...

Which you can argue is not that bad, it only has one class on it and an external style does the job. But obviously it would be better if the stored data didn’t even have that class. So then in the migration I could just kill all the table classes from the body field and apply those stylings externally (all the tables inside the body field). This is a first good step. But what about Bootstrap?

I needed something like this instead to pick up Bootstrap styles:

...

So to make the tables show up with Bootstrap styles I’d have to step on my earlier point of not storing the table classes in the body field storage. Even if I could somehow alter the CKEditor plugin to apply the classes from the widget. And not to mention that if I wanted responsive tables, I’d have to wrap the table element with a

...

. So even more crap to store. No.

Then it dawned on me: why not just store the clean table elements and then, upon rendering, apply the Bootstrap classes, as well as wrap them into the necessary div? After replying Hold my beer to my self-challenging alter ego, I went and I did. So I came up with this little number (will explain after):

getElementsByTagName('table');
    if ($elements->length === 0) {
      return new FilterProcessResult(Html::serialize($dom));
    }

    /** @var \DOMElement $element */
    foreach ($elements as $element) {
      $classes = explode(' ', $element->getAttribute('class'));
      $bootstrap_classes = [
        'table',
        'table-sm',
        'table-striped',
        'table-hover'
      ];

      foreach ($bootstrap_classes as $class) {
        $classes[] = $class;
      }

      $new_element = clone $element;
      $new_element->setAttribute('class', join(' ', array_unique($classes)));

      $wrapper = $dom->createElement('div');
      $wrapper->setAttribute('class', 'table-responsive');
      $wrapper->appendChild($new_element);
      $element->parentNode->replaceChild($wrapper, $element);
    }

    return new FilterProcessResult(Html::serialize($dom));
  }

}

So what do we have here? Well, it’s a Filter plugin that you can add to your text format and which processes the text before it’s rendered. And obviously gets cached after.

In the plugin annotation I used the type \Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE because this doesn’t seem to be skipped by core anywhere and its purpose is to generate HTML. Then I implement the process() method to achieve my goal. And I do this quite easily with the following steps:

  1. Find all the table DOM elements and return early if none are found
  2. Loop through all the table DOM elements, clone them and apply the classes to the clone
  3. Create a wrapper div DOM element with the Bootstrap responsive class and append the table element clone to it
  4. Replace the initial table DOM element with the new wrapper
  5. Profit

The return of the method needs to be a FilterProcessResult object that contains the HTML in the same format as the method receives it in. So I serialize the DOM object back into an HTML string and use that.

And that’s it. After clearing the cache you can add this to a text format and all the tables found in the content rendered using that format will be Bootstrap ready. And tables are just an example. Imagine all the possibilities you have to turn simple HTML tags into the markup required by your corner frontend framework. All the while keeping your data clean and not pissing off the developer that will have to migrate that content somewhere else or render it in some other place differently.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web