6

How do we alter what's returned from $node->url()? or $node->toUrl()?

There are many entity reference fields that don't allow referencing more than one entity type or a custom route, such as entity queue or entity reference fields. For this purpose, so that we can create entity queues that reference other entities other than nodes, I am attempting to create a node with the bundle "redirect", and when the node referenced on the site, it would display the link to the referenced item on a field on that entity field_path (which is a link field).

The problem is that $node->url() or $node->toUrl() is tightly integrated into the node object. In Drupal 7 we had hook_url_outbound_alter which made this kind of thing easy. I would like a Drupal 8 equivalent. So that whenever a url or link to my redirect node is displayed, it is replaced with the value of field_path. Not just that node/ redirects, but that it displays the redirect.

So how do we alter $node->toUrl() if $node->bundle() == 'redirect'?

I have found I can fix this most places on the site using two hooks:

/**
 * Implements hook_entity_view().
 *
 * Gets the output for extra fields.
 */
function mymodule_preprocess_node(&$variables) {
  if ($variables['node']->bundle() == 'redirect') {
    if (!$variables['node']->field_path->isEmpty()) {
      $redirect = $variables['node']->field_path->getValue()[0]['value'];
      $variables['url'] = $redirect;
    }
  }
}

/**
 * Implements hook_link_alter().
 */
function mymodule_link_alter(&$variables) {
  if (empty($variables['options']['external']) && !empty($variables['options']['entity'])) {
    $entity = $variables['options']['entity'];
    if ($entity->getEntityTypeId() == 'node' && $entity->bundle() == 'redirect') {
      if ($variables['url']->getRouteName() == "entity.node.canonical" && !$entity->field_path->isEmpty()) {
        $redirect = $entity->field_path->getValue()[0]['value'];

        if ($new_url = URL::fromUri('internal:'.$redirect)) {
          $variables['url'] = $new_url;
        }
      }
    }
  }
}

But this doesn't help if someone calls the ->url() function without the link wrapping it.

2 Answers 2

4

Path outbound processing still exists: OutboundPathProcessorInterface, check the implementations, like path alias handling. It's a tagged service.

That said, not sure if that's the approach that I'd take, I usually implemented something like that simply by overriding the necessary node templates/preprocess.

3
  • I tested this out, and it only alters paths within drupal, I don't think you can use OutboundPathProcessorInterface to change a node->url() to an external link.
    – oknate
    Commented Sep 26, 2017 at 20:42
  • 1
    I would say it is possible, just probably rather ugly. Language path processing uses exactly that and language doesn't just support path prefixes but also per-language domains. See api.drupal.org/api/drupal/…, which shows how it uses the base_url option to set a different domain
    – Berdir
    Commented Sep 26, 2017 at 23:10
  • Awesome, let me test that out and see how it compares to the other approach of using uri callbacks.
    – oknate
    Commented Sep 27, 2017 at 3:36
3

You can set uri callback function for an entity and for each bundle of an entity. This way, when code calls $node->url(), or $node->toUrl() you can have complete control of the output.

For the node content type, the tricky part is that you have to unset the link template for the canonical link in order for a uri callback to be used instead of the link template. By unsetting the link template for the entity, you have to set a uri callback for all bundles as well as for your specific bundle. So you need two functions, or one function that can handle all the bundles. Below is an example where for the "redirect" content type I use one uri callback and then for all the other bundles, I use another uri callback.

use Drupal\node\NodeInterface;
use Drupal\Core\Url;

/**
 * @file
 * Contains mymodule.module.
 */

/**
 * Implements hook_entity_type_alter().
 */
function mymodule_entity_type_alter(array &$entity_types) {

  // Unset the canonical link template for node content type and
  // replace with a uri callback.
  // Unsetting the uri template is necessary for the uri callbacks to work.
  $entity_types['node']->setUriCallback('mymodule_node_uri');
  $links = $entity_types['node']->get('links');
  unset($links['canonical']);
  $entity_types['node']->set('links', $links);
}

/**
 * Implements hook_entity_bundle_info_alter().
 */
function mymodule_entity_bundle_info_alter(&$bundles) {

  // Set the uri callback for the redirect content type.
  if (isset($bundles['node']['redirect'])) {
    $bundles['node']['redirect']['uri_callback'] = 'mymodule_redirect_uri';
  }
}

/**
 * Bundle URI callback for redirect content type.
 */
function mymodule_redirect_uri(NodeInterface $node) {
  if ($node->field_redirect->isEmpty() === FALSE) {
    return $node->field_redirect[0]->getUrl();
  }
  return new Url('entity.node.canonical', ['node' => $node->id()]);
}

/**
 * Bundle URI callback, all other content types outside of "redirect".
 */
function mymodule_node_uri(NodeInterface $node) {
  return new Url('entity.node.canonical', ['node' => $node->id()]);
}

To understand the order of operations that requires you to unset the link template, you can look at the toUrl() function on the Entity class: /core/lib/Drupal/Core/Entity/Entity.php

if (isset($link_templates[$rel])) {
  // uses link template . . . .
}
else {
  // checks for bundle specific uri callbacks and then entity specific uri callbacks.
}
1
  • 1
    Interesting approach but on the admin content ui I get this error: Uncaught PHP Exception UnexpectedValueException: "Unrouted URIs do not have internal representations." at web/core/lib/Drupal/Core/Url.php line 786 Seems this doesn't work for external urls.
    – mpp
    Commented Jan 2, 2019 at 10:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.