Commit e3d86383 authored by Dries's avatar Dries

Issue #1269742 by katbailey, beejeebus, pwolanin, g.oechsler, ksenzee: Make...

Issue #1269742 by katbailey, beejeebus, pwolanin, g.oechsler, ksenzee: Make path lookup code into an pluggable class.
parent daf490d3
......@@ -2490,6 +2490,10 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) {
->register('keyvalue.database', 'Drupal\Core\KeyValueStore\KeyValueDatabaseFactory')
->addArgument(new Reference('database'));
$container->register('path.alias_manager', 'Drupal\Core\Path\AliasManager')
->addArgument(new Reference('database'))
->addArgument(new Reference('keyvalue.database'));
// Register the EntityManager.
$container->register('plugin.manager.entity', 'Drupal\Core\Entity\EntityManager');
}
......
......@@ -2208,7 +2208,7 @@ function url($path = NULL, array $options = array()) {
}
elseif (!empty($path) && !$options['alias']) {
$langcode = isset($options['language']) && isset($options['language']->langcode) ? $options['language']->langcode : '';
$alias = drupal_get_path_alias($original_path, $langcode);
$alias = drupal_container()->get('path.alias_manager')->getPathAlias($original_path, $langcode);
if ($alias != $original_path) {
$path = $alias;
}
......@@ -4922,9 +4922,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
// current_path().
drupal_language_initialize();
// Initialize current_path() prior to invoking hook_init().
drupal_path_initialize();
// Let all modules take action before the menu system handles the request.
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
......
......@@ -2994,7 +2994,7 @@ function _menu_delete_item($item, $force = FALSE) {
* @param $item
* An associative array representing a menu link item, with elements:
* - link_path: (required) The path of the menu item, which should be
* normalized first by calling drupal_get_normal_path() on it.
* normalized first by calling drupal_container()->get('path.alias_manager')->getSystemPath() on it.
* - link_title: (required) Title to appear in menu for the link.
* - menu_name: (optional) The machine name of the menu for the link.
* Defaults to 'tools'.
......
This diff is collapsed.
<?php
/**
* @file
* Contains Drupal\Core\CacheDecorator\AliasManagerCacheDecorator.
*/
namespace Drupal\Core\CacheDecorator;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Path\AliasManagerInterface;
/**
* Class used by the PathSubscriber to get the system path and cache path lookups.
*/
class AliasManagerCacheDecorator implements CacheDecoratorInterface, AliasManagerInterface {
/**
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface;
*/
protected $cache;
/**
* Stack of request paths for use as cids when caching system paths.
*
* @var array
*/
protected $cacheKeys = array();
/**
* Holds an array of previously cached paths based on a request path.
*
* @var array
*/
protected $preloadedPathLookups = array();
/**
* Whether the cache needs to be written.
*
* @var boolean
*/
protected $cacheNeedsWriting = TRUE;
/**
* Constructs a \Drupal\Core\CacheDecorator\AliasManagerCacheDecorator.
*/
public function __construct(AliasManagerInterface $alias_manager, CacheBackendInterface $cache) {
$this->aliasManager = $alias_manager;
$this->cache = $cache;
}
/**
* Implements \Drupal\Core\CacheDecorator\CacheDecoratorInterface::setCacheKey().
*/
public function setCacheKey($key) {
$this->cacheKeys[] = $key;
}
/**
* Implements \Drupal\Core\CacheDecorator\CacheDecoratorInterface::writeCache().
*
* Cache an array of the system 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() {
$path_lookups = $this->getPathLookups();
// Check if the system paths for this page were loaded from cache in this
// request to avoid writing to cache on every request.
if ($this->cacheNeedsWriting && !empty($path_lookups) && !empty($this->cacheKeys)) {
// Use the system path of the current request for the cache ID (cid).
$cid = end($this->cacheKeys);
// Set the path cache to expire in 24 hours.
$expire = REQUEST_TIME + (60 * 60 * 24);
$this->cache->set($cid, $path_lookups, $expire);
}
// We are at the end of the request, so pop off the last request path.
array_pop($this->cacheKeys);
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::getSystemPath().
*/
public function getSystemPath($path, $path_language = NULL) {
$system_path = $this->aliasManager->getSystemPath($path, $path_language);
// We need to pass on the list of previously cached system paths for this
// key to the alias manager for use in subsequent lookups.
$cached = $this->cache->get($system_path);
$cached_paths = array();
if ($cached) {
$cached_paths = $cached->data;
$this->cacheNeedsWriting = FALSE;
}
$this->preloadPathLookups($cached_paths);
return $system_path;
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::getPathAlias().
*/
public function getPathAlias($path, $path_language = NULL) {
return $this->aliasManager->getPathAlias($path, $path_language);
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::getPathLookups().
*/
public function getPathLookups() {
return $this->aliasManager->getPathLookups();
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::preloadPathLookups().
*/
public function preloadPathLookups(array $path_list) {
$this->aliasManager->preloadPathLookups($path_list);
}
}
<?php
/**
* @file
* Contains Drupal\Core\CacheDecorator\CacheDecoratorInterface.
*/
namespace Drupal\Core\CacheDecorator;
/**
* Defines an interface for cache decorator implementations.
*/
interface CacheDecoratorInterface {
/**
* Specify the key to use when writing the cache.
*/
public function setCacheKey($key);
/**
* Write the cache.
*/
public function writeCache();
}
......@@ -13,6 +13,7 @@
use Drupal\Core\DependencyInjection\Compiler\RegisterSerializationClassesPass;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Scope;
use Symfony\Component\HttpKernel\Bundle\Bundle;
......@@ -82,6 +83,20 @@ public function build(ContainerBuilder $container) {
$container->register('nested_matcher', 'Drupal\Core\Routing\NestedMatcher')
->addTag('chained_matcher', array('priority' => 5));
$container
->register('cache.path', 'Drupal\Core\Cache\CacheBackendInterface')
->setFactoryClass('Drupal\Core\Cache\CacheFactory')
->setFactoryMethod('get')
->addArgument('path');
$container->register('path.alias_manager.cached', 'Drupal\Core\CacheDecorator\AliasManagerCacheDecorator')
->addArgument(new Reference('path.alias_manager'))
->addArgument(new Reference('cache.path'));
$container->register('path.crud', 'Drupal\Core\Path\Path')
->addArgument(new Reference('database'))
->addArgument(new Reference('path.alias_manager'));
// The following services are tagged as 'nested_matcher' services and are
// processed in the RegisterNestedMatchersPass compiler pass. Each one
// needs to be set on the matcher using a different method, so we use a
......@@ -110,6 +125,7 @@ public function build(ContainerBuilder $container) {
$container->register('maintenance_mode_subscriber', 'Drupal\Core\EventSubscriber\MaintenanceModeSubscriber')
->addTag('event_subscriber');
$container->register('path_subscriber', 'Drupal\Core\EventSubscriber\PathSubscriber')
->addArgument(new Reference('path.alias_manager.cached'))
->addTag('event_subscriber');
$container->register('legacy_request_subscriber', 'Drupal\Core\EventSubscriber\LegacyRequestSubscriber')
->addTag('event_subscriber');
......
......@@ -7,8 +7,10 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\CacheDecorator\AliasManagerCacheDecorator;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -16,23 +18,31 @@
*/
class PathSubscriber extends PathListenerBase implements EventSubscriberInterface {
protected $aliasManager;
public function __construct(AliasManagerCacheDecorator $alias_manager) {
$this->aliasManager = $alias_manager;
}
/**
* Resolve the system path.
*
* @todo The path system should be objectified to remove the function calls in
* this method.
*
* @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The Event to process.
*/
public function onKernelRequestPathResolve(GetResponseEvent $event) {
$request = $event->getRequest();
$path = $this->extractPath($request);
$path = drupal_get_normal_path($path);
$path = $this->aliasManager->getSystemPath($path);
$this->setPath($request, $path);
$this->aliasManager->setCacheKey($path);
}
/**
* Ensures system paths for the request get cached.
*/
public function onKernelTerminate(PostResponseEvent $event) {
$this->aliasManager->writeCache();
}
/**
......@@ -116,6 +126,7 @@ static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequestLanguageResolve', 150);
$events[KernelEvents::REQUEST][] = array('onKernelRequestFrontPageResolve', 101);
$events[KernelEvents::REQUEST][] = array('onKernelRequestPathResolve', 100);
$events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 200);
return $events;
}
......
......@@ -29,7 +29,6 @@ class RequestCloseSubscriber implements EventSubscriberInterface {
*/
public function onTerminate(PostResponseEvent $event) {
module_invoke_all('exit');
drupal_cache_system_paths();
module_implements_write_cache();
system_run_automated_cron();
}
......@@ -41,7 +40,7 @@ public function onTerminate(PostResponseEvent $event) {
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::TERMINATE][] = array('onTerminate');
$events[KernelEvents::TERMINATE][] = array('onTerminate', 100);
return $events;
}
......
......@@ -101,7 +101,7 @@ public function on403Html(FlattenException $exception, Request $request) {
$system_path = $request->attributes->get('system_path');
watchdog('access denied', $system_path, array(), WATCHDOG_WARNING);
$path = drupal_get_normal_path(config('system.site')->get('page.403'));
$path = $this->container->get('path.alias_manager')->getSystemPath(config('system.site')->get('page.403'));
if ($path && $path != $system_path) {
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
......@@ -173,7 +173,7 @@ public function on404Html(FlattenException $exception, Request $request) {
$_GET['destination'] = $system_path;
}
$path = drupal_get_normal_path(config('system.site')->get('page.404'));
$path = $this->container->get('path.alias_manager')->getSystemPath(config('system.site')->get('page.404'));
if ($path && $path != $system_path) {
// @todo Um, how do I specify an override URL again? Totally not clear. Do
// that and sub-call the kernel rather than using meah().
......
This diff is collapsed.
<?php
/**
* @file
* Contains Drupal\Core\Path\AliasManagerInterface.
*/
namespace Drupal\Core\Path;
interface AliasManagerInterface {
/**
* Given a path alias, return the internal path it represents.
*
* @param $path
* A Drupal path alias.
* @param $path_language
* An optional language code to look up the path in.
*
* @return
* The internal path represented by the alias, or the original alias if no
* internal path was found.
*/
public function getSystemPath($path, $path_language = NULL);
/**
* Given an internal Drupal path, return the alias set by the administrator.
*
* @param $path
* An internal Drupal path.
*
* @param $path_language
* An optional language code to look up the path in.
*
* @return
* An aliased path if one was found, or the original path if no alias was
* found.
*/
public function getPathAlias($path, $path_language = NULL);
/**
* Returns an array of system paths that have been looked up.
*
* @return array
* An array of all system paths that have been looked up during the current
* request.
*/
public function getPathLookups();
/**
* Preload a set of paths for bulk alias lookups.
*
* @param $path_list
* An array of system paths.
*/
public function preloadPathLookups(array $path_list);
}
<?php
/**
* @file
* Contains Drupal\Core\Path\Path.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* Defines a class for CRUD operations on path aliases.
*/
class Path {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs a Path CRUD object.
*
* @param \Drupal\Core\Database\Connection $connection
* A database connection for reading and writing path aliases.
*
* @param \Drupal\Core\Path\AliasManager $alias_manager
* An alias manager with an internal cache of stored aliases.
*
* @todo This class should not take an alias manager in its constructor. Once
* we move to firing an event for CRUD operations instead of invoking a
* hook, we can have a listener that calls cacheClear() on the alias manager.
*/
public function __construct(Connection $connection, AliasManager $alias_manager) {
$this->connection = $connection;
$this->alias_manager = $alias_manager;
}
/**
* Saves a path alias to the database.
*
* @param string $source
* The internal system path.
*
* @param string $alias
* The URL alias.
*
* @param string $langcode
* The language code of the alias.
*
* @param int $pid
* Unique path alias identifier.
*
* @return
* FALSE if the path could not be saved or an associative array containing
* the following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: Unique path alias identifier.
* - langcode: The language code of the alias.
*/
public function save($source, $alias, $langcode = LANGUAGE_NOT_SPECIFIED, $pid = NULL) {
$fields = array(
'source' => $source,
'alias' => $alias,
'langcode' => $langcode,
);
// Insert or update the alias.
if (empty($pid)) {
$query = $this->connection->insert('url_alias')
->fields($fields);
$pid = $query->execute();
$fields['pid'] = $pid;
// @todo: Find a correct place to invoke hook_path_insert().
$hook = 'path_insert';
}
else {
$fields['pid'] = $pid;
$query = $this->connection->update('url_alias')
->fields($fields)
->condition('pid', $pid);
$pid = $query->execute();
// @todo: figure out where we can invoke hook_path_update()
$hook = 'path_update';
}
if ($pid) {
// @todo Switch to using an event for this instead of a hook.
module_invoke_all($hook, $fields);
$this->alias_manager->cacheClear();
return $fields;
}
return FALSE;
}
/**
* Fetches a specific URL alias from the database.
*
* @param $conditions
* An array of query conditions.
*
* @return
* FALSE if no alias was found or an associative array containing the
* following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: Unique path alias identifier.
* - langcode: The language code of the alias.
*/
public function load($conditions) {
$select = $this->connection->select('url_alias');
foreach ($conditions as $field => $value) {
$select->condition($field, $value);
}
return $select
->fields('url_alias')
->execute()
->fetchAssoc();
}
/**
* Deletes a URL alias.
*
* @param array $conditions
* An array of criteria.
*/
public function delete($conditions) {
$path = $this->load($conditions);
$query = $this->connection->delete('url_alias');
foreach ($conditions as $field => $value) {
$query->condition($field, $value);
}
$deleted = $query->execute();
// @todo Switch to using an event for this instead of a hook.
module_invoke_all('path_delete', $path);
$this->alias_manager->cacheClear();
return $deleted;
}
}
......@@ -851,7 +851,7 @@ function block_block_list_alter(&$blocks) {
if ($block->visibility < BLOCK_VISIBILITY_PHP) {
// Compare the lowercase path alias (if any) and internal path.
$path = current_path();
$path_alias = drupal_strtolower(drupal_get_path_alias($path));
$path_alias = drupal_strtolower(drupal_container()->get('path.alias_manager')->getPathAlias($path));
$page_match = drupal_match_path($path_alias, $pages) || (($path != $path_alias) && drupal_match_path($path, $pages));
// When $block->visibility has a value of 0 (BLOCK_VISIBILITY_NOTLISTED),
// the block is displayed on all pages except those listed in $block->pages.
......
......@@ -109,13 +109,13 @@ function testPathLanguageConfiguration() {
'alias' => $custom_path,
'langcode' => LANGUAGE_NOT_SPECIFIED,
);
path_save($edit);
$lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en');
drupal_container()->get('path.crud')->save($edit['source'], $edit['alias'], $edit['langcode']);
$lookup_path = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $node->nid, 'en');
$this->assertEqual($english_path, $lookup_path, t('English language alias has priority.'));
// Same check for language 'xx'.
$lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix);
$lookup_path = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $node->nid, $prefix);
$this->assertEqual($custom_language_path, $lookup_path, t('Custom language alias has priority.'));
path_delete($edit);
drupal_container()->get('path.crud')->delete($edit);
// Create language nodes to check priority of aliases.
$first_node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
......@@ -127,7 +127,7 @@ function testPathLanguageConfiguration() {
'alias' => $custom_path,
'langcode' => 'en',
);
path_save($edit);
drupal_container()->get('path.crud')->save($edit['source'], $edit['alias'], $edit['langcode']);
// Assign a custom path alias to second node with LANGUAGE_NOT_SPECIFIED.
$edit = array(
......@@ -135,7 +135,7 @@ function testPathLanguageConfiguration() {
'alias' => $custom_path,
'langcode' => LANGUAGE_NOT_SPECIFIED,
);
path_save($edit);
drupal_container()->get('path.crud')->save($edit['source'], $edit['alias'], $edit['langcode']);
// Test that both node titles link to our path alias.
$this->drupalGet('<front>');
......
......@@ -417,7 +417,7 @@ function menu_edit_item($form, &$form_state, $type, $item, $menu) {
*/
function menu_edit_item_validate($form, &$form_state) {
$item = &$form_state['values'];
$normal_path = drupal_get_normal_path($item['link_path']);
$normal_path = drupal_container()->get('path.alias_manager')->getSystemPath($item['link_path']);
if ($item['link_path'] != $normal_path) {
drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $item['link_path'], '%normal_path' => $normal_path)));
$item['link_path'] = $normal_path;
......
......@@ -57,9 +57,7 @@ function testPathCache() {
// Visit the alias for the node and confirm a cache entry is created.
cache('path')->flush();
$this->drupalGet($edit['alias']);
// @todo The alias should actually have been cached with the system path as
// the key, see the todo in drupal_cache_system_paths() in path.inc.
$this->assertTrue(cache('path')->get($edit['alias']), 'Cache entry was created.');
$this->assertTrue(cache('path')->get($edit['source']), 'Cache entry was created.');
}
/**
......@@ -94,7 +92,7 @@ function testAdminAlias() {
$this->assertText($node1->label(), 'Changed alias works.');
$this->assertResponse(200);
drupal_static_reset('drupal_lookup_path');
drupal_container()->get('path.alias_manager')->cacheClear();
// Confirm that previous alias no longer works.
$this->drupalGet($previous);
$this->assertNoText($node1->label(), 'Previous alias no longer works.');
......
......@@ -82,7 +82,7 @@ function testAliasTranslation() {
$this->drupalPost(NULL, $edit, t('Save'));
// Clear the path lookup cache.
drupal_lookup_path('wipe');
drupal_container()->get('path.alias_manager')->cacheClear();
// Ensure the node was created.
$french_node = $this->drupalGetNodeByTitle($edit["title"]);
......@@ -121,7 +121,7 @@ function testAliasTranslation() {
// We need to ensure that the user language preference is not taken into
// account while determining the path alias language, because if this
// happens we have no way to check that the path alias is valid: there is no
// path alias for French matching the english alias. So drupal_lookup_path()
// path alias for French matching the english alias. So the alias manager
// needs to use the URL language to check whether the alias is valid.
$this->drupalGet($english_alias);
$this->assertText($english_node->label(), 'Alias for English translation works.');
......@@ -145,20 +145,20 @@ function testAliasTranslation() {
$this->drupalGet($french_alias);
$this->assertResponse(404, 'Alias for French translation is unavailable when URL language negotiation is disabled.');
// drupal_lookup_path() has an internal static cache. Check to see that
// The alias manager has an internal path lookup cache. Check to see that
// it has the appropriate contents at this point.
drupal_lookup_path('wipe');
$french_node_path = drupal_lookup_path('source', $french_alias, $french_node->langcode);
drupal_container()->get('path.alias_manager')->cacheClear();
$french_node_path = drupal_container()->get('path.alias_manager')->getSystemPath($french_alias, $french_node->langcode);
$this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path works.');
// Second call should return the same path.
$french_node_path = drupal_lookup_path('source', $french_alias, $french_node->langcode);
$french_node_path = drupal_container()->get('path.alias_manager')->getSystemPath($french_alias, $french_node->langcode);
$this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path is the same.');
// Confirm that the alias works.
$french_node_alias = drupal_lookup_path('alias', 'node/' . $french_node->nid, $french_node->langcode);
$french_node_alias = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $french_node->nid, $french_node->langcode);
$this->assertEqual($french_node_alias, $french_alias, 'Alias works.');
// Second call should return the same alias.
$french_node_alias = drupal_lookup_path('alias', 'node/' . $french_node->nid, $french_node->langcode);
$french_node_alias = drupal_container()->get('path.alias_manager')->getPathAlias('node/' . $french_node->nid, $french_node->langcode);
$this->assertEqual($french_node_alias, $french_alias, 'Alias is the same.');
}
}
......@@ -75,7 +75,7 @@ function path_admin_overview($keys = NULL) {
// If the system path maps to a different URL alias, highlight this table
// row to let the user know of old aliases.
if ($data->alias != drupal_get_path_alias($data->source, $data->langcode)) {
if ($data->alias != drupal_container()->get('path.alias_manager')->getPathAlias($data->source, $data->langcode)) {
$row['class'] = array('warning');
}
......@@ -217,7 +217,7 @@ function path_admin_form_delete_submit($form, &$form_state) {
*/
function path_admin_form_validate($form, &$form_state) {
$source = &$form_state['values']['source'];
$source = drupal_get_normal_path($source);
$source = drupal_container()->get('path.alias_manager')->getSystemPath($source);
$alias = $form_state['values']['alias'];
$pid = isset($form_state['values']['pid']) ? $form_state['values']['pid'] : 0;
// Language is only set if language.module is enabled, otherwise save for all
......@@ -249,7 +249,15 @@ function path_admin_form_submit($form, &$form_state) {
// Remove unnecessary values.
form_state_values_clean($form_state);
path_save($form_state['values']);
$pid = isset($form_state['values']['pid']) ? $form_state['values']['pid'] : 0;
$source = &$form_state['values']['source'];
$source = drupal_container()->get('path.alias_manager')->getSystemPath($source);
$alias = $form_state['values']['alias'];
// Language is only set if language.module is enabled, otherwise save for all
// languages.
$langcode = isset($form_state['values']['langcode']) ? $form_state['values']['langcode'] : LANGUAGE_NOT_SPECIFIED;
drupal_container()->get('path.crud')->save($source, $alias, $langcode, $pid);
drupal_set_message(t('The alias has been saved.'));
$form_state['redirect'] = 'admin/config/search/path';
......@@ -281,7 +289,7 @@ function path_admin_delete_confirm($form, &$form_state, $path) {
*/
function path_admin_delete_confirm_submit($form, &$form_state) {
if ($form_state['values']['confirm']) {
path_delete($form_state['path']['pid']);
drupal_container()->get('path.crud')->delete(array('pid' => $form_state['path']['pid']));
$form_state['redirect'] = 'admin/config/search/path';
}
}
......
......@@ -20,7 +20,7 @@
* - pid: Unique path alias identifier.
* - langcode: The language code of the alias.
*
* @see path_save()