EntityResolverManager.php 7.67 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
<?php

namespace Drupal\Core\Entity;

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 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\DependencyInjection\ClassResolverInterface $class_resolver
   *   The class resolver.
   */
38
  public function __construct(EntityManagerInterface $entity_manager, ClassResolverInterface $class_resolver) {
39 40 41 42 43
    $this->entityManager = $entity_manager;
    $this->classResolver = $class_resolver;
  }

  /**
44
   * Gets the controller class using route defaults.
45 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.
48
   *
49 50 51 52 53 54 55
   * Rather than creating an instance of every controller determine the class
   * and method that would be used. This is not possible for the service:method
   * notation as the runtime container does not allow static introspection.
   *
   * @see \Drupal\Core\Controller\ControllerResolver::getControllerFromDefinition()
   * @see \Drupal\Core\Controller\ClassResolver::getInstanceFromDefinition()
   *
56 57 58
   * @param array $defaults
   *   The default values provided by the route.
   *
59 60
   * @return string|null
   *   Returns the controller class, otherwise NULL.
61
   */
62
  protected function getControllerClass(array $defaults) {
63 64
    $controller = NULL;
    if (isset($defaults['_controller'])) {
65
      $controller = $defaults['_controller'];
66 67 68
    }

    if (isset($defaults['_form'])) {
69 70 71 72
      $controller = $defaults['_form'];
      // Check if the class exists and if so use the buildForm() method from the
      // interface.
      if (class_exists($controller)) {
73
        return [$controller, 'buildForm'];
74 75 76
      }
    }

77 78
    if (strpos($controller, ':') === FALSE) {
      if (method_exists($controller, '__invoke')) {
79
        return [$controller, '__invoke'];
80 81 82 83 84 85 86 87 88 89 90 91 92 93
      }
      if (function_exists($controller)) {
        return $controller;
      }
      return NULL;
    }

    $count = substr_count($controller, ':');
    if ($count == 1) {
      // Controller in the service:method notation. Get the information from the
      // service. This is dangerous as the controller could depend on services
      // that could not exist at this point. There is however no other way to
      // do it, as the container does not allow static introspection.
      list($class_or_service, $method) = explode(':', $controller, 2);
94
      return [$this->classResolver->getInstanceFromDefinition($class_or_service), $method];
95 96 97 98 99 100 101
    }
    elseif (strpos($controller, '::') !== FALSE) {
      // Controller in the class::method notation.
      return explode('::', $controller, 2);
    }

    return NULL;
102 103 104 105 106
  }

  /**
   * Sets the upcasting information using reflection.
   *
107 108
   * @param string|array $controller
   *   A PHP callable representing the controller.
109 110 111 112 113 114
   * @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.
   */
115
  protected function setParametersFromReflection($controller, Route $route) {
116
    $entity_types = $this->getEntityTypes();
117
    $parameter_definitions = $route->getOption('parameters') ?: [];
118 119

    $result = FALSE;
120 121 122 123 124 125 126 127 128

    if (is_array($controller)) {
      list($instance, $method) = $controller;
      $reflection = new \ReflectionMethod($instance, $method);
    }
    else {
      $reflection = new \ReflectionFunction($controller);
    }

129 130 131 132 133
    $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
134
      // the user has specified some interface, so the upcasting is intended.
135 136 137 138
      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)) {
139 140
          $parameter_definitions += [$parameter_name => []];
          $parameter_definitions[$parameter_name] += [
141
            'type' => 'entity:' . $parameter_name,
142
          ];
143 144 145 146 147 148 149 150 151 152 153 154 155
          $result = TRUE;
        }
      }
    }
    if (!empty($parameter_definitions)) {
      $route->setOption('parameters', $parameter_definitions);
    }
    return $result;
  }

  /**
   * Sets the upcasting information using the _entity_* route defaults.
   *
156
   * Supports the '_entity_view' and '_entity_form' route defaults.
157 158 159 160 161 162 163 164 165 166 167 168
   *
   * @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_form = $route->getDefault('_entity_form')) {
      list($entity_type) = explode('.', $entity_form, 2);
    }

catch's avatar
catch committed
169 170 171 172
    // Do not add parameter information if the route does not declare a
    // parameter in the first place. This is the case for add forms, for
    // example.
    if (isset($entity_type) && isset($this->getEntityTypes()[$entity_type]) && (strpos($route->getPath(), '{' . $entity_type . '}') !== FALSE)) {
173
      $parameter_definitions = $route->getOption('parameters') ?: [];
174 175 176 177

      // First try to figure out whether there is already a parameter upcasting
      // the same entity type already.
      foreach ($parameter_definitions as $info) {
178
        if (isset($info['type']) && (strpos($info['type'], 'entity:') === 0)) {
179 180 181 182 183 184 185 186 187
          // 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])) {
188
        $parameter_definitions[$entity_type] = [];
189
      }
190
      $parameter_definitions[$entity_type] += [
191
        'type' => 'entity:' . $entity_type,
192
      ];
193 194 195 196 197 198 199 200 201 202 203 204 205
      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) {
206
    if ($controller = $this->getControllerClass($route->getDefaults())) {
207 208 209 210 211 212
      // Try to use reflection.
      if ($this->setParametersFromReflection($controller, $route)) {
        return;
      }
    }

213
    // Try to use _entity_* information on the route.
214 215 216 217
    $this->setParametersFromEntityInformation($route);
  }

  /**
218
   * Gets the list of all entity types.
219 220 221 222 223 224 225 226 227 228 229
   *
   * @return \Drupal\Core\Entity\EntityTypeInterface[]
   */
  protected function getEntityTypes() {
    if (!isset($this->entityTypes)) {
      $this->entityTypes = $this->entityManager->getDefinitions();
    }
    return $this->entityTypes;
  }

}