EntityResolverManager.php

Same filename and directory in other branches
  1. 9 core/lib/Drupal/Core/Entity/EntityResolverManager.php
  2. 8.9.x core/lib/Drupal/Core/Entity/EntityResolverManager.php
  3. 10 core/lib/Drupal/Core/Entity/EntityResolverManager.php

Namespace

Drupal\Core\Entity

File

core/lib/Drupal/Core/Entity/EntityResolverManager.php

View source
<?php

namespace Drupal\Core\Entity;

use Drupal\Component\Utility\Reflection;
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 type manager service.
     *
     * @var \Drupal\Core\Entity\EntityTypeManagerInterface
     */
    protected $entityTypeManager;
    
    /**
     * The class resolver.
     *
     * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
     */
    protected $classResolver;
    
    /**
     * The list of all entity types.
     *
     * @var \Drupal\Core\Entity\EntityTypeInterface[]
     */
    protected ?array $entityTypes;
    
    /**
     * Constructs a new EntityRouteAlterSubscriber.
     *
     * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
     *   The entity type manager service.
     * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
     *   The class resolver.
     */
    public function __construct(EntityTypeManagerInterface $entity_type_manager, ClassResolverInterface $class_resolver) {
        $this->entityTypeManager = $entity_type_manager;
        $this->classResolver = $class_resolver;
    }
    
    /**
     * Gets the controller class using route defaults.
     *
     * By design we cannot support all possible routes, but just the ones which
     * use the defaults provided by core, which are _controller and _form.
     *
     * 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()
     *
     * @param array $defaults
     *   The default values provided by the route.
     *
     * @return string|null
     *   Returns the controller class, otherwise NULL.
     */
    protected function getControllerClass(array $defaults) {
        $controller = NULL;
        if (isset($defaults['_controller'])) {
            $controller = $defaults['_controller'];
        }
        if (isset($defaults['_form'])) {
            $controller = $defaults['_form'];
            // Check if the class exists and if so use the buildForm() method from the
            // interface.
            if (class_exists($controller)) {
                return [
                    $controller,
                    'buildForm',
                ];
            }
        }
        if ($controller === NULL) {
            return NULL;
        }
        if (!str_contains($controller, ':')) {
            if (method_exists($controller, '__invoke')) {
                return [
                    $controller,
                    '__invoke',
                ];
            }
            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.
            [
                $class_or_service,
                $method,
            ] = explode(':', $controller, 2);
            return [
                $this->classResolver
                    ->getInstanceFromDefinition($class_or_service),
                $method,
            ];
        }
        elseif (str_contains($controller, '::')) {
            // Controller in the class::method notation.
            return explode('::', $controller, 2);
        }
        return NULL;
    }
    
    /**
     * Sets the upcasting information using reflection.
     *
     * @param string|array $controller
     *   A PHP callable representing the controller.
     * @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($controller, Route $route) {
        $entity_types = $this->getEntityTypes();
        $parameter_definitions = $route->getOption('parameters') ?: [];
        $result = FALSE;
        if (is_array($controller)) {
            [
                $instance,
                $method,
            ] = $controller;
            $reflection = new \ReflectionMethod($instance, $method);
        }
        else {
            $reflection = new \ReflectionFunction($controller);
        }
        $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 upcasting is intended.
            if (isset($entity_types[$parameter_name])) {
                $entity_type = $entity_types[$parameter_name];
                $entity_class = $entity_type->getClass();
                $reflection_class = Reflection::getParameterClassName($parameter);
                if ($reflection_class && (is_subclass_of($entity_class, $reflection_class) || $entity_class == $reflection_class)) {
                    $parameter_definitions += [
                        $parameter_name => [],
                    ];
                    $parameter_definitions[$parameter_name] += [
                        '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.
     *
     * Supports the '_entity_view' 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')) {
            [
                $entity_type,
            ] = explode('.', $entity_view, 2);
        }
        elseif ($entity_form = $route->getDefault('_entity_form')) {
            [
                $entity_type,
            ] = explode('.', $entity_form, 2);
        }
        // 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]) && str_contains($route->getPath(), '{' . $entity_type . '}')) {
            $parameter_definitions = $route->getOption('parameters') ?: [];
            // 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']) && str_starts_with($info['type'], 'entity:')) {
                    // The parameter types are in the form 'entity:$entity_type'.
                    [
                        ,
                        $parameter_entity_type,
                    ] = explode(':', $info['type'], 2);
                    if ($parameter_entity_type == $entity_type) {
                        return;
                    }
                }
            }
            if (!isset($parameter_definitions[$entity_type])) {
                $parameter_definitions[$entity_type] = [];
            }
            $parameter_definitions[$entity_type] += [
                '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->getControllerClass($route->getDefaults())) {
            // Try to use reflection.
            if ($this->setParametersFromReflection($controller, $route)) {
                return;
            }
        }
        // Try to use _entity_* information on the route.
        $this->setParametersFromEntityInformation($route);
    }
    
    /**
     * Gets the list of all entity types.
     *
     * @return \Drupal\Core\Entity\EntityTypeInterface[]
     */
    protected function getEntityTypes() {
        if (!isset($this->entityTypes)) {
            $this->entityTypes = $this->entityTypeManager
                ->getDefinitions();
        }
        return $this->entityTypes;
    }

}

Classes

Title Deprecated Summary
EntityResolverManager Sets the entity route parameter converter options automatically.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.