Unverified Commit 4ba82cdc authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3496369 by catch, berdir, oily, darvanen, alexpott: Multiple load path...

Issue #3496369 by catch, berdir, oily, darvanen, alexpott: Multiple load path aliases without the preload cache
parent 6bce3bb2
Loading
Loading
Loading
Loading
Loading
+0 −12
Original line number Diff line number Diff line
@@ -27382,18 +27382,6 @@
	'count' => 1,
	'path' => __DIR__ . '/modules/path_alias/src/Entity/PathAlias.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\path_alias\\\\EventSubscriber\\\\PathAliasSubscriber\\:\\:onKernelController\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/path_alias/src/EventSubscriber/PathAliasSubscriber.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\path_alias\\\\EventSubscriber\\\\PathAliasSubscriber\\:\\:onKernelTerminate\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/path_alias/src/EventSubscriber/PathAliasSubscriber.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\Tests\\\\path_alias\\\\Functional\\\\Rest\\\\PathAliasJsonAnonTest\\:\\:assertAuthenticationEdgeCases\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
+0 −35
Original line number Diff line number Diff line
@@ -4,7 +4,6 @@

namespace Drupal\Tests\path\Functional;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Tests\WaitTerminateTestTrait;
@@ -52,40 +51,6 @@ protected function setUp(): void {
    $this->setWaitForTerminate();
  }

  /**
   * Tests the path cache.
   */
  public function testPathCache(): void {
    // Create test node.
    $node1 = $this->drupalCreateNode();

    // Create alias.
    $edit = [];
    $edit['path[0][value]'] = '/node/' . $node1->id();
    $edit['alias[0][value]'] = '/' . $this->randomMachineName(8);
    $this->drupalGet('admin/config/search/path/add');
    $this->submitForm($edit, 'Save');

    // Check the path alias prefix list cache.
    $prefix_list = \Drupal::cache('bootstrap')->get('path_alias_prefix_list');
    $this->assertTrue($prefix_list->data['node']);
    $this->assertFalse($prefix_list->data['admin']);

    // Visit the system path for the node and confirm a cache entry is
    // created.
    \Drupal::cache('data')->deleteAll();
    // Make sure the path is not converted to the alias.
    $this->drupalGet(trim($edit['path[0][value]'], '/'), ['alias' => TRUE]);
    $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.');

    // Visit the alias for the node and confirm a cache entry is created.
    \Drupal::cache('data')->deleteAll();
    // @todo Remove this once https://www.drupal.org/node/2480077 lands.
    Cache::invalidateTags(['rendered']);
    $this->drupalGet(trim($edit['alias[0][value]'], '/'));
    $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.');
  }

  /**
   * Tests alias functionality through the admin interfaces.
   */
+0 −3
Original line number Diff line number Diff line
@@ -4,9 +4,6 @@ parameters:
services:
  _defaults:
    autoconfigure: true
  path_alias.subscriber:
    class: Drupal\path_alias\EventSubscriber\PathAliasSubscriber
    arguments: ['@path_alias.manager', '@path.current']
  path_alias.path_processor:
    class: Drupal\path_alias\PathProcessor\AliasPathProcessor
    tags:
+46 −86
Original line number Diff line number Diff line
@@ -12,20 +12,6 @@
 */
class AliasManager implements AliasManagerInterface {

  /**
   * The cache key to use when caching paths.
   *
   * @var string
   */
  protected $cacheKey;

  /**
   * Whether the cache needs to be written.
   *
   * @var bool
   */
  protected $cacheNeedsWriting = FALSE;

  /**
   * Holds the map of path lookups per language.
   *
@@ -48,21 +34,9 @@ class AliasManager implements AliasManagerInterface {
  protected $noAlias = [];

  /**
   * Whether preloaded path lookups has already been loaded.
   *
   * @var array
   */
  protected $langcodePreloaded = [];

  /**
   * Holds an array of previously looked up paths for the current request path.
   *
   * This will only get populated if a cache key has been set, which for example
   * happens if the alias manager is used in the context of a request.
   *
   * @var array
   * Holds an array of paths that have been requested but not loaded yet.
   */
  protected $preloadedPathLookups = FALSE;
  protected array $requestedPaths = [];

  public function __construct(
    protected AliasRepositoryInterface $pathAliasRepository,
@@ -74,37 +48,25 @@ public function __construct(
  }

  /**
   * {@inheritdoc}
   * Sets the cache key for the preload alias cache.
   *
   * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There
   *   is no replacement.
   * @see https://www.drupal.org/node/3532412
   */
  public function setCacheKey($key) {
    // Prefix the cache key to avoid clashes with other caches.
    $this->cacheKey = 'preload-paths:' . $key;
    @trigger_error(__METHOD__ . ' is deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There is no replacement. See https://www.drupal.org/node/3532412', E_USER_DEPRECATED);
  }

  /**
   * {@inheritdoc}
   * Writes to the per-page system path cache.
   *
   * Cache an array of the paths available on each page. We assume that aliases
   * will be needed for the majority of these paths during subsequent requests,
   * and load them in a single query during path alias lookup.
   * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There
   *   is no replacement.
   * @see https://www.drupal.org/node/3532412
   */
  public function writeCache() {
    // Check if the paths for this page were loaded from cache in this request
    // to avoid writing to cache on every request.
    if ($this->cacheNeedsWriting && !empty($this->cacheKey)) {
      // Start with the preloaded path lookups, so that cached entries for other
      // languages will not be lost.
      $path_lookups = $this->preloadedPathLookups ?: [];
      foreach ($this->lookupMap as $langcode => $lookups) {
        $path_lookups[$langcode] = array_keys($lookups);
        if (!empty($this->noAlias[$langcode])) {
          $path_lookups[$langcode] = array_merge($path_lookups[$langcode], array_keys($this->noAlias[$langcode]));
        }
      }

      $twenty_four_hours = 60 * 60 * 24;
      $this->cache->set($this->cacheKey, $path_lookups, $this->time->getRequestTime() + $twenty_four_hours);
    }
    @trigger_error(__METHOD__ . ' is deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There is no replacement. See https://www.drupal.org/node/3532412', E_USER_DEPRECATED);
  }

  /**
@@ -160,35 +122,27 @@ public function getAliasByPath($path, $langcode = NULL) {
      return $path;
    }

    // During the first call to this method per language, load the expected
    // paths for the page from cache.
    if (empty($this->langcodePreloaded[$langcode])) {
      $this->langcodePreloaded[$langcode] = TRUE;
      $this->lookupMap[$langcode] = [];

      // Load the cached paths that should be used for preloading. This only
      // happens if a cache key has been set.
      if ($this->preloadedPathLookups === FALSE) {
        $this->preloadedPathLookups = [];
        if ($this->cacheKey) {
          if ($cached = $this->cache->get($this->cacheKey)) {
            $this->preloadedPathLookups = $cached->data;
          }
          else {
            $this->cacheNeedsWriting = TRUE;
          }
    // If we already know that there are no aliases for this path simply return.
    if (!empty($this->noAlias[$langcode][$path])) {
      return $path;
    }
    // If the alias has already been loaded, return it from static cache.
    if (isset($this->lookupMap[$langcode][$path])) {
      return $this->lookupMap[$langcode][$path];
    }

      // Load paths from cache.
      if (!empty($this->preloadedPathLookups[$langcode])) {
        $this->lookupMap[$langcode] = $this->pathAliasRepository->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
        // Keep a record of paths with no alias to avoid querying twice.
        $this->noAlias[$langcode] = array_flip(array_diff($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
      }
    // Add the path to the list of requested paths.
    $this->requestedPaths[$langcode][$path] = $path;

    // If we're inside a Fiber, suspend now, this allows other fibers to collect
    // more requested paths.
    if (\Fiber::getCurrent() !== NULL) {
      \Fiber::suspend();
    }

    // If we already know that there are no aliases for this path simply return.
    // If we reach here, then either there are no other Fibers, or none of them
    // have aliases left to look up. Check the static caches in case the path
    // we're looking for was looked up in the meantime.
    if (!empty($this->noAlias[$langcode][$path])) {
      return $path;
    }
@@ -198,15 +152,23 @@ public function getAliasByPath($path, $langcode = NULL) {
      return $this->lookupMap[$langcode][$path];
    }

    // Try to load alias from storage.
    if ($path_alias = $this->pathAliasRepository->lookupBySystemPath($path, $langcode)) {
      $this->lookupMap[$langcode][$path] = $path_alias['alias'];
      return $path_alias['alias'];
    $this->lookupMap[$langcode] = array_merge($this->lookupMap[$langcode] ?? [], $this->pathAliasRepository->preloadPathAlias($this->requestedPaths[$langcode], $langcode));

    // Keep a record of paths with no alias to avoid querying twice.
    $this->noAlias[$langcode] = array_merge($this->noAlias[$langcode] ?? [], array_diff_key($this->requestedPaths[$langcode], $this->lookupMap[$langcode]));

    // Unset the requested paths variable now they've been loaded.
    unset($this->requestedPaths[$langcode]);

    // If we already know that there are no aliases for this path simply return.
    if (!empty($this->noAlias[$langcode][$path])) {
      return $path;
    }

    // We can't record anything into $this->lookupMap because we didn't find any
    // aliases for this path. Thus cache to $this->noAlias.
    $this->noAlias[$langcode][$path] = TRUE;
    // If the alias has already been loaded, return it from static cache.
    if (isset($this->lookupMap[$langcode][$path])) {
      return $this->lookupMap[$langcode][$path];
    }
    return $path;
  }

@@ -228,8 +190,6 @@ public function cacheClear($source = NULL) {
    }
    $this->noPath = [];
    $this->noAlias = [];
    $this->langcodePreloaded = [];
    $this->preloadedPathLookups = [];
    $this->pathAliasPrefixListRebuild($source);
  }

+0 −78
Original line number Diff line number Diff line
<?php

namespace Drupal\path_alias\EventSubscriber;

use Drupal\Core\Path\CurrentPathStack;
use Drupal\path_alias\AliasManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;

/**
 * Provides a path subscriber that converts path aliases.
 */
class PathAliasSubscriber implements EventSubscriberInterface {

  /**
   * The alias manager that caches alias lookups based on the request.
   *
   * @var \Drupal\path_alias\AliasManagerInterface
   */
  protected $aliasManager;

  /**
   * The current path.
   *
   * @var \Drupal\Core\Path\CurrentPathStack
   */
  protected $currentPath;

  /**
   * Constructs a new PathSubscriber instance.
   *
   * @param \Drupal\path_alias\AliasManagerInterface $alias_manager
   *   The alias manager.
   * @param \Drupal\Core\Path\CurrentPathStack $current_path
   *   The current path.
   */
  public function __construct(AliasManagerInterface $alias_manager, CurrentPathStack $current_path) {
    $this->aliasManager = $alias_manager;
    $this->currentPath = $current_path;
  }

  /**
   * Sets the cache key on the alias manager cache decorator.
   *
   * KernelEvents::CONTROLLER is used in order to be executed after routing.
   *
   * @param \Symfony\Component\HttpKernel\Event\ControllerEvent $event
   *   The Event to process.
   */
  public function onKernelController(ControllerEvent $event) {
    // Set the cache key on the alias manager cache decorator.
    if ($event->isMainRequest()) {
      $this->aliasManager->setCacheKey(rtrim($this->currentPath->getPath($event->getRequest()), '/'));
    }
  }

  /**
   * Ensures system paths for the request get cached.
   */
  public function onKernelTerminate(TerminateEvent $event) {
    $this->aliasManager->writeCache();
  }

  /**
   * Registers the methods in this class that should be listeners.
   *
   * @return array
   *   An array of event listener definitions.
   */
  public static function getSubscribedEvents(): array {
    $events[KernelEvents::CONTROLLER][] = ['onKernelController', 200];
    $events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 200];
    return $events;
  }

}
Loading