Commit 4ab57bed authored by catch's avatar catch

Issue #1910318 by katbailey: Make path resolution the responsibility of a...

Issue #1910318 by katbailey: Make path resolution the responsibility of a series of PathProcessor classes, rather than one PathSubscriber class.
parent 98294873
......@@ -10,6 +10,7 @@
use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterMatchersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass;
......@@ -245,6 +246,7 @@ public function build(ContainerBuilder $container) {
->addTag('event_subscriber');
$container->register('path_subscriber', 'Drupal\Core\EventSubscriber\PathSubscriber')
->addArgument(new Reference('path.alias_manager.cached'))
->addArgument(new Reference('path_processor_manager'))
->addTag('event_subscriber');
$container->register('legacy_request_subscriber', 'Drupal\Core\EventSubscriber\LegacyRequestSubscriber')
->addTag('event_subscriber');
......@@ -270,6 +272,8 @@ public function build(ContainerBuilder $container) {
->addTag('event_subscriber')
->addArgument(array(new Reference('exception_controller'), 'execute'));
$this->registerPathProcessors($container);
$container
->register('transliteration', 'Drupal\Core\Transliteration\PHPTransliteration');
......@@ -375,4 +379,27 @@ protected function registerTwig(ContainerBuilder $container) {
// @see http://drupal.org/node/1804998
->addMethodCall('addExtension', array(new Definition('Twig_Extension_Debug')));
}
/**
* Register services related to path processing.
*/
protected function registerPathProcessors(ContainerBuilder $container) {
// Register the path processor manager service.
$container->register('path_processor_manager', 'Drupal\Core\PathProcessor\PathProcessorManager');
// Register the processor that urldecodes the path.
$container->register('path_processor_decode', 'Drupal\Core\PathProcessor\PathProcessorDecode')
->addTag('path_processor_inbound', array('priority' => 1000));
// Register the processor that resolves the front page.
$container->register('path_processor_front', 'Drupal\Core\PathProcessor\PathProcessorFront')
->addArgument(new Reference('config.factory'))
->addTag('path_processor_inbound', array('priority' => 200));
// Register the alias path processor.
$container->register('path_processor_alias', 'Drupal\Core\PathProcessor\PathProcessorAlias')
->addArgument(new Reference('path.alias_manager'))
->addTag('path_processor_inbound', array('priority' => 100));
// Add the compiler pass that will process the tagged services.
$container->addCompilerPass(new RegisterPathProcessorsPass());
}
}
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services to the 'path_processor_manager service.
*/
class RegisterPathProcessorsPass implements CompilerPassInterface {
/**
* Adds services tagged 'path_processor_inbound' to the path processor manager.
*
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
* The container to process.
*/
public function process(ContainerBuilder $container) {
if (!$container->hasDefinition('path_processor_manager')) {
return;
}
$manager = $container->getDefinition('path_processor_manager');
foreach ($container->findTaggedServiceIds('path_processor_inbound') as $id => $attributes) {
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
$manager->addMethodCall('addInbound', array(new Reference($id), $priority));
}
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\CacheDecorator\AliasManagerCacheDecorator;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
......@@ -20,24 +21,24 @@
class PathSubscriber extends PathListenerBase implements EventSubscriberInterface {
protected $aliasManager;
protected $pathProcessor;
public function __construct(AliasManagerCacheDecorator $alias_manager) {
public function __construct(AliasManagerCacheDecorator $alias_manager, InboundPathProcessorInterface $path_processor) {
$this->aliasManager = $alias_manager;
$this->pathProcessor = $path_processor;
}
/**
* Resolve the system path.
* Converts the request path to a system path.
*
* @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The Event to process.
*/
public function onKernelRequestPathResolve(GetResponseEvent $event) {
public function onKernelRequestConvertPath(GetResponseEvent $event) {
$request = $event->getRequest();
$path = $this->extractPath($request);
$path = $this->aliasManager->getSystemPath($path);
$this->setPath($request, $path);
// If this is the master request, set the cache key for the caching of all
// system paths looked up during the request.
$path = trim($request->getPathInfo(), '/');
$path = $this->pathProcessor->processInbound($path, $request);
$request->attributes->set('system_path', $path);
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
$this->aliasManager->setCacheKey($path);
}
......@@ -50,72 +51,6 @@ public function onKernelTerminate(PostResponseEvent $event) {
$this->aliasManager->writeCache();
}
/**
* Resolve the front-page default 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 onKernelRequestFrontPageResolve(GetResponseEvent $event) {
$request = $event->getRequest();
$path = $this->extractPath($request);
if (empty($path)) {
// @todo Temporary hack. Fix when configuration is injectable.
$path = config('system.site')->get('page.front');
if (empty($path)) {
$path = 'user';
}
}
$this->setPath($request, $path);
}
/**
* Decode language information embedded in the request path.
*
* @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The Event to process.
*/
public function onKernelRequestLanguageResolve(GetResponseEvent $event) {
// We need to act only on the master request, otherwise subrequests will
// inherit the main request path and an infinite loop will be started.
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
$path = _language_resolved_path();
if ($path !== NULL) {
$this->setPath($event->getRequest(), $path);
}
}
}
/**
* Decodes the path of the request.
*
* Parameters in the URL sometimes represent code-meaningful strings. It is
* therefore useful to always urldecode() those values so that individual
* controllers need not concern themselves with it. This is Drupal-specific
* logic and may not be familiar for developers used to other Symfony-family
* projects.
*
* @todo Revisit whether or not this logic is appropriate for here or if
* controllers should be required to implement this logic themselves. If we
* decide to keep this code, remove this TODO.
*
* @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The Event to process.
*/
public function onKernelRequestDecodePath(GetResponseEvent $event) {
$request = $event->getRequest();
$path = $this->extractPath($request);
$path = urldecode($path);
$this->setPath($request, $path);
}
/**
* Registers the methods in this class that should be listeners.
*
......@@ -123,12 +58,8 @@ public function onKernelRequestDecodePath(GetResponseEvent $event) {
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequestDecodePath', 200);
$events[KernelEvents::REQUEST][] = array('onKernelRequestLanguageResolve', 150);
$events[KernelEvents::REQUEST][] = array('onKernelRequestFrontPageResolve', 101);
$events[KernelEvents::REQUEST][] = array('onKernelRequestPathResolve', 100);
$events[KernelEvents::REQUEST][] = array('onKernelRequestConvertPath', 200);
$events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 200);
return $events;
}
}
<?php
/**
* @file
* Contains Drupal\Core\PathProcessor\InboundPathProcessorInterface.
*/
namespace Drupal\Core\PathProcessor;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines an interface for classes that process the inbound path.
*/
interface InboundPathProcessorInterface {
/**
* Processes the inbound path.
*
* @param string $path
* The path to process.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*/
public function processInbound($path, Request $request);
}
<?php
/**
* @file
* Contains \Drupal\Core\PathProcessor\PathProcessorAlias.
*/
namespace Drupal\Core\PathProcessor;
use Drupal\Core\Path\AliasManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Processes the inbound path using path alias lookups.
*/
class PathProcessorAlias implements InboundPathProcessorInterface {
/**
* An alias manager for looking up the system path.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a PathProcessorAlias object.
*
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* An alias manager for looking up the system path.
*/
public function __construct(AliasManagerInterface $alias_manager) {
$this->aliasManager = $alias_manager;
}
/**
* Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound().
*/
public function processInbound($path, Request $request) {
$path = $this->aliasManager->getSystemPath($path);
return $path;
}
}
<?php
/**
* @file
* Contains Drupal\Core\PathProcessor\PathProcessorDecode.
*/
namespace Drupal\Core\PathProcessor;
use Drupal\Core\Config\ConfigFactory;
use Symfony\Component\HttpFoundation\Request;
/**
* Processes the inbound path by urldecoding it.
*
* Parameters in the URL sometimes represent code-meaningful strings. It is
* therefore useful to always urldecode() those values so that individual
* controllers need not concern themselves with it. This is Drupal-specific
* logic and may not be familiar for developers used to other Symfony-family
* projects.
*
* @todo Revisit whether or not this logic is appropriate for here or if
* controllers should be required to implement this logic themselves. If we
* decide to keep this code, remove this TODO.
*/
class PathProcessorDecode implements InboundPathProcessorInterface {
/**
* Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound().
*/
public function processInbound($path, Request $request) {
return urldecode($path);
}
}
<?php
/**
* @file
* Contains Drupal\Core\PathProcessor\PathProcessorFront.
*/
namespace Drupal\Core\PathProcessor;
use Drupal\Core\Config\ConfigFactory;
use Symfony\Component\HttpFoundation\Request;
/**
* Processes the inbound path by resolving it to the front page if empty.
*/
class PathProcessorFront implements InboundPathProcessorInterface {
/**
* A config factory for retrieving required config settings.
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $config;
/**
* Constructs a PathProcessorFront object.
*
* @param Drupal\Core\Config\ConfigFactory $config
* A config factory for retrieving the site front page configuration.
*/
public function __construct(ConfigFactory $config) {
$this->config = $config;
}
/**
* Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound().
*/
public function processInbound($path, Request $request) {
if (empty($path)) {
$path = $this->config->get('system.site')->get('page.front');
if (empty($path)) {
$path = 'user';
}
}
return $path;
}
}
<?php
/**
* @file
* Contains Drupal\Core\PathProcessor\PathProcessorManager.
*/
namespace Drupal\Core\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Path processor manager.
*
* Holds an array of path processor objects and uses them to sequentially process
* a path, in order of processor priority.
*/
class PathProcessorManager implements InboundPathProcessorInterface {
/**
* Holds the array of processors to cycle through.
*
* @var array
* An array whose keys are priorities and whose values are arrays of path
* processor objects.
*/
protected $inboundProcessors = array();
/**
* Holds the array of processors, sorted by priority.
*
* @var array
* An array of path processor objects.
*/
protected $sortedInbound = array();
/**
* Adds an inbound processor object to the $inboundProcessors property.
*
* @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $processor
* The processor object to add.
*
* @param int $priority
* The priority of the processor being added.
*/
public function addInbound(InboundPathProcessorInterface $processor, $priority = 0) {
$this->inboundProcessors[$priority][] = $processor;
$this->sortedInbound = array();
}
/**
* Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound().
*/
public function processInbound($path, Request $request) {
$processors = $this->getInbound();
foreach ($processors as $processor) {
$path = $processor->processInbound($path, $request);
}
return $path;
}
/**
* Returns the sorted array of inbound processors.
*
* @return array
* An array of processor objects.
*/
protected function getInbound() {
if (empty($this->sortedInbound)) {
$this->sortedInbound = $this->sortProcessors('inboundProcessors');
}
return $this->sortedInbound;
}
/**
* Sorts the processors according to priority.
*
* @param string $type
* The processor type to sort, e.g. 'inboundProcessors'.
*/
protected function sortProcessors($type) {
$sorted = array();
krsort($this->{$type});
foreach ($this->{$type} as $processors) {
$sorted = array_merge($sorted, $processors);
}
return $sorted;
}
}
......@@ -282,11 +282,7 @@ function language_from_url($languages, Request $request = NULL) {
case LANGUAGE_NEGOTIATION_URL_PREFIX:
$request_path = urldecode(trim($request->getPathInfo(), '/'));
list($language, $path) = language_url_split_prefix($request_path, $languages);
// Store the correct system path, i.e., the request path without the
// language prefix.
_language_resolved_path($path);
if ($language !== FALSE) {
$language_url = $language->langcode;
......
<?php
/**
* @file
* Contains Drupal\language\HttpKernel\PathProcessorLanguage.
*/
namespace Drupal\language\HttpKernel;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Processes the inbound path using path alias lookups.
*/
class PathProcessorLanguage implements InboundPathProcessorInterface {
protected $moduleHandler;
public function __construct(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
/**
* Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound().
*/
public function processInbound($path, Request $request) {
include_once DRUPAL_ROOT . '/core/includes/language.inc';
$this->moduleHandler->loadInclude('language', 'inc', 'language.negotiation');
$languages = language_list();
list($language, $path) = language_url_split_prefix($path, $languages);
return $path;
}
}
<?php
/**
* @file
* Definition of Drupal\language\LanguageBundle.
*/
namespace Drupal\language;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Reference;
/**
* language dependency injection container.
*/
class LanguageBundle extends Bundle {
/**
* Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
*/
public function build(ContainerBuilder $container) {
// Register the language-based path processor.
$container->register('path_processor_language', 'Drupal\language\HttpKernel\PathProcessorLanguage')
->addArgument(new Reference('module_handler'))
->addTag('path_processor_inbound', array('priority' => 300));
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Definition of Drupal\system\Tests\Path\CrudTest.
* Contains Drupal\system\Tests\Path\AliasTest.
*/
namespace Drupal\system\Tests\Path;
......@@ -15,7 +15,7 @@
/**
* Tests path alias CRUD and lookup functionality.
*/
class AliasTest extends DrupalUnitTestBase {
class AliasTest extends PathUnitTestBase {
public static function getInfo() {
return array(
......@@ -25,18 +25,6 @@ public static function getInfo() {
);
}
public function setUp() {
parent::setUp();
$this->fixtures = new UrlAliasFixtures();
}
public function tearDown() {
$this->fixtures->dropTables(Database::getConnection());
parent::tearDown();
}
function testCRUD() {
//Prepare database table.
$connection = Database::getConnection();
......
<?php
/**
* @file
* Contains Drupal\system\Tests\Path\PathUnitTestBase.
*/
namespace Drupal\system\Tests\Path;
use Drupal\simpletest\DrupalUnitTestBase;
use Drupal\Core\Database\Database;
/**
* Defines a base class for path unit testing.
*/
class PathUnitTestBase extends DrupalUnitTestBase {
public function setUp() {
parent::setUp();
$this->fixtures = new UrlAliasFixtures();
}
public function tearDown() {
$this->fixtures->dropTables(Database::getConnection());
parent::tearDown();
}
}
......@@ -16,7 +16,7 @@ class UrlAliasFixtures {
* The connection to use to create the tables.
*/
public function createTables(Connection $connection) {
$tables = $this->urlAliasTableDefinition();
$tables = $this->tableDefinition();
$schema = $connection->schema();
foreach ($tables as $name => $table) {
......@@ -32,7 +32,7 @@ public function createTables(Connection $connection) {
* The connection to use to drop the tables.
*/
public function dropTables(Connection $connection) {
$tables = $this->urlAliasTableDefinition();
$tables = $this->tableDefinition();
$schema = $connection->schema();
foreach ($tables as $name => $table) {
......@@ -77,7 +77,7 @@ public function sampleUrlAliases() {
* @return array
* Table definitions.
*/
public function urlAliasTableDefinition() {
public function tableDefinition() {
$tables = array();
module_load_install('system');
......
<?php
namespace Drupal\system\Tests\PathProcessor;
use Drupal\Core\Database\Connection;
use Drupal\system\Tests\Path\UrlAliasFixtures;
/**
* Utility methods to provide necessary database tables for tests.
*/
class PathProcessorFixtures extends UrlAliasFixtures {
/**
* Overrides Drupal\system\Tests\Path\UrlAliasFixtures::tableDefinition() .
*/
public function tableDefinition() {
// In addition to the tables added by the parent method, we also need the
// language and variable tables for the path processor tests.
$tables = parent::tableDefinition();
$schema = system_schema();
$tables['variable'] = $schema['variable'];
module_load_install('language');
$schema = language_schema();
$tables['language'] = $schema['language'];
return $tables;
}
}
<?php
/**
* @file
* Contains Drupal\system\Tests\PathProcessor\PathProcessorTest.
*/
namespace Drupal\system\Tests\PathProcessor;
use Drupal\system\Tests\Path\PathUnitTestBase;
use Drupal\Core\Database\Database;
use Drupal\Core\Path\Path;
use Drupal\Core\Path\AliasManager;
use Drupal\Core\PathProcessor\PathProcessorAlias;
use Drupal\Core\PathProcessor\PathProcessorDecode;
use Drupal\Core\PathProcessor\PathProcessorFront;
use Drupal\Core\PathProcessor\PathProcessorManager;
use Drupal\language\HttpKernel\PathProcessorLanguage;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests path processor functionality.
*/
class PathProcessorTest extends PathUnitTestBase {
public static function getInfo() {
return array(
'name' => t('Path Processor Unit Tests'),
'description' => t('Tests processing of the inbound path.'),
'group' => t('Path API'),
);
}
public function setUp() {
parent::setUp();
$this->fixtures = new PathProcessorFixtures();
}
/**
* Tests resolving the inbound path to the system path.
*/
function testProcessInbound() {
// Ensure all tables needed for these tests are created.
$connection = Database::getConnection();