Decorators: Implement multilingual alias fallback

Tags: Drupal 8, Design Patterns, Multilingual Content


The decorator design pattern is among popular design patterns. We used the decorator design pattern to implement multilingual fallback of URL alias.

Drupal uses a language fallback feature while displaying translated content. So if you don’t have a translated node, it will show the original node from the base language. Currently in language fallback, if the English (base language) node has a URL alias and no translations, when we try to access the node in a different language, the URL will be something like:

/de/node/nid i.e. in typical node path.

The client’s requirement was that when Drupal uses the language fallback for content, it should do the same for URL aliases as well. While debugging, I found that everything was handled in the AliasStorage class, which is implementing AliasStorageInterface.

To achieve our goal, we wanted to decorate AliasStorage class and implement some of the AliasStorageInterface interface’s function to achieve the desired result.

This is where decorators come into the picture.

To implement a decorator to AliasStorageInterface, we needed to implement one service that would specify where to find the related class implementation.

Implement the services.yml file for your module; let’s say my_module.services.yml.

services:


 my_module.path.decorating_alias_storage:


   class: Drupal\my_module\FallbackAliasStorage


   decorates: path.alias_storage


   arguments: ['@my_module.path.decorating_alias_storage.inner', '@language_manager']


   public: false

 
Here we are specifying decorates attributes and which service to decorate.

You can read more about decorates attributes in the Symfony documentation here.

The decorates option tells the container that the my_module.path.decorating_alias_storage service replaces the path.alias_storage. The old path.alias_storage service is renamed to my_module.path.decorating_alias_storage.inner so you can inject it into your new service.

This way we will override the original path alias service. We will also pass other required services as well.

Can we apply multiple decorators to a single service?

Yes, we can. When you apply multiple decorators to one service, the “Decoration_priority” option will allow you to specify decoration order. 

Decoration_priority’s value is an integer that defaults to 0, and higher priorities mean that decorators will be applied earlier.

Suppose we have multiple decorators given below:

# config/services.yaml
foo:
    class: Foo

bar:
    class: Bar
    public: false
    decorates: foo
    decoration_priority: 5
    arguments: ['@bar.inner']

baz:
    class: Baz
    public: false
    decorates: foo
    decoration_priority: 1
    arguments: ['@baz.inner']


So in this case, the generated code will be evaluated as:

$this->services['foo'] = new Baz(new Bar(new Foo()));


Now we just need to implement the related decorator class.

So we create FallbackAliasStorage class inside the 'my_module/src' folder. You can see class implementation in this gist.

As we want to modify the path look up process and return URL alias of the base language when translation doesn’t exist, so we will focus on the lookupPathAlias() method of class.

/**
  * @inheritdoc
  */

 public function lookupPathAlias($path, $langcode) {
   // If language code is not specified then lookup alias from base/default service.

   if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {

      return $this->subject->lookupPathAlias($path, $langcode);

   }

   // Define the context based on current language to retrieve language fallback             // candidates list from “Drupal\Core\Language\LanguageManager.php”
   $context = [

     'langcode' => $langcode,

     'operation' => 'path_alias',

   ];

   $fallbacks = $this->language_manager->getFallbackCandidates($context);

   unset($fallbacks[LanguageInterface::LANGCODE_NOT_SPECIFIED]);
   // For each fallback language candidate check if alias exists.

   foreach ($fallbacks as $fallback) {

     $alias = $this->subject->lookupPathAlias($path, $fallback);

     if ($alias) {

       return $alias;

     }

   }

   return FALSE;

 }


In this method, we will look up all the relevant aliases of the current language that don’t have a translated alias. If we are viewing translated content, then it will return the URL alias itself.

This way, we can use decorators to implement multilingual URL alias fallback.

Limitations

There are some limitations with this approach as well. 

For example, we may have one node in English language with some alias, say (https://mysite.com/hello-world). When we visit this node in a different language, it will give the URL alias of English node.

Now there is one more translated node with the same alias (node in German language with alias “/hello-world”), so Drupal won’t show that node as we will return the path of a different node as the path of alias fallback.

But considering our site is multi-lingual and so are the aliases, there will not be any chances of collisions like this.

Mohit Aghera, Back-end Developer
Posted on Feb 15, 2018 4:47:05 AM by

Mohit Aghera, Back-end Developer

Offline, if he's not wandering around the city with family or friends, you can find him in his home studio painting away.