3 namespace Drupal\Core\Entity;
5 use Drupal\Core\DependencyInjection\ClassResolverInterface;
6 use Symfony\Component\Routing\Route;
9 * Sets the entity route parameter converter options automatically.
11 * If controllers of routes with route parameters, type-hint the parameters with
12 * an entity interface, upcasting is done automatically.
14 class EntityResolverManager {
19 * @var \Drupal\Core\Entity\EntityManagerInterface
21 protected $entityManager;
26 * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
28 protected $classResolver;
31 * Constructs a new EntityRouteAlterSubscriber.
33 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
35 * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
38 public function __construct(EntityManagerInterface $entity_manager, ClassResolverInterface $class_resolver) {
39 $this->entityManager = $entity_manager;
40 $this->classResolver = $class_resolver;
44 * Gets the controller class using route defaults.
46 * By design we cannot support all possible routes, but just the ones which
47 * use the defaults provided by core, which are _controller and _form.
49 * Rather than creating an instance of every controller determine the class
50 * and method that would be used. This is not possible for the service:method
51 * notation as the runtime container does not allow static introspection.
53 * @see \Drupal\Core\Controller\ControllerResolver::getControllerFromDefinition()
54 * @see \Drupal\Core\Controller\ClassResolver::getInstanceFromDefinition()
56 * @param array $defaults
57 * The default values provided by the route.
60 * Returns the controller class, otherwise NULL.
62 protected function getControllerClass(array $defaults) {
64 if (isset($defaults['_controller'])) {
65 $controller = $defaults['_controller'];
68 if (isset($defaults['_form'])) {
69 $controller = $defaults['_form'];
70 // Check if the class exists and if so use the buildForm() method from the
72 if (class_exists($controller)) {
73 return [$controller, 'buildForm'];
77 if (strpos($controller, ':') === FALSE) {
78 if (method_exists($controller, '__invoke')) {
79 return [$controller, '__invoke'];
81 if (function_exists($controller)) {
87 $count = substr_count($controller, ':');
89 // Controller in the service:method notation. Get the information from the
90 // service. This is dangerous as the controller could depend on services
91 // that could not exist at this point. There is however no other way to
92 // do it, as the container does not allow static introspection.
93 list($class_or_service, $method) = explode(':', $controller, 2);
94 return [$this->classResolver->getInstanceFromDefinition($class_or_service), $method];
96 elseif (strpos($controller, '::') !== FALSE) {
97 // Controller in the class::method notation.
98 return explode('::', $controller, 2);
105 * Sets the upcasting information using reflection.
107 * @param string|array $controller
108 * A PHP callable representing the controller.
109 * @param \Symfony\Component\Routing\Route $route
110 * The route object to populate without upcasting information.
113 * Returns TRUE if the upcasting parameters could be set, FALSE otherwise.
115 protected function setParametersFromReflection($controller, Route $route) {
116 $entity_types = $this->getEntityTypes();
117 $parameter_definitions = $route->getOption('parameters') ?: [];
121 if (is_array($controller)) {
122 list($instance, $method) = $controller;
123 $reflection = new \ReflectionMethod($instance, $method);
126 $reflection = new \ReflectionFunction($controller);
129 $parameters = $reflection->getParameters();
130 foreach ($parameters as $parameter) {
131 $parameter_name = $parameter->getName();
132 // If the parameter name matches with an entity type try to set the
133 // upcasting information automatically. Therefore take into account that
134 // the user has specified some interface, so the upcasting is intended.
135 if (isset($entity_types[$parameter_name])) {
136 $entity_type = $entity_types[$parameter_name];
137 $entity_class = $entity_type->getClass();
138 if (($reflection_class = $parameter->getClass()) && (is_subclass_of($entity_class, $reflection_class->name) || $entity_class == $reflection_class->name)) {
139 $parameter_definitions += [$parameter_name => []];
140 $parameter_definitions[$parameter_name] += [
141 'type' => 'entity:' . $parameter_name,
147 if (!empty($parameter_definitions)) {
148 $route->setOption('parameters', $parameter_definitions);
154 * Sets the upcasting information using the _entity_* route defaults.
156 * Supports the '_entity_view' and '_entity_form' route defaults.
158 * @param \Symfony\Component\Routing\Route $route
161 protected function setParametersFromEntityInformation(Route $route) {
162 if ($entity_view = $route->getDefault('_entity_view')) {
163 list($entity_type) = explode('.', $entity_view, 2);
165 elseif ($entity_form = $route->getDefault('_entity_form')) {
166 list($entity_type) = explode('.', $entity_form, 2);
169 // Do not add parameter information if the route does not declare a
170 // parameter in the first place. This is the case for add forms, for
172 if (isset($entity_type) && isset($this->getEntityTypes()[$entity_type]) && (strpos($route->getPath(), '{' . $entity_type . '}') !== FALSE)) {
173 $parameter_definitions = $route->getOption('parameters') ?: [];
175 // First try to figure out whether there is already a parameter upcasting
176 // the same entity type already.
177 foreach ($parameter_definitions as $info) {
178 if (isset($info['type']) && (strpos($info['type'], 'entity:') === 0)) {
179 // The parameter types are in the form 'entity:$entity_type'.
180 list(, $parameter_entity_type) = explode(':', $info['type'], 2);
181 if ($parameter_entity_type == $entity_type) {
187 if (!isset($parameter_definitions[$entity_type])) {
188 $parameter_definitions[$entity_type] = [];
190 $parameter_definitions[$entity_type] += [
191 'type' => 'entity:' . $entity_type,
193 if (!empty($parameter_definitions)) {
194 $route->setOption('parameters', $parameter_definitions);
200 * Set the upcasting route objects.
202 * @param \Symfony\Component\Routing\Route $route
203 * The route object to add the upcasting information onto.
205 public function setRouteOptions(Route $route) {
206 if ($controller = $this->getControllerClass($route->getDefaults())) {
207 // Try to use reflection.
208 if ($this->setParametersFromReflection($controller, $route)) {
213 // Try to use _entity_* information on the route.
214 $this->setParametersFromEntityInformation($route);
218 * Gets the list of all entity types.
220 * @return \Drupal\Core\Entity\EntityTypeInterface[]
222 protected function getEntityTypes() {
223 if (!isset($this->entityTypes)) {
224 $this->entityTypes = $this->entityManager->getDefinitions();
226 return $this->entityTypes;