AliasManager.php

Same filename in this branch
  1. 8.9.x core/modules/path_alias/src/AliasManager.php
Same filename and directory in other branches
  1. 9 core/modules/path_alias/src/AliasManager.php
  2. 10 core/modules/path_alias/src/AliasManager.php
  3. 11.x core/modules/path_alias/src/AliasManager.php

Namespace

Drupal\Core\Path

File

core/lib/Drupal/Core/Path/AliasManager.php

View source
<?php

namespace Drupal\Core\Path;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\CacheDecorator\CacheDecoratorInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;

/**
 * The default alias manager implementation.
 *
 * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
 *   \Drupal\path_alias\AliasManager instead.
 *
 * @see https://www.drupal.org/node/3092086
 */
class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
    use DeprecatedServicePropertyTrait;
    
    /**
     * {@inheritdoc}
     */
    protected $deprecatedProperties = [
        'storage' => 'path.alias_storage',
    ];
    
    /**
     * The path alias repository.
     *
     * @var \Drupal\Core\Path\AliasRepositoryInterface
     */
    protected $pathAliasRepository;
    
    /**
     * Cache backend service.
     *
     * @var \Drupal\Core\Cache\CacheBackendInterface
     */
    protected $cache;
    
    /**
     * The cache key to use when caching paths.
     *
     * @var string
     */
    protected $cacheKey;
    
    /**
     * Whether the cache needs to be written.
     *
     * @var bool
     */
    protected $cacheNeedsWriting = FALSE;
    
    /**
     * Language manager for retrieving the default langcode when none is specified.
     *
     * @var \Drupal\Core\Language\LanguageManagerInterface
     */
    protected $languageManager;
    
    /**
     * Holds the map of path lookups per language.
     *
     * @var array
     */
    protected $lookupMap = [];
    
    /**
     * Holds an array of aliases for which no path was found.
     *
     * @var array
     */
    protected $noPath = [];
    
    /**
     * Holds the array of whitelisted path aliases.
     *
     * @var \Drupal\Core\Path\AliasWhitelistInterface
     */
    protected $whitelist;
    
    /**
     * Holds an array of paths that have no alias.
     *
     * @var array
     */
    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
     */
    protected $preloadedPathLookups = FALSE;
    
    /**
     * Constructs an AliasManager.
     *
     * @param \Drupal\Core\Path\AliasRepositoryInterface $alias_repository
     *   The path alias repository.
     * @param \Drupal\Core\Path\AliasWhitelistInterface $whitelist
     *   The whitelist implementation to use.
     * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
     *   The language manager.
     * @param \Drupal\Core\Cache\CacheBackendInterface $cache
     *   Cache backend.
     */
    public function __construct($alias_repository, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
        if (!$alias_repository instanceof AliasRepositoryInterface) {
            @trigger_error('Passing the path.alias_storage service to AliasManager::__construct() is deprecated in drupal:8.8.0 and will be removed before drupal:9.0.0. Pass the new dependencies instead. See https://www.drupal.org/node/3013865.', E_USER_DEPRECATED);
            $alias_repository = \Drupal::service('path_alias.repository');
        }
        $this->pathAliasRepository = $alias_repository;
        $this->languageManager = $language_manager;
        $this->whitelist = $whitelist;
        $this->cache = $cache;
        // This is used as base class by the new class, so we do not trigger
        // deprecation notices when that or any child class is instantiated.
        $new_class = 'Drupal\\path_alias\\AliasManager';
        if (!is_a($this, $new_class) && class_exists($new_class)) {
            @trigger_error('The \\' . __CLASS__ . ' class is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Instead, use \\' . $new_class . '. See https://drupal.org/node/3092086', E_USER_DEPRECATED);
            // Despite being two different services, hence two different class
            // instances, both the new and the legacy alias managers need to share the
            // same internal state to keep the path/alias lookup optimizations
            // working.
            try {
                $alias_manager = \Drupal::service('path_alias.manager');
                if ($alias_manager instanceof $new_class) {
                    $synced_properties = [
                        'cacheKey',
                        'langcodePreloaded',
                        'lookupMap',
                        'noAlias',
                        'noPath',
                        'preloadedPathLookups',
                    ];
                    foreach ($synced_properties as $property) {
                        $this->{$property} =& $alias_manager->{$property};
                    }
                }
            } catch (ServiceCircularReferenceException $e) {
                // This may happen during installation when the "path_alias" module has
                // not been installed yet. Nothing to do in this case.
            }
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function setCacheKey($key) {
        // Prefix the cache key to avoid clashes with other caches.
        $this->cacheKey = 'preload-paths:' . $key;
    }
    
    /**
     * {@inheritdoc}
     *
     * 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.
     */
    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->getRequestTime() + $twenty_four_hours);
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function getPathByAlias($alias, $langcode = NULL) {
        // If no language is explicitly specified we default to the current URL
        // language. If we used a language different from the one conveyed by the
        // requested URL, we might end up being unable to check if there is a path
        // alias matching the URL path.
        $langcode = $langcode ?: $this->languageManager
            ->getCurrentLanguage(LanguageInterface::TYPE_URL)
            ->getId();
        // If we already know that there are no paths for this alias simply return.
        if (empty($alias) || !empty($this->noPath[$langcode][$alias])) {
            return $alias;
        }
        // Look for the alias within the cached map.
        if (isset($this->lookupMap[$langcode]) && ($path = array_search($alias, $this->lookupMap[$langcode]))) {
            return $path;
        }
        // Look for path in storage.
        if ($path_alias = $this->pathAliasRepository
            ->lookupByAlias($alias, $langcode)) {
            $this->lookupMap[$langcode][$path_alias['path']] = $alias;
            return $path_alias['path'];
        }
        // We can't record anything into $this->lookupMap because we didn't find any
        // paths for this alias. Thus cache to $this->noPath.
        $this->noPath[$langcode][$alias] = TRUE;
        return $alias;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getAliasByPath($path, $langcode = NULL) {
        if ($path[0] !== '/') {
            throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
        }
        // If no language is explicitly specified we default to the current URL
        // language. If we used a language different from the one conveyed by the
        // requested URL, we might end up being unable to check if there is a path
        // alias matching the URL path.
        $langcode = $langcode ?: $this->languageManager
            ->getCurrentLanguage(LanguageInterface::TYPE_URL)
            ->getId();
        // Check the path whitelist, if the top-level part before the first /
        // is not in the list, then there is no need to do anything further,
        // it is not in the database.
        if ($path === '/' || !$this->whitelist
            ->get(strtok(trim($path, '/'), '/'))) {
            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;
                    }
                }
            }
            // 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_key($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
            }
        }
        // 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];
        }
        // 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'];
        }
        // 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;
        return $path;
    }
    
    /**
     * {@inheritdoc}
     */
    public function cacheClear($source = NULL) {
        if ($source) {
            foreach (array_keys($this->lookupMap) as $lang) {
                unset($this->lookupMap[$lang][$source]);
            }
        }
        else {
            $this->lookupMap = [];
        }
        $this->noPath = [];
        $this->noAlias = [];
        $this->langcodePreloaded = [];
        $this->preloadedPathLookups = [];
        $this->cache
            ->delete($this->cacheKey);
        $this->pathAliasWhitelistRebuild($source);
    }
    
    /**
     * Rebuild the path alias white list.
     *
     * @param string $path
     *   An optional path for which an alias is being inserted.
     *
     * @return
     *   An array containing a white list of path aliases.
     */
    protected function pathAliasWhitelistRebuild($path = NULL) {
        // When paths are inserted, only rebuild the whitelist if the path has a top
        // level component which is not already in the whitelist.
        if (!empty($path)) {
            if ($this->whitelist
                ->get(strtok($path, '/'))) {
                return;
            }
        }
        $this->whitelist
            ->clear();
    }
    
    /**
     * Wrapper method for REQUEST_TIME constant.
     *
     * @return int
     */
    protected function getRequestTime() {
        return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME'];
    }

}

Classes

Title Deprecated Summary
AliasManager

in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\path_alias\AliasManager instead.

The default alias manager implementation.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.