AliasManager.php 9.09 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Path;

5 6
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\CacheDecorator\CacheDecoratorInterface;
7
use Drupal\Core\Language\LanguageInterface;
8
use Drupal\Core\Language\LanguageManagerInterface;
9

10 11 12
/**
 * The default alias manager implementation.
 */
13
class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
14 15

  /**
16
   * The alias storage service.
17
   *
18
   * @var \Drupal\Core\Path\AliasStorageInterface
19
   */
20
  protected $storage;
21

22 23 24
  /**
   * Cache backend service.
   *
25
   * @var \Drupal\Core\Cache\CacheBackendInterface
26 27 28 29 30 31 32 33 34 35 36 37 38
   */
  protected $cache;

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

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

43
  /**
44
   * Language manager for retrieving the default langcode when none is specified.
45
   *
46
   * @var \Drupal\Core\Language\LanguageManagerInterface
47
   */
48
  protected $languageManager;
49 50 51 52 53 54

  /**
   * Holds the map of path lookups per language.
   *
   * @var array
   */
55
  protected $lookupMap = [];
56 57

  /**
58
   * Holds an array of aliases for which no path was found.
59 60 61
   *
   * @var array
   */
62
  protected $noPath = [];
63 64 65 66

  /**
   * Holds the array of whitelisted path aliases.
   *
67
   * @var \Drupal\Core\Path\AliasWhitelistInterface
68 69 70 71
   */
  protected $whitelist;

  /**
72
   * Holds an array of paths that have no alias.
73 74 75
   *
   * @var array
   */
76
  protected $noAlias = [];
77 78

  /**
79
   * Whether preloaded path lookups has already been loaded.
80
   *
81
   * @var array
82
   */
83
  protected $langcodePreloaded = [];
84 85 86 87

  /**
   * Holds an array of previously looked up paths for the current request path.
   *
88 89
   * 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.
90 91 92
   *
   * @var array
   */
93
  protected $preloadedPathLookups = FALSE;
94

95 96 97
  /**
   * Constructs an AliasManager.
   *
98 99
   * @param \Drupal\Core\Path\AliasStorageInterface $storage
   *   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 105
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache backend.
106
   */
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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    $this->cache = $cache;
  }

  /**
   * {@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.
135
      $path_lookups = $this->preloadedPathLookups ?: [];
136 137 138 139 140 141 142
      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]));
        }
      }

143 144
      $twenty_four_hours = 60 * 60 * 24;
      $this->cache->set($this->cacheKey, $path_lookups, $this->getRequestTime() + $twenty_four_hours);
145
    }
146 147 148
  }

  /**
149
   * {@inheritdoc}
150
   */
151
  public function getPathByAlias($alias, $langcode = NULL) {
152 153 154 155
    // 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.
156
    $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
157 158 159 160

    // If we already know that there are no paths for this alias simply return.
    if (empty($alias) || !empty($this->noPath[$langcode][$alias])) {
      return $alias;
161 162
    }

163 164 165 166 167 168 169 170 171 172 173 174 175 176
    // 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 = $this->storage->lookupPathSource($alias, $langcode)) {
      $this->lookupMap[$langcode][$path] = $alias;
      return $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;
177

178
    return $alias;
179 180 181
  }

  /**
182
   * {@inheritdoc}
183
   */
184
  public function getAliasByPath($path, $langcode = NULL) {
185 186 187
    if ($path[0] !== '/') {
      throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
    }
188 189 190 191
    // 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.
192
    $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
193 194 195 196

    // 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.
197
    if ($path === '/' || !$this->whitelist->get(strtok(trim($path, '/'), '/'))) {
198
      return $path;
199
    }
200 201 202 203 204

    // 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;
205
      $this->lookupMap[$langcode] = [];
206 207 208 209

      // Load the cached paths that should be used for preloading. This only
      // happens if a cache key has been set.
      if ($this->preloadedPathLookups === FALSE) {
210
        $this->preloadedPathLookups = [];
211 212 213 214 215 216 217
        if ($this->cacheKey) {
          if ($cached = $this->cache->get($this->cacheKey)) {
            $this->preloadedPathLookups = $cached->data;
          }
          else {
            $this->cacheNeedsWriting = TRUE;
          }
218 219 220
        }
      }

221
      // Load paths from cache.
222 223
      if (!empty($this->preloadedPathLookups[$langcode])) {
        $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])));
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
      }
    }

    // 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 ($alias = $this->storage->lookupPathAlias($path, $langcode)) {
      $this->lookupMap[$langcode][$path] = $alias;
      return $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;
249 250 251
  }

  /**
252
   * {@inheritdoc}
253 254
   */
  public function cacheClear($source = NULL) {
255 256
    if ($source) {
      foreach (array_keys($this->lookupMap) as $lang) {
257
        unset($this->lookupMap[$lang][$source]);
258 259 260
      }
    }
    else {
261
      $this->lookupMap = [];
262
    }
263 264 265 266
    $this->noPath = [];
    $this->noAlias = [];
    $this->langcodePreloaded = [];
    $this->preloadedPathLookups = [];
267
    $this->cache->delete($this->cacheKey);
268
    $this->pathAliasWhitelistRebuild($source);
269 270 271 272 273
  }

  /**
   * Rebuild the path alias white list.
   *
274 275
   * @param string $path
   *   An optional path for which an alias is being inserted.
276 277 278 279
   *
   * @return
   *   An array containing a white list of path aliases.
   */
280 281 282 283 284
  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, '/'))) {
285
        return;
286
      }
287
    }
288
    $this->whitelist->clear();
289
  }
290 291 292 293 294 295 296 297 298

  /**
   * Wrapper method for REQUEST_TIME constant.
   *
   * @return int
   */
  protected function getRequestTime() {
    return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME'];
  }
299

300
}