3 namespace Drupal\Core\Path;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\CacheDecorator\CacheDecoratorInterface;
7 use Drupal\Core\Language\LanguageInterface;
8 use Drupal\Core\Language\LanguageManagerInterface;
11 * The default alias manager implementation.
13 class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
16 * The alias storage service.
18 * @var \Drupal\Core\Path\AliasStorageInterface
23 * Cache backend service.
25 * @var \Drupal\Core\Cache\CacheBackendInterface
30 * The cache key to use when caching paths.
37 * Whether the cache needs to be written.
41 protected $cacheNeedsWriting = FALSE;
44 * Language manager for retrieving the default langcode when none is specified.
46 * @var \Drupal\Core\Language\LanguageManagerInterface
48 protected $languageManager;
51 * Holds the map of path lookups per language.
55 protected $lookupMap = [];
58 * Holds an array of aliases for which no path was found.
62 protected $noPath = [];
65 * Holds the array of whitelisted path aliases.
67 * @var \Drupal\Core\Path\AliasWhitelistInterface
72 * Holds an array of paths that have no alias.
76 protected $noAlias = [];
79 * Whether preloaded path lookups has already been loaded.
83 protected $langcodePreloaded = [];
86 * Holds an array of previously looked up paths for the current request path.
88 * This will only get populated if a cache key has been set, which for example
89 * happens if the alias manager is used in the context of a request.
93 protected $preloadedPathLookups = FALSE;
96 * Constructs an AliasManager.
98 * @param \Drupal\Core\Path\AliasStorageInterface $storage
99 * The alias storage service.
100 * @param \Drupal\Core\Path\AliasWhitelistInterface $whitelist
101 * The whitelist implementation to use.
102 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
103 * The language manager.
104 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
107 public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
108 $this->storage = $storage;
109 $this->languageManager = $language_manager;
110 $this->whitelist = $whitelist;
111 $this->cache = $cache;
117 public function setCacheKey($key) {
118 // Prefix the cache key to avoid clashes with other caches.
119 $this->cacheKey = 'preload-paths:' . $key;
125 * Cache an array of the paths available on each page. We assume that aliases
126 * will be needed for the majority of these paths during subsequent requests,
127 * and load them in a single query during path alias lookup.
129 public function writeCache() {
130 // Check if the paths for this page were loaded from cache in this request
131 // to avoid writing to cache on every request.
132 if ($this->cacheNeedsWriting && !empty($this->cacheKey)) {
133 // Start with the preloaded path lookups, so that cached entries for other
134 // languages will not be lost.
135 $path_lookups = $this->preloadedPathLookups ?: [];
136 foreach ($this->lookupMap as $langcode => $lookups) {
137 $path_lookups[$langcode] = array_keys($lookups);
138 if (!empty($this->noAlias[$langcode])) {
139 $path_lookups[$langcode] = array_merge($path_lookups[$langcode], array_keys($this->noAlias[$langcode]));
143 $twenty_four_hours = 60 * 60 * 24;
144 $this->cache->set($this->cacheKey, $path_lookups, $this->getRequestTime() + $twenty_four_hours);
151 public function getPathByAlias($alias, $langcode = NULL) {
152 // If no language is explicitly specified we default to the current URL
153 // language. If we used a language different from the one conveyed by the
154 // requested URL, we might end up being unable to check if there is a path
155 // alias matching the URL path.
156 $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
158 // If we already know that there are no paths for this alias simply return.
159 if (empty($alias) || !empty($this->noPath[$langcode][$alias])) {
163 // Look for the alias within the cached map.
164 if (isset($this->lookupMap[$langcode]) && ($path = array_search($alias, $this->lookupMap[$langcode]))) {
168 // Look for path in storage.
169 if ($path = $this->storage->lookupPathSource($alias, $langcode)) {
170 $this->lookupMap[$langcode][$path] = $alias;
174 // We can't record anything into $this->lookupMap because we didn't find any
175 // paths for this alias. Thus cache to $this->noPath.
176 $this->noPath[$langcode][$alias] = TRUE;
184 public function getAliasByPath($path, $langcode = NULL) {
185 if ($path[0] !== '/') {
186 throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
188 // If no language is explicitly specified we default to the current URL
189 // language. If we used a language different from the one conveyed by the
190 // requested URL, we might end up being unable to check if there is a path
191 // alias matching the URL path.
192 $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
194 // Check the path whitelist, if the top-level part before the first /
195 // is not in the list, then there is no need to do anything further,
196 // it is not in the database.
197 if ($path === '/' || !$this->whitelist->get(strtok(trim($path, '/'), '/'))) {
201 // During the first call to this method per language, load the expected
202 // paths for the page from cache.
203 if (empty($this->langcodePreloaded[$langcode])) {
204 $this->langcodePreloaded[$langcode] = TRUE;
205 $this->lookupMap[$langcode] = [];
207 // Load the cached paths that should be used for preloading. This only
208 // happens if a cache key has been set.
209 if ($this->preloadedPathLookups === FALSE) {
210 $this->preloadedPathLookups = [];
211 if ($this->cacheKey) {
212 if ($cached = $this->cache->get($this->cacheKey)) {
213 $this->preloadedPathLookups = $cached->data;
216 $this->cacheNeedsWriting = TRUE;
221 // Load paths from cache.
222 if (!empty($this->preloadedPathLookups[$langcode])) {
223 $this->lookupMap[$langcode] = $this->storage->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
224 // Keep a record of paths with no alias to avoid querying twice.
225 $this->noAlias[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
229 // If we already know that there are no aliases for this path simply return.
230 if (!empty($this->noAlias[$langcode][$path])) {
234 // If the alias has already been loaded, return it from static cache.
235 if (isset($this->lookupMap[$langcode][$path])) {
236 return $this->lookupMap[$langcode][$path];
239 // Try to load alias from storage.
240 if ($alias = $this->storage->lookupPathAlias($path, $langcode)) {
241 $this->lookupMap[$langcode][$path] = $alias;
245 // We can't record anything into $this->lookupMap because we didn't find any
246 // aliases for this path. Thus cache to $this->noAlias.
247 $this->noAlias[$langcode][$path] = TRUE;
254 public function cacheClear($source = NULL) {
256 foreach (array_keys($this->lookupMap) as $lang) {
257 unset($this->lookupMap[$lang][$source]);
261 $this->lookupMap = [];
265 $this->langcodePreloaded = [];
266 $this->preloadedPathLookups = [];
267 $this->cache->delete($this->cacheKey);
268 $this->pathAliasWhitelistRebuild($source);
272 * Rebuild the path alias white list.
274 * @param string $path
275 * An optional path for which an alias is being inserted.
278 * An array containing a white list of path aliases.
280 protected function pathAliasWhitelistRebuild($path = NULL) {
281 // When paths are inserted, only rebuild the whitelist if the path has a top
282 // level component which is not already in the whitelist.
284 if ($this->whitelist->get(strtok($path, '/'))) {
288 $this->whitelist->clear();
292 * Wrapper method for REQUEST_TIME constant.
296 protected function getRequestTime() {
297 return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME'];