2

I've got a project with a multi-user / multi-domain environment. There is one "main" portal like www.myproject.com and for every registered user with a specific custom permission has_personal_subdomain there is a subdomain and a theme with his username, e.g. user johndoe has a subdomain johndoe.myproject.com and this domain is using his custom theme johndoe.

I've already set up all the domains and the theme switching part. All editing/administrative actions are always done on the main portal domain, also users with personal subdomains work on the main portal domain.

The problem are the canonical entity links: When editing nodes, editing is always done in admin theme on the main portal domain, so of course all canonical entity links also point to the portal domain. E.g. for users with a personal subdomain, the canonical links to www.myproject.com/node/123 or www.myproject.com/my-node-url-alias, but I want johndoe.myproject.com/node/123 or johndoe.myproject.com/my-node-url-alias.

What is the best way to dynamically manipulate the canonical entity view links? I'm not asking for a complete code, I'm just looking for the best starting point/hook/event/service to inject my custom logic.

What I want to avoid:
Create a million custom hooks for hook_menu_local_tasks_alter AND hook_form_alter AND hook_views_pre_render AND whatever else...

I do not care about metatags, SEO, duplicate URLs,... I just want a better editor experience with personalized entity view links pointing to the correct subdomain using the correct theme.

I know there is the OutboundPathProcessor, but this seems a bit over the top, as I only want to change canonical URLs and none of the admin links.

Is there a service specifically for generating canonical URLs I can decorate?

  • What is wrong about OutboundPathProcessor? It's perfect for this use case because you can also add cache metadata to $bubbleable_metadata. Which is important when the URLs are dynamic. – 4k4 Aug 14 at 9:13
  • Nothing wrong, it just seemed "oversized" for my use case. AFAIK OutboundPathProcessor runs on every link, and I only need to modify the subset of canonical entity URLs. But if the answer is "there is no better option than OutboundPathProcessor", I can live with it. – Hudri Aug 14 at 9:18
  • I don't see a performance impact, the result is cacheable the same way as if you would change it in a different place, if you find one. Another advantage, you can use the priority to position your path processor before or after other path processors, for example aliasing. – 4k4 Aug 14 at 9:22
2

Is there a service specifically for generating canonical URLs I can decorate?

Yes, for a route based handling of outgoing canonical links you can use OutboundRouteProcessorInterface::processOutbound

For example like core implements the dynamic route <current>.

/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php

  public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
    if ($route_name === '<current>') {
      if ($current_route = $this->routeMatch->getRouteObject()) {
        $requirements = $current_route->getRequirements();
        // Setting _method and _schema is deprecated since 2.7. Using
        // setMethods() and setSchemes() are now the recommended ways.
        unset($requirements['_method']);
        unset($requirements['_schema']);
        $route->setRequirements($requirements);

        $route->setPath($current_route->getPath());
        $route->setSchemes($current_route->getSchemes());
        $route->setMethods($current_route->getMethods());
        $route->setOptions($current_route->getOptions());
        $route->setDefaults($current_route->getDefaults());
        $parameters = array_merge($parameters, $this->routeMatch->getRawParameters()->all());
        if ($bubbleable_metadata) {
          $bubbleable_metadata->addCacheContexts(['route']);
        }
      }
      else {
        // If we have no current route match available, point to the frontpage.
        $route->setPath('/');
      }
    }
  }
| improve this answer | |
  • This looked good, but I've failed to redirect accross domains with this service. So I've been going back to OutboundPathProcessor which allowed me to use $options['absolute'] = TRUE` and $options['base_url'] = $myhost. I was unable to find something similar in OutboundRouteProcessor` – Hudri Aug 17 at 10:31
  • I've answered this specific part of your question because you wanted to know which point/hook/event/service are available, not a complete solution. You can use both by the way, everything you put into the route object is available later in path processing. Starting with the canonical route here has the advantage that you can identify the route by its name without the need to resolve the path backwards. – 4k4 Aug 17 at 11:15
0

I've finally a working version using OutboundPathProcessor.

(I was not able to use OutboundRouteProcessor as suggested by @4k4 because I did not know how to create absolute, cross-domain URL manipulations in there).

This is my code (wt_dgm is my module name):

wt_dgm.services.yml
(I need @current_user and @config.factory for my custom business logic, replace or remove by your needs)

  wt_dgm.path_outbound_host_processor:
    class: Drupal\wt_dgm\Path\OutboundHostProcessor
    arguments: [ '@current_user', '@config.factory']
    tags:
      - { name: path_processor_outbound, priority: 3000 }

OutboundHostProcessor.php

For my use case the key thing was using $options['absolute'] and $options['base_url'], replace with your needs.

<?php

namespace Drupal\wt_dgm\Path;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Symfony\Component\HttpFoundation\Request;

class OutboundHostProcessor implements OutboundPathProcessorInterface {

  /**
   * @var $currentUser AccountProxyInterface
   */
  protected $currentUser;

  /**
   * @var $configFactory ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * @var $frontendCache array['username' => 'scheme:://username.dgmdomain.com']
   */
  protected $frontendCache;

  public function __construct(AccountProxyInterface $current_user, ConfigFactoryInterface $config_factory) {
    $this->currentUser = $current_user;
    $this->configFactory = $config_factory;
    $this->frontendCache = [];
  }

  /**
   * {@inheritdoc}
   */
  public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
    try {
      $url = Url::fromUri('internal:' .  $path);
    }
    catch (\Throwable $throwable) {
      return $path;
    }
    if (!$url) {
      return $path;
    }

    $routeName = $url->getRouteName();
    if ($routeName != 'entity.node.canonical') {
      return $path;
    }

    $params = $url->getRouteParameters();
    if (!isset($params['node'])) {
      return $path;
    }

    $node = $params['node'];
    if (!($node instanceof Node) && is_numeric($node)) {
      $node = Node::load($node);
    }
    if (!($node instanceof Node)) {
      return $path;
    }

    $userDomain = $this->getCustomHost($request);
    if (!$userDomain) {
      return $path;
    }

    $options['absolute'] = TRUE;
    $options['base_url'] = $request->getScheme() . '://' . $userDomain;

    if ($bubbleable_metadata) {
      $bubbleable_metadata->addCacheContexts([
        'user',
      ]);
    }
    return $path;
  }

  protected function getCustomHost(Request $request) {
    // my custom logic here with current user etc
    if ($my_custom_yes) {
      return $myCustomHostname;
    }
    return NULL;
  }
}
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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