Commit 0f752ca6 authored by catch's avatar catch

Issue #1906810 by dawehner, damiankloip, tstoeckler, kgoel, fubhy, jrglasgow,...

Issue #1906810 by dawehner, damiankloip, tstoeckler, kgoel, fubhy, jrglasgow, xjm, Gaelan, socketwench: Require type hints for automatic entity upcasting.
parent a4d7fad2
......@@ -451,11 +451,14 @@ services:
tags:
- { name: event_subscriber }
arguments: ['@module_handler']
resolver_manager.entity:
class: Drupal\Core\Entity\EntityResolverManager
arguments: ['@entity.manager', '@controller_resolver', '@class_resolver']
route_subscriber.entity:
class: Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber
tags:
- { name: event_subscriber }
arguments: ['@entity.manager']
arguments: ['@resolver_manager.entity']
reverse_proxy_subscriber:
class: Drupal\Core\EventSubscriber\ReverseProxySubscriber
tags:
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityResolverManager.
*/
namespace Drupal\Core\Entity;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Symfony\Component\Routing\Route;
/**
* Sets the entity route parameter converter options automatically.
*
* If controllers of routes with route parameters, type-hint the parameters with
* an entity interface, upcasting is done automatically.
*/
class EntityResolverManager {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* Constructs a new EntityRouteAlterSubscriber.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
*/
public function __construct(EntityManagerInterface $entity_manager, ControllerResolverInterface $controller_resolver, ClassResolverInterface $class_resolver) {
$this->entityManager = $entity_manager;
$this->controllerResolver = $controller_resolver;
$this->classResolver = $class_resolver;
}
/**
* Creates a controller instance using route defaults.
*
* By design we cannot support all possible routes, but just the ones which
* use the defaults provided by core, which are _content, _controller
* and _form.
*
* @param array $defaults
* The default values provided by the route.
*
* @return array|null
* Returns the controller instance if it is possible to instantiate it, NULL
*/
protected function getController(array $defaults) {
$controller = NULL;
if (isset($defaults['_content'])) {
$controller = $this->controllerResolver->getControllerFromDefinition($defaults['_content']);
}
if (isset($defaults['_controller'])) {
$controller = $this->controllerResolver->getControllerFromDefinition($defaults['_controller']);
}
if (isset($defaults['_form'])) {
$form_arg = $defaults['_form'];
// Check if the class exists first as the class resolver will throw an
// exception if it doesn't. This also means a service cannot be used here.
if (class_exists($form_arg)) {
$controller = array($this->classResolver->getInstanceFromDefinition($form_arg), 'buildForm');
}
}
return $controller;
}
/**
* Sets the upcasting information using reflection.
*
* @param array $controller
* An array of class instance and method name.
* @param \Symfony\Component\Routing\Route $route
* The route object to populate without upcasting information.
*
* @return bool
* Returns TRUE if the upcasting parameters could be set, FALSE otherwise.
*/
protected function setParametersFromReflection(array $controller, Route $route) {
$entity_types = $this->getEntityTypes();
$parameter_definitions = $route->getOption('parameters') ?: array();
$result = FALSE;
list($instance, $method) = $controller;
$reflection = new \ReflectionMethod($instance, $method);
$parameters = $reflection->getParameters();
foreach ($parameters as $parameter) {
$parameter_name = $parameter->getName();
// If the parameter name matches with an entity type try to set the
// upcasting information automatically. Therefore take into account that
// the user has specified some interface, so the upasting is intended.
if (isset($entity_types[$parameter_name])) {
$entity_type = $entity_types[$parameter_name];
$entity_class = $entity_type->getClass();
if (($reflection_class = $parameter->getClass()) && (is_subclass_of($entity_class, $reflection_class->name) || $entity_class == $reflection_class->name)) {
$parameter_definitions += array($parameter_name => array());
$parameter_definitions[$parameter_name] += array(
'type' => 'entity:' . $parameter_name,
);
$result = TRUE;
}
}
}
if (!empty($parameter_definitions)) {
$route->setOption('parameters', $parameter_definitions);
}
return $result;
}
/**
* Sets the upcasting information using the _entity_* route defaults.
*
* Supported are the '_entity_view', '_entity_list' and '_entity_form' route
* defaults.
*
* @param \Symfony\Component\Routing\Route $route
* The route object.
*/
protected function setParametersFromEntityInformation(Route $route) {
if ($entity_view = $route->getDefault('_entity_view')) {
list($entity_type) = explode('.', $entity_view, 2);
}
elseif ($entity_list = $route->getDefault('_entity_list')) {
$entity_type = $entity_list;
}
elseif ($entity_form = $route->getDefault('_entity_form')) {
list($entity_type) = explode('.', $entity_form, 2);
}
if (isset($entity_type) && isset($this->getEntityTypes()[$entity_type])) {
$parameter_definitions = $route->getOption('parameters') ?: array();
// First try to figure out whether there is already a parameter upcasting
// the same entity type already.
foreach ($parameter_definitions as $info) {
if (isset($info['type'])) {
// The parameter types are in the form 'entity:$entity_type'.
list(, $parameter_entity_type) = explode(':', $info['type'], 2);
if ($parameter_entity_type == $entity_type) {
return;
}
}
}
if (!isset($parameter_definitions[$entity_type])) {
$parameter_definitions[$entity_type] = array();
}
$parameter_definitions[$entity_type] += array(
'type' => 'entity:' . $entity_type,
);
if (!empty($parameter_definitions)) {
$route->setOption('parameters', $parameter_definitions);
}
}
}
/**
* Set the upcasting route objects.
*
* @param \Symfony\Component\Routing\Route $route
* The route object to add the upcasting information onto.
*/
public function setRouteOptions(Route $route) {
if ($controller = $this->getController($route->getDefaults())) {
// Try to use reflection.
if ($this->setParametersFromReflection($controller, $route)) {
return;
}
}
// Try to use _entity_view, _entity_list information on the route.
$this->setParametersFromEntityInformation($route);
}
/**
* Returns a list of all entity types.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
*/
protected function getEntityTypes() {
if (!isset($this->entityTypes)) {
$this->entityTypes = $this->entityManager->getDefinitions();
}
return $this->entityTypes;
}
}
......@@ -7,7 +7,7 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityResolverManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Routing\RouteBuildEvent;
......@@ -26,20 +26,20 @@
class EntityRouteAlterSubscriber implements EventSubscriberInterface {
/**
* Entity manager.
* The entity resolver manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
* @var \Drupal\Core\Entity\EntityResolverManager
*/
protected $entityManager;
protected $resolverManager;
/**
* Constructs a new EntityRouteAlterSubscriber.
* Constructs an EntityRouteAlterSubscriber instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Entity\EntityResolverManager
* The entity resolver manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
public function __construct(EntityResolverManager $entity_resolver_manager) {
$this->resolverManager = $entity_resolver_manager;
}
/**
......@@ -49,22 +49,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
* The event to process.
*/
public function onRoutingRouteAlterSetType(RouteBuildEvent $event) {
$entity_types = array_keys($this->entityManager->getDefinitions());
foreach ($event->getRouteCollection() as $route) {
$parameter_definitions = $route->getOption('parameters') ?: array();
// For all route parameter names that match an entity type, add the 'type'
// to the parameter definition if it's not already explicitly provided.
foreach (array_intersect($route->compile()->getVariables(), $entity_types) as $parameter_name) {
if (!isset($parameter_definitions[$parameter_name])) {
$parameter_definitions[$parameter_name] = array();
}
$parameter_definitions[$parameter_name] += array(
'type' => 'entity:' . $parameter_name,
);
}
if (!empty($parameter_definitions)) {
$route->setOption('parameters', $parameter_definitions);
}
$this->resolverManager->setRouteOptions($route);
}
}
......
......@@ -56,8 +56,7 @@ function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $l
$this->cache = $cache;
$this->lock = $lock;
$this->tags = $tags;
$request = \Drupal::request();
$this->persistable = $modules_loaded && $request->isMethod('GET');
$this->persistable = $modules_loaded && \Drupal::hasRequest() && \Drupal::request()->isMethod('GET');
// @todo: Implement lazyload.
$this->cacheLoaded = TRUE;
......
......@@ -21,7 +21,7 @@ class CommentDefaultFormatterCacheTagsTest extends EntityUnitTestBase {
*
* @var array
*/
public static $modules = array('entity_test', 'comment');
public static $modules = array('entity_test', 'comment', 'menu_link');
/**
* {@inheritdoc}
......
......@@ -7,7 +7,7 @@ config_translation.mapper_list:
_permission: 'translate configuration'
config_translation.entity_list:
path: '/admin/config/regional/config-translation/{config_translation_mapper}'
path: '/admin/config/regional/config-translation/{mapper_id}'
defaults:
_content: '\Drupal\config_translation\Controller\ConfigTranslationListController::listing'
requirements:
......
......@@ -15,6 +15,7 @@
use Drupal\locale\LocaleConfigManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Configuration mapper for configuration entities.
......@@ -233,4 +234,18 @@ public function getContextualLinkGroup() {
}
}
/**
* {@inheritdoc}
*/
protected function processRoute(Route $route) {
// Add entity upcasting information.
$parameters = $route->getOption('parameters') ?: array();
$parameters += array(
$this->entityType => array(
'type' => 'entity:' . $this->entityType,
)
);
$route->setOption('parameters', $parameters);
}
}
......@@ -46,6 +46,13 @@ class ConfigNamesMapper extends PluginBase implements ConfigMapperInterface, Con
*/
protected $configMapperManager;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The base route object that the mapper is attached to.
*
......@@ -168,6 +175,15 @@ public function getBaseRoute() {
}
}
/**
* Allows to process all config translation routes.
*
* @param \Symfony\Component\Routing\Route $route
* The route object to process.
*/
protected function processRoute(Route $route) {
}
/**
* {@inheritdoc}
*/
......@@ -193,7 +209,7 @@ public function getOverviewRouteParameters() {
* {@inheritdoc}
*/
public function getOverviewRoute() {
return new Route(
$route = new Route(
$this->getBaseRoute()->getPath() . '/translate',
array(
'_content' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
......@@ -201,6 +217,8 @@ public function getOverviewRoute() {
),
array('_config_translation_overview_access' => 'TRUE')
);
$this->processRoute($route);
return $route;
}
/**
......@@ -232,7 +250,7 @@ public function getAddRouteParameters() {
* {@inheritdoc}
*/
public function getAddRoute() {
return new Route(
$route = new Route(
$this->getBaseRoute()->getPath() . '/translate/{langcode}/add',
array(
'_form' => '\Drupal\config_translation\Form\ConfigTranslationAddForm',
......@@ -240,6 +258,8 @@ public function getAddRoute() {
),
array('_config_translation_form_access' => 'TRUE')
);
$this->processRoute($route);
return $route;
}
/**
......@@ -260,7 +280,7 @@ public function getEditRouteParameters() {
* {@inheritdoc}
*/
public function getEditRoute() {
return new Route(
$route = new Route(
$this->getBaseRoute()->getPath() . '/translate/{langcode}/edit',
array(
'_form' => '\Drupal\config_translation\Form\ConfigTranslationEditForm',
......@@ -268,6 +288,8 @@ public function getEditRoute() {
),
array('_config_translation_form_access' => 'TRUE')
);
$this->processRoute($route);
return $route;
}
/**
......@@ -288,7 +310,7 @@ public function getDeleteRouteParameters() {
* {@inheritdoc}
*/
public function getDeleteRoute() {
return new Route(
$route = new Route(
$this->getBaseRoute()->getPath() . '/translate/{langcode}/delete',
array(
'_form' => '\Drupal\config_translation\Form\ConfigTranslationDeleteForm',
......@@ -296,6 +318,8 @@ public function getDeleteRoute() {
),
array('_config_translation_form_access' => 'TRUE')
);
$this->processRoute($route);
return $route;
}
/**
......
......@@ -18,30 +18,20 @@
class ConfigTranslationListController extends ControllerBase {
/**
* The definition of the config mapper.
* The mapper manager.
*
* @var array
* @var \Drupal\config_translation\ConfigMapperManagerInterface
*/
protected $mapperDefinition;
/**
* The config mapper.
*
* @var \Drupal\config_translation\ConfigEntityMapper
*/
protected $mapper;
protected $mapperManager;
/**
* Constructs a new ConfigTranslationListController object.
*
* @param \Drupal\config_translation\ConfigMapperManagerInterface $mapper_manager
* The config mapper manager.
* @param string $config_translation_mapper
* The config mapper id.
*/
public function __construct(ConfigMapperManagerInterface $mapper_manager, $config_translation_mapper) {
$this->mapperDefinition = $mapper_manager->getDefinition($config_translation_mapper);
$this->mapper = $mapper_manager->createInstance($config_translation_mapper, $this->mapperDefinition);
public function __construct(ConfigMapperManagerInterface $mapper_manager) {
$this->mapperManager = $mapper_manager;
}
/**
......@@ -49,14 +39,16 @@ public function __construct(ConfigMapperManagerInterface $mapper_manager, $confi
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.config_translation.mapper'),
$container->get('request')->attributes->get('_raw_variables')->get('config_translation_mapper')
$container->get('plugin.manager.config_translation.mapper')
);
}
/**
* Provides the listing page for any entity type.
*
* @param string $mapper_id
* The name of the mapper.
*
* @return array
* A render array as expected by drupal_render().
*
......@@ -64,20 +56,22 @@ public static function create(ContainerInterface $container) {
* Throws an exception if a mapper plugin could not be instantiated from the
* mapper definition in the constructor.
*/
public function listing() {
if (!$this->mapper) {
public function listing($mapper_id) {
$mapper_definition = $this->mapperManager->getDefinition($mapper_id);
$mapper = $this->mapperManager->createInstance($mapper_id, $mapper_definition);
if (!$mapper) {
throw new NotFoundHttpException();
}
$entity_type = $this->mapper->getType();
$entity_type = $mapper->getType();
// If the mapper, for example the mapper for field instances, has a custom
// list controller defined, use it. Other mappers, for examples the ones for
// node_type and block, fallback to the generic configuration translation
// list controller.
$build = $this->entityManager()
->getController($entity_type, 'config_translation_list')
->setMapperDefinition($this->mapperDefinition)
->setMapperDefinition($mapper_definition)
->render();
$build['#title'] = $this->mapper->getTypeLabel();
$build['#title'] = $mapper->getTypeLabel();
return $build;
}
......
......@@ -68,6 +68,9 @@ protected function alterRoutes(RouteCollection $collection) {
'entity' => array(
'type' => 'entity:' . $entity_type_id,
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
......@@ -94,6 +97,9 @@ protected function alterRoutes(RouteCollection $collection) {
'entity' => array(
'type' => 'entity:' . $entity_type_id,
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
......@@ -118,6 +124,9 @@ protected function alterRoutes(RouteCollection $collection) {
'entity' => array(
'type' => 'entity:' . $entity_type_id,
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
......@@ -141,6 +150,9 @@ protected function alterRoutes(RouteCollection $collection) {
'entity' => array(
'type' => 'entity:' . $entity_type_id,
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_access_mode' => 'ANY',
'_admin_route' => $is_admin,
......
......@@ -20,7 +20,7 @@ class FieldImportDeleteUninstallTest extends FieldUnitTestBase {
*
* @var array
*/
public static $modules = array('telephone');
public static $modules = array('telephone', 'menu_link');
public static function getInfo() {
return array(
......
......@@ -48,27 +48,37 @@ protected function alterRoutes(RouteCollection $collection) {
}
$path = $entity_route->getPath();
$options = array();
if (($bundle_entity_type = $entity_type->getBundleEntityType()) && $bundle_entity_type !== 'bundle') {
$options['parameters'][$entity_type->getBundleEntityType()] = array(
'type' => 'entity:' . $entity_type->getBundleEntityType(),
);
}
$route = new Route(
"$path/fields/{field_instance_config}",
array(
'_form' => '\Drupal\field_ui\Form\FieldInstanceEditForm',
'_title_callback' => '\Drupal\field_ui\Form\FieldInstanceEditForm::getTitle',
),
array('_entity_access' => 'field_instance_config.update')
array('_entity_access' => 'field_instance_config.update'),
$options
);
$collection->add("field_ui.instance_edit_$entity_type_id", $route);
$route = new Route(
"$path/fields/{field_instance_config}/field",
array('_form' => '\Drupal\field_ui\Form\FieldEditForm'),
array('_entity_access' => 'field_instance_config.update')
array('_entity_access' => 'field_instance_config.update'),
$options
);
$collection->add("field_ui.field_edit_$entity_type_id", $route);
$route = new Route(
"$path/fields/{field_instance_config}/delete",
array('_entity_form' => 'field_instance_config.delete'),
array('_entity_access' => 'field_instance_config.delete')
array('_entity_access' => 'field_instance_config.delete'),
$options
);
$collection->add("field_ui.delete_$entity_type_id", $route);
......@@ -83,7 +93,8 @@ protected function alterRoutes(RouteCollection $collection) {
'_form' => '\Drupal\field_ui\FieldOverview',
'_title' => 'Manage fields',
) + $defaults,
array('_permission' => 'administer ' . $entity_type_id . ' fields')
array('_permission' => 'administer ' . $entity_type_id . ' fields'),
$options
);
$collection->add("field_ui.overview_$entity_type_id", $route);
......@@ -93,7 +104,8 @@ protected function alterRoutes(RouteCollection $collection) {
'_form' => '\Drupal\field_ui\FormDisplayOverview',
'_title' => 'Manage form display',
) + $defaults,
array('_field_ui_form_mode_access' => 'administer ' . $entity_type_id . ' form display')
array('_field_ui_form_mode_access' => 'administer ' . $entity_type_id . ' form display'),
$options
);
$collection->add("field_ui.form_display_overview_$entity_type_id", $route);
......@@ -103,7 +115,8 @@ protected function alterRoutes(RouteCollection $collection) {
'_form' => '\Drupal\field_ui\FormDisplayOverview',
'_title' => 'Manage form display',
) + $defaults,
array('_field_ui_form_mode_access' => 'administer ' . $entity_type_id . ' form display')
array('_field_ui_form_mode_access' => 'administer ' . $entity_type_id . ' form display'),
$options
);
$collection->add("field_ui.form_display_overview_form_mode_$entity_type_id", $route);
......@@ -113,7 +126,8 @@ protected function alterRoutes(RouteCollection $collection) {
'_form' => '\Drupal\field_ui\DisplayOverview',
'_title' => 'Manage display',
) + $defaults,
array('_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display')
array('_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'),
$options
);
$collection->add("field_ui.display_overview_$entity_type_id", $route);
......@@ -123,7 +137,8 @@ protected function alterRoutes(RouteCollection $collection) {
'_form' => '\Drupal\field_ui\DisplayOverview',
'_title' => 'Manage display',
) + $defaults,
array('_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display')
array('_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'),
$options
);
$collection->add("field_ui.display_overview_view_mode_$entity_type_id", $route);
}
......
......@@ -33,7 +33,7 @@ abstract class NormalizerTestBase extends DrupalUnitTestBase {
*
* @var array
*/
public static $modules = array('entity', 'entity_test', 'entity_reference', 'field', 'hal', 'language', 'rest', 'serialization', 'system', 'text', 'user', 'filter');
public static $modules = array('entity', 'entity_test', 'entity_reference', 'field', 'hal', 'language', 'rest', 'serialization', 'system', 'text', 'user', 'filter', 'menu_link');
/**
* The mock serializer.
......
......@@ -150,12 +150,12 @@ function testContentTypeDirLang() {
$edit = array();
$edit['predefined_langcode'] = 'ar';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
\Drupal::languageManager()->reset();
// Install Spanish language.
$edit = array();
$edit['predefined_langcode'] = 'es';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
\Drupal::languageManager()->reset();
// Set the content type to use multilingual support.
$this->drupalGet("admin/structure/types/manage/{$type->type}");
......
......@@ -17,7 +17,7 @@ class LocaleLocaleLookupTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('locale');
public static $modules = array('locale', 'menu_link');
/**
* {@inheritdoc}
......