The Path(auto) Less Traveled

Joshua
Senior Drupal Developer
Sep
14
2017

The Path(auto) Less Traveled

The Path(auto) Less Traveled

Building URL Aliases Based on Specific Conditions

Have you ever needed to generate URL aliases for an entity based on specific set of conditions? I was recently on a Drupal 8 project that needed the ability to generate custom URL aliases based on very specific criteria outlined by the client. Out of the box, Pathauto module was not flexible enough to handle the customization, but leveraging its API and providing my own hook implementation in a custom module allowed me to perform conditional checks on data and build the conditional URL structures.

In this blog post, I will walk you through getting set up with some starter code that can be used to provide your own complex logic to generate custom path aliases based on specific business logic.
 

Specs and logic

Desired URL structure:

Here is an example of the desired URL structure for Article nodes provided by the client:

For “Article” content type:
<tag> = <field_tags_reference> | <field_category_reference>
<slug> = <field_custom_path> | <title>
<url> = article/<tag>/<slug>

Note - The values in angle brackets signify a Drupal field on the node. The pipe ( | ) character is seen as an OR operator, for example for <tag>, only if <field_tags_reference> is not present will it consider using <field_category_reference>.

Example URLs:
article/general/example-node
article/general/example-node-custom
article/example-node-custom
 

Pseudo code:

The first step in deciding the programmatic logic for building proper URL aliases is writing out some pseudo code.

To complete the <url>, we need to determine <tag> and <slug>.

If (node has <field_tags_reference>) {
  <tag> = <field_tags_reference>
}
else if (node has <field_category_reference>) {
  <tag> = <field_category_reference>
}
else {
  <tag> = NULL
}

If (node has <field_custom_path>) {
  <slug> = <field_custom_path>
else {
  // Fallback to entity title.
  <slug> = <title>
}

Now we have <tag> and <slug> and have the final URL structure we need in <url> = article/<tag>/<slug>.

We will implement this logic into Drupal programmatically in the next steps.

Setup

Step 1:

To get started, you’ll need Pathauto module installed on your Drupal site.
https://www.drupal.org/project/pathauto

For Drupal 8, you can use `composer require drupal/pathauto`.

Note that Ctools and Token will also be installed, as they are dependencies.
 

Step 2:

Once Pathauto module is installed, we can edit the Pathauto patterns on an entity type-specific basis.

Configuration → Search and Metadata → URL aliases -> Patterns

  • Click the “+Add Pathauto pattern” button.
  • For this example, we are generating URL aliases for nodes, so choose “Content” as the Pattern type.
  • For the Path pattern, enter “article/[node:title]”.
  • For the Content type, choose “Article”.
  • Enter a Label of “Article”
  • Save

If we were to create a new Article node at this point, it would build a nice URL, but Pathauto module out-of-the-box does not allow for condition building of the URLs like we need.

Example: Create a new Article node with the title of “Test”. When the node gets saved, we get a URL of ‘article/test’.

Code

hook_pathauto_pattern_alter()

Remember that [node:title] token we added into the pattern above? We added that basically as a placeholder pattern, that we can override in a custom implementation of hook_pathauto_pattern_alter().

I won’t get into creating a custom module, but in your own custom module, implement hook_pathauto_pattern_alter().

The hook implementation looks like:

function my_module_pathauto_pattern_alter(PathautoPattern &$pattern, array $context) {

}

Your IDE may automatically add the required use statement in the .module file, but if it doesn’t, you can add:

use Drupal\pathauto\Entity\PathautoPattern;

From the alter function, we can figure out what type of data it’s trying to build the URL for, by looking at the $context parameter, which has a key for `module`. The module key of that array provides the entity type, such as node or taxonomy. For this example, we’re only dealing with nodes, but the same technique can be applied to taxonomies or any other entities that allow for pathauto patterns.

We also check the `op` key of the `$context` array to make sure the pathauto pattern generates when nodes are created and/or updated.

Here we can get the node:

if ($context['module'] == 'node' && ($context['op'] == 'insert' || $context['op'] == 'update')) {

 /** @var \Drupal\node\Entity\Node $node */
$node = $context['data']['node'];

Initialize an empty $replacements array, which will have tokens dynamically added to it based on the node-type specific criteria and used to replace the placeholder [node:title] at the end of the logic.

$replacements = [];

Now that we have the node object, we can easily access the bundle or node type and use it in a switch statement for per-node-type logic rules.

switch ($node->getType()) {
  case ‘article’:

Looking at the data we’re needing to extract from the node to build the URL from, we’re going to need to use a few parts of Drupal API. For entity reference fields, such as tag or category, we can get the referenced entities via:

if (!empty($node->get('field_tags_reference')->getValue())) {
      $tags = $node->get('field_tags_reference')
        ->first()
        ->get('entity')
        ->getTarget()
        ->getValue();
      if ($tags) {
        $replacements[] = '[node:field_tags_reference:entity:name]';
      }
    }
    elseif (!empty($node->get('field_category_reference')->getValue())) {
      $categories= $node->get('field_category_reference')
        ->first()
        ->get('entity')
        ->getTarget()
        ->getValue();
      if ($categories) {
        $replacements[] = '[node:field_category_reference:entity:name]';
      }
    }

At the end of the hook_pathauto_pattern_alter() function’s code, there is code to set the new pattern to use which replaces [node:title] with the slash-separated $replacements array.

if ($replacements) {

 // Split the replacements with slashes for URL.
 $replacements = implode('/', $replacements);

 // Replace default [node:title] with logic-derived tokens.
 $pattern->setPattern(preg_replace('/\[node:title(\:[^]]*)?\]/', $replacements . '$1', $pattern->getPattern()));
}

Now, if we edit our Test article node, enter a value into the field_custom_path field, such as ‘custom-path’ and save, our URL now looks like ‘article/custom-path’.

If we were to add Tags or Categories to the node, the logic will decide which to use in the URL.
For example, if we added a Tag, such as “Test Tag”, and added it to the node via the field_tags_reference term reference field, we end up with a URL such as ‘article/test-tag/custom-path’.

In summary, below is the full function used in this specific example. I hope this blog post helps you realize the flexibility this provides and understand a way to build conditional paths with Pathauto.

function my_module_pathauto_pattern_alter(PathautoPattern &$pattern, array $context) {
  // When nodes are created or updated, alter pattern with criteria logic.
  if ($context['module'] == 'node' && ($context['op'] == 'insert' || $context['op'] == 'update')) {

	/** @var \Drupal\node\Entity\Node $node */
	$node = $context['data']['node'];

	$replacements = [];

	switch ($node->getType()) {
  	// Article URL logic.
  	case 'article':
    	if (!empty($node->get('field_tags_reference')->getValue())) {
      	$tags = $node->get('field_tags_reference')
        	->first()
        	->get('entity')
        	->getTarget()
        	->getValue();
      	if ($tags) {
        	$replacements[] = '[node:field_tags_reference:entity:name]';
      	}
    	}
    	elseif (!empty($node->get('field_category_reference')->getValue())) {
      	$categories = $node->get('field_category_reference')
        	->first()
        	->get('entity')
        	->getTarget()
        	->getValue();
      	if ($categories) {
        	$replacements[] = '[node:field_category_reference:entity:name]';
      	}
    	}

    	$field_path = $node->get('field_custom_path')->value;
    	if ($field_path) {
      	$replacements[] = '[node:field_custom_path]';
    	}
    	else {
      	$replacements[] = '[node:title]';
    	}
    	break;

  	default:
    	break;
	}

	if ($replacements) {

  	// Split the replacements with slashes for URL.
  	$replacements = implode('/', $replacements);

  	// Replace default [node:title] with logic-derived tokens.
  	$pattern->setPattern(preg_replace('/\[node:title(\:[^]]*)?\]/', $replacements . '$1', $pattern->getPattern()));
	}
  }
}

Additional Resources
Autocomplete Deluxe Released for D8 | Blog
Building REST Endpoints With Drupal 8 | Blog
Typescript and Drupal.behaviors | Blog

comments powered by Disqus