Building more dynamic patterns with Pathauto
The premise
Recently I had a simple, yet challenging task for a client. A content type was supposed to receive an automatic path alias depending on what value was selected in a dropdown. I knew one way of tackling this was to use custom tokens, but there are limits to what you can do with them and you’re kind of polluting the list of tokens that way.
So instead, I looked at the Pathauto pattern form and saw something familiar: Condition plugins. I figured if Pathauto was using those, we should be able to add even more conditions to a pattern, provided Pathauto would correctly invoke all of them instead of a hard-coded few. Turns out it does! Lucky me.
So for the sake of this blog post, assume a content type called Event. It has a field called Event list that is of type List (Text) with the form display Select list and a cardinality of 1. In other words: It’s a single-value dropdown. Depending on what value you select, the event shows up in different views and has a different path alias.
The battle plan
So now that we know Pathauto supports Condition plugins, what do we need to do? Not that much really. We need to:
- Write a Condition plugin that allows you to select Event content of a specific Event list
- Alter the Pathauto pattern form to show your condition
- Alter the Pathauto pattern form to actually save your condition on the pattern
Writing the Condition plugin
The code below does not assume you know anything about writing Drupal 8 plugins. It should be self-explanatory for the most part, but it will definitely help if you study up on the plugin system. I’d advise watching one of the sessions by Joe Shindelar, he explains it really well.
So in order to properly define a plugin, we need to do two things:
- Create the actual plugin in
my_module/src/Plugin/Condition/EventList.php
- Write a schema for your plugin’s custom configuration in
my_module/config/schema/my_module.schema.yml
The actual plugin
<?php namespace Drupal\my_module\Plugin\Condition; use Drupal\Core\Condition\ConditionPluginBase; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides an 'Event list' condition. * * @Condition( * id = "event_list", * label = @Translation("Event list"), * context = { * "node" = @ContextDefinition("entity:node", label = @Translation("Node")) * } * ) */ class EventList extends ConditionPluginBase implements ContainerFactoryPluginInterface { /** * The entity field manager. * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ protected $fieldManager; /** * Creates a new EventList instance. * * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager * The entity field manager. * @param array $configuration * The plugin configuration, i.e. an array with configuration values keyed * by configuration option name. The special key 'context' may be used to * initialize the defined contexts by setting it to an array of context * values keyed by context names. * @param string $plugin_id * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. */ public function __construct(EntityFieldManagerInterface $field_manager, array $configuration, $plugin_id, $plugin_definition) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->fieldManager = $field_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $container->get('entity_field.manager'), $configuration, $plugin_id, $plugin_definition ); } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { $form['event_list'] = [ '#title' => $this->t('Event list'), '#type' => 'select', '#options' => $this->getEventLists(), '#default_value' => $this->configuration['event_list'], '#empty_value' => '', ]; return parent::buildConfigurationForm($form, $form_state); } /** * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { $this->configuration['event_list'] = $form_state->getValue('event_list'); parent::submitConfigurationForm($form, $form_state); } /** * {@inheritdoc} */ public function summary() { if ($event_list = $this->configuration['event_list']) { $event_lists = $this->getEventLists(); $replace = ['@event_list' => $event_lists[$event_list]]; return $this->isNegated() ? $this->t('The event is not listed on the @event_list list', $replace) : $this->t('The event is listed on the @event_list list', $replace); } // If no list is selected it means the event should not be listed. return $this->isNegated() ? $this->t('The event is listed') : $this->t('The event is not listed'); } /** * {@inheritdoc} */ public function evaluate() { $node = $this->getContextValue('node'); // This condition always fails if the node is not an event. if ($node->bundle() != 'event') { return FALSE; } // Check whether the node is listed on the selected list. $node_event_list = $node->field_event_list->value; if ($event_list = $this->configuration['event_list']) { return $node_event_list == $event_list xor $this->isNegated(); } // If no list is selected it means the event should not be listed. return empty($node_event_list) xor $this->isNegated(); } /** * {@inheritdoc} */ public function defaultConfiguration() { return ['event_list' => ''] + parent::defaultConfiguration(); } /** * Retrieves the possible event lists. * * @return string[] * The list of possible event lists, keyed by their machine name. */ public function getEventLists() { $definitions = $this->fieldManager->getFieldStorageDefinitions('node'); $definition = $definitions['field_event_list']; return $definition->getSetting('allowed_values'); } }
So as you can see there’s nothing too fancy going on in here.
The annotation specifies that we are creating a condition which requires a node to function and we override the constructor to inject the entity field manager which we will use to retrieve our Event list field with.
We have one custom configuration entry ‘event_list’
which can be configured using a select element. It gets its values straight from the Event list field in getEventLists()
so we avoid any code duplication. It also defaults to nothing being selected because we will show the condition on all Pathauto patterns concerning nodes, even those that are not an Event.
In summary()
you’ll notice we check whether an event list was selected and return a string summarizing how the plugin was configured. In evaluate()
we check whether a passed in node meets the configured condition. Every condition comes with a Negate checkbox by default, which we need to account for using isNegated()
.
The plugin schema
As mentioned above we need to make sure the config system can recognize our plugin. In order to do so we need to document our single custom configuration key: ‘event_list’
.
condition.plugin.event_list: type: condition.plugin mapping: event_list: type: string
Hooking it up to the form
So now that we have our custom condition, let’s add it to the Pathauto pattern form.
p /** * Implements hook_form_FORM_ID_alter(). * * @see \Drupal\pathauto\Form\PatternEditForm */ function my_module_form_pathauto_pattern_form_alter(&$form, FormStateInterface $form_state) { /** @var \Drupal\pathauto\Entity\PathautoPattern $entity */ $entity = $form_state->getFormObject()->getEntity(); if ($entity->get('type') == 'canonical_entities:node') { // Search the Pathauto pattern for our event_list condition. foreach ($entity->getSelectionConditions() as $candidate) { if ($candidate->getPluginId() == 'event_list') { $condition = $candidate; break; } } // If we could not find a configured condition, create an empty one. if (empty($condition)) { /** @var \Drupal\Core\Condition\ConditionManager $condition_manager */ $condition_manager = \Drupal::service('plugin.manager.condition'); $condition = $condition_manager->createInstance('event_list'); } // Add the condition plugin form to the form, removing the negate checkbox. $condition_form = $condition->buildConfigurationForm($form['pattern_container'], $form_state); unset($condition_form['negate']); $form['pattern_container'] += $condition_form; // Add our submit handler after ::submit but before ::save. $offset = array_search('::submit', $form['actions']['submit']['#submit']) + 1; array_splice($form['actions']['submit']['#submit'], $offset, 0 , 'my_module_pathauto_pattern_submit'); } }
You’ll notice we check for the Pathauto pattern type being ‘canonical_entities:node’
. Out of the box, Pathauto patterns only serves types with IDs matching ‘canonical_entities:ENTITY_TYPE_ID’
. Since we’re dealing with nodes, that’s what we’re checking for.
Next we check whether the Pathauto pattern already has our condition set in case we’re on an edit form. If it does, we use that one. If not, we instantiate an empty copy of our condition. We add our condition’s form to the whole and set an extra submit handler.
<?php /** * Submit handler for form ID pathauto_pattern_form. * * @see my_module_form_pathauto_pattern_form_alter() */ function my_module_pathauto_pattern_submit($form, FormStateInterface $form_state) { /** @var \Drupal\pathauto\Entity\PathautoPattern $entity */ $entity = $form_state->getFormObject()->getEntity(); if ($event_list = $form_state->getValue('event_list')) { $entity->addSelectionCondition([ 'id' => 'event_list', 'event_list' => $event_list, 'negate' => FALSE, 'context_mapping' => [ 'node' => 'node', ], ]); } }
The only thing worth noting here is that we added our submit handler before the Pathauto pattern gets saved in PatternEditForm::save()
, so we don’t even need to take care of saving the pattern. How nice.
That’s it!
Here are a few screenshots showing you what it looks like in the UI. You’ll notice it fits in there seamlessly, as if it were part of Pathauto to begin with.
Considerations
You are duplicating the Event check
Yes I am, I should probably check for the presence of the field_event_list field instead of the event bundle and call the plugin something fancy like “Has an Event list field with a specific value”. Why don’t I then? Because reasons. Mainly laziness induced by writing a blog post about this.
Oh em gee this is awesome!
Yes it is. The only thing that would make it even more awesome is Pathauto allowing us to hook up extra conditions more easily instead of hard-coding a few in PatternEditForm.
You can also do this another way
Yes.
You didn’t answer the previous statement
Yes.