EntityResolverManager.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <?php
  2. namespace Drupal\Core\Entity;
  3. use Drupal\Core\DependencyInjection\ClassResolverInterface;
  4. use Symfony\Component\Routing\Route;
  5. /**
  6. * Sets the entity route parameter converter options automatically.
  7. *
  8. * If controllers of routes with route parameters, type-hint the parameters with
  9. * an entity interface, upcasting is done automatically.
  10. */
  11. class EntityResolverManager {
  12. /**
  13. * The entity manager.
  14. *
  15. * @var \Drupal\Core\Entity\EntityManagerInterface
  16. */
  17. protected $entityManager;
  18. /**
  19. * The class resolver.
  20. *
  21. * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
  22. */
  23. protected $classResolver;
  24. /**
  25. * Constructs a new EntityRouteAlterSubscriber.
  26. *
  27. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
  28. * The entity manager.
  29. * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
  30. * The class resolver.
  31. */
  32. public function __construct(EntityManagerInterface $entity_manager, ClassResolverInterface $class_resolver) {
  33. $this->entityManager = $entity_manager;
  34. $this->classResolver = $class_resolver;
  35. }
  36. /**
  37. * Gets the controller class using route defaults.
  38. *
  39. * By design we cannot support all possible routes, but just the ones which
  40. * use the defaults provided by core, which are _controller and _form.
  41. *
  42. * Rather than creating an instance of every controller determine the class
  43. * and method that would be used. This is not possible for the service:method
  44. * notation as the runtime container does not allow static introspection.
  45. *
  46. * @see \Drupal\Core\Controller\ControllerResolver::getControllerFromDefinition()
  47. * @see \Drupal\Core\Controller\ClassResolver::getInstanceFromDefinition()
  48. *
  49. * @param array $defaults
  50. * The default values provided by the route.
  51. *
  52. * @return string|null
  53. * Returns the controller class, otherwise NULL.
  54. */
  55. protected function getControllerClass(array $defaults) {
  56. $controller = NULL;
  57. if (isset($defaults['_controller'])) {
  58. $controller = $defaults['_controller'];
  59. }
  60. if (isset($defaults['_form'])) {
  61. $controller = $defaults['_form'];
  62. // Check if the class exists and if so use the buildForm() method from the
  63. // interface.
  64. if (class_exists($controller)) {
  65. return [$controller, 'buildForm'];
  66. }
  67. }
  68. if (strpos($controller, ':') === FALSE) {
  69. if (method_exists($controller, '__invoke')) {
  70. return [$controller, '__invoke'];
  71. }
  72. if (function_exists($controller)) {
  73. return $controller;
  74. }
  75. return NULL;
  76. }
  77. $count = substr_count($controller, ':');
  78. if ($count == 1) {
  79. // Controller in the service:method notation. Get the information from the
  80. // service. This is dangerous as the controller could depend on services
  81. // that could not exist at this point. There is however no other way to
  82. // do it, as the container does not allow static introspection.
  83. list($class_or_service, $method) = explode(':', $controller, 2);
  84. return [$this->classResolver->getInstanceFromDefinition($class_or_service), $method];
  85. }
  86. elseif (strpos($controller, '::') !== FALSE) {
  87. // Controller in the class::method notation.
  88. return explode('::', $controller, 2);
  89. }
  90. return NULL;
  91. }
  92. /**
  93. * Sets the upcasting information using reflection.
  94. *
  95. * @param string|array $controller
  96. * A PHP callable representing the controller.
  97. * @param \Symfony\Component\Routing\Route $route
  98. * The route object to populate without upcasting information.
  99. *
  100. * @return bool
  101. * Returns TRUE if the upcasting parameters could be set, FALSE otherwise.
  102. */
  103. protected function setParametersFromReflection($controller, Route $route) {
  104. $entity_types = $this->getEntityTypes();
  105. $parameter_definitions = $route->getOption('parameters') ?: [];
  106. $result = FALSE;
  107. if (is_array($controller)) {
  108. list($instance, $method) = $controller;
  109. $reflection = new \ReflectionMethod($instance, $method);
  110. }
  111. else {
  112. $reflection = new \ReflectionFunction($controller);
  113. }
  114. $parameters = $reflection->getParameters();
  115. foreach ($parameters as $parameter) {
  116. $parameter_name = $parameter->getName();
  117. // If the parameter name matches with an entity type try to set the
  118. // upcasting information automatically. Therefore take into account that
  119. // the user has specified some interface, so the upcasting is intended.
  120. if (isset($entity_types[$parameter_name])) {
  121. $entity_type = $entity_types[$parameter_name];
  122. $entity_class = $entity_type->getClass();
  123. if (($reflection_class = $parameter->getClass()) && (is_subclass_of($entity_class, $reflection_class->name) || $entity_class == $reflection_class->name)) {
  124. $parameter_definitions += [$parameter_name => []];
  125. $parameter_definitions[$parameter_name] += [
  126. 'type' => 'entity:' . $parameter_name,
  127. ];
  128. $result = TRUE;
  129. }
  130. }
  131. }
  132. if (!empty($parameter_definitions)) {
  133. $route->setOption('parameters', $parameter_definitions);
  134. }
  135. return $result;
  136. }
  137. /**
  138. * Sets the upcasting information using the _entity_* route defaults.
  139. *
  140. * Supports the '_entity_view' and '_entity_form' route defaults.
  141. *
  142. * @param \Symfony\Component\Routing\Route $route
  143. * The route object.
  144. */
  145. protected function setParametersFromEntityInformation(Route $route) {
  146. if ($entity_view = $route->getDefault('_entity_view')) {
  147. list($entity_type) = explode('.', $entity_view, 2);
  148. }
  149. elseif ($entity_form = $route->getDefault('_entity_form')) {
  150. list($entity_type) = explode('.', $entity_form, 2);
  151. }
  152. // Do not add parameter information if the route does not declare a
  153. // parameter in the first place. This is the case for add forms, for
  154. // example.
  155. if (isset($entity_type) && isset($this->getEntityTypes()[$entity_type]) && (strpos($route->getPath(), '{' . $entity_type . '}') !== FALSE)) {
  156. $parameter_definitions = $route->getOption('parameters') ?: [];
  157. // First try to figure out whether there is already a parameter upcasting
  158. // the same entity type already.
  159. foreach ($parameter_definitions as $info) {
  160. if (isset($info['type']) && (strpos($info['type'], 'entity:') === 0)) {
  161. // The parameter types are in the form 'entity:$entity_type'.
  162. list(, $parameter_entity_type) = explode(':', $info['type'], 2);
  163. if ($parameter_entity_type == $entity_type) {
  164. return;
  165. }
  166. }
  167. }
  168. if (!isset($parameter_definitions[$entity_type])) {
  169. $parameter_definitions[$entity_type] = [];
  170. }
  171. $parameter_definitions[$entity_type] += [
  172. 'type' => 'entity:' . $entity_type,
  173. ];
  174. if (!empty($parameter_definitions)) {
  175. $route->setOption('parameters', $parameter_definitions);
  176. }
  177. }
  178. }
  179. /**
  180. * Set the upcasting route objects.
  181. *
  182. * @param \Symfony\Component\Routing\Route $route
  183. * The route object to add the upcasting information onto.
  184. */
  185. public function setRouteOptions(Route $route) {
  186. if ($controller = $this->getControllerClass($route->getDefaults())) {
  187. // Try to use reflection.
  188. if ($this->setParametersFromReflection($controller, $route)) {
  189. return;
  190. }
  191. }
  192. // Try to use _entity_* information on the route.
  193. $this->setParametersFromEntityInformation($route);
  194. }
  195. /**
  196. * Gets the list of all entity types.
  197. *
  198. * @return \Drupal\Core\Entity\EntityTypeInterface[]
  199. */
  200. protected function getEntityTypes() {
  201. if (!isset($this->entityTypes)) {
  202. $this->entityTypes = $this->entityManager->getDefinitions();
  203. }
  204. return $this->entityTypes;
  205. }
  206. }