8000 Add an Entity Argument Resolver · fancyweb/symfony@4524083 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4524083

Browse files
jderussefabpot
authored andcommitted
Add an Entity Argument Resolver
1 parent 397abb6 commit 4524083

File tree

3 files changed

+939
-0
lines changed
  • src/Symfony/Bridge/Doctrine
    • ArgumentResolver
      • < 10000 div class="PRIVATE_VisuallyHidden prc-TreeView-TreeViewVisuallyHidden-4-mPv" aria-hidden="true" id=":RmlntddabH1:">
        EntityValueResolver.php
  • Attribute
  • Tests/ArgumentResolver
  • 3 files changed

    +939
    -0
    lines changed
    Lines changed: 293 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,293 @@
    1+
    <?php
    2+
    3+
    /*
    4+
    * This file is part of the Symfony package.
    5+
    *
    6+
    * (c) Fabien Potencier <fabien@symfony.com>
    7+
    *
    8+
    * For the full copyright and license information, please view the LICENSE
    9+
    * file that was distributed with this source code.
    10+
    */
    11+
    12+
    namespace Symfony\Bridge\Doctrine\ArgumentResolver;
    13+
    14+
    use Doctrine\DBAL\Types\ConversionException;
    15+
    use Doctrine\ORM\EntityManagerInterface;
    16+
    use Doctrine\ORM\NoResultException;
    17+
    use Doctrine\Persistence\ManagerRegistry;
    18+
    use Doctrine\Persistence\ObjectManager;
    19+
    use Symfony\Bridge\Doctrine\Attribute\MapEntity;
    20+
    use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
    21+
    use Symfony\Component\HttpFoundation\Request;
    22+
    use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
    23+
    use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
    24+
    use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
    25+
    26+
    /**
    27+
    * Yields the entity matching the criteria provided in the route.
    28+
    *
    29+
    * @author Fabien Potencier <fabien@symfony.com>
    30+
    * @author Jérémy Derussé <jeremy@derusse.com>
    31+
    */
    32+
    final class EntityValueResolver implements ArgumentValueResolverInterface
    33+
    {
    34+
    private array $defaultOptions = [
    35+
    'object_manager' => null,
    36+
    'expr' => null,
    37+
    'mapping' => [],
    38+
    'exclude' => [],
    39+
    'strip_null' => false,
    40+
    'id' => null,
    41+
    'evict_cache' => false,
    42+
    'auto_mapping' => true,
    43+
    'attribute_only' => false,
    44+
    ];
    45+
    46+
    public function __construct(
    47+
    private ManagerRegistry $registry,
    48+
    private ?ExpressionLanguage $language = null,
    49+
    array $defaultOptions = [],
    50+
    ) {
    51+
    $this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions);
    52+
    }
    53+
    54+
    /**
    55+
    * {@inheritdoc}
    56+
    */
    57+
    public function supports(Request $request, ArgumentMetadata $argument): bool
    58+
    {
    59+
    if (!$this->registry->getManagerNames()) {
    60+
    return false;
    61+
    }
    62+
    63+
    $options = $this->getOptions($argument);
    64+
    if (null === $options['class']) {
    65+
    return false;
    66+
    }
    67+
    68+
    if ($options['attribute_only'] && !$options['has_attribute']) {
    69+
    return false;
    70+
    }
    71+
    72+
    // Doctrine Entity?
    73+
    if (null === $objectManager = $this->getManager($options['object_manager'], $options['class'])) {
    74+
    return false;
    75+
    }
    76+
    77+
    return !$objectManager->getMetadataFactory()->isTransient($options['class']);
    78+
    }
    79+
    80+
    /**
    81+
    * {@inheritdoc}
    82+
    */
    83+
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    84+
    {
    85+
    $options = $this->getOptions($argument);
    86+
    87+
    $name = $argument->getName();
    88+
    $class = $options['class'];
    89+
    90+
    $errorMessage = null;
    91+
    if (null !== $options['expr']) {
    92+
    if (null === $object = $this->findViaExpression($class, $request, $options['expr'], $options)) {
    93+
    $errorMessage = sprintf('The expression "%s" returned null', $options['expr']);
    94+
    }
    95+
    // find by identifier?
    96+
    } elseif (false === $object = $this->find($class, $request, $options, $name)) {
    97+
    // find by criteria
    98+
    $object = $this->findOneBy($class, $request, $options);
    99+
    if (false === $object) {
    100+
    if (!$argument->isNullable()) {
    101+
    throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', $name));
    102+
    }
    103+
    104+
    $object = null;
    105+
    }
    106+
    }
    107+
    108+
    if (null === $object && !$argument->isNullable()) {
    109+
    $message = sprintf('"%s" object not found by the "%s" Argument Resolver.', $class, self::class);
    110+
    if ($errorMessage) {
    111+
    $message .= ' '.$errorMessage;
    112+
    }
    113+
    114+
    throw new NotFoundHttpException($message);
    115+
    }
    116+
    117+
    return [$object];
    118+
    }
    119+
    120+
    private function getManager(?string $name, string $class): ?ObjectManager
    121+
    {
    122+
    if (null === $name) {
    123+
    return $this->registry->getManagerForClass($class);
    124+
    }
    125+
    126+
    if (!isset($this->registry->getManagerNames()[$name])) {
    127+
    return null;
    128+
    }
    129+
    130+
    try {
    131+
    return $this->registry->getManager($name);
    132+
    } catch (\InvalidArgumentException) {
    133+
    return null;
    134+
    }
    135+
    }
    136+
    137+
    private function find(string $class, Request $request, array $options, string $name): false|object|null
    138+
    {
    139+
    if ($options['mapping'] || $options['exclude']) {
    140+
    return false;
    141+
    }
    142+
    143+
    $id = $this->getIdentifier($request, $options, $name);
    144+
    if (false === $id || null === $id) {
    145+
    return false;
    146+
    }
    147+
    148+
    $objectManager = $this->getManager($options['object_manager'], $class);
    149+
    if ($options['evict_cache'] && $objectManager instanceof EntityManagerInterface) {
    150+
    $cacheProvider = $objectManager->getCache();
    151+
    if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) {
    152+
    $cacheProvider->evictEntity($class, $id);
    153+
    }
    154+
    }
    155+
    156+
    try {
    157+
    return $objectManager->getRepository($class)->find($id);
    158+
    } catch (NoResultException|ConversionException) {
    159+
    return null;
    160+
    }
    161+
    }
    162+
    163+
    private function getIdentifier(Request $request, array $options, string $name): mixed
    164+
    {
    165+
    if (\is_array($options['id'])) {
    166+
    $id = [];
    167+
    foreach ($options['id'] as $field) {
    168+
    // Convert "%s_uuid" to "foobar_uuid"
    169+
    if (str_contains($field, '%s')) {
    170+
    $field = sprintf($field, $name);
    171+
    }
    172+
    173+
    $id[$field] = $request->attributes->get($field);
    174+
    }
    175+
    176+
    return $id;
    177+
    }
    178+
    179+
    if (null !== $options['id']) {
    180+
    $name = $options['id'];
    181+
    }
    182+
    183+
    if ($request->attributes->has($name)) {
    184+
    return $request->attributes->get($name);
    185+
    }
    186+
    187+
    if (!$options['id'] && $request->attributes->has('id')) {
    188+
    return $request->attributes->get('id');
    189+
    }
    190+
    191+
    return false;
    192+
    }
    193+
    194+
    private function findOneBy(string $class, Request $request, array $options): false|object|null
    195+
    {
    196+
    if (!$options['mapping']) {
    197+
    if (!$options['auto_mapping']) {
    198+
    return false;
    199+
    }
    200+
    201+
    $keys = $request->attributes->keys();
    202+
    $options['mapping'] = $keys ? array_combine($keys, $keys) : [];
    203+
    }
    204+
    205+
    foreach ($options['exclude'] as $exclude) {
    206+
    unset($options['mapping'][$exclude]);
    207+
    }
    208+
    209+
    if (!$options['mapping']) {
    210+
    return false;
    211+
    }
    212+
    213+
    // if a specific id has been defined in the options and there is no corresponding attribute
    214+
    // return false in order to avoid a fallback to the id which might be of another object
    215+
    if ($options['id'] && null === $request->attributes->get($options['id'])) {
    216+
    return false;
    217+
    }
    218+
    219+
    $criteria = [];
    220+
    $objectManager = $this->getManager($options['object_manager'], $class);
    221+
    $metadata = $objectManager->getClassMetadata($class);
    222+
    223+
    foreach ($options['mapping'] as $attribute => $field) {
    224+
    if (!$metadata->hasField($field) && (!$metadata->hasAssociation($field) || !$metadata->isSingleValuedAssociation($field))) {
    225+
    continue;
    226+
    }
    227+
    228+
    $criteria[$field] = $request->attributes->get($attribute);
    229+
    }
    230+
    231+
    if ($options['strip_null']) {
    232+
    $criteria = array_filter($criteria, static fn ($value) => null !== $value);
    233+
    }
    234+
    235+
    if (!$criteria) {
    236+
    return false;
    237+
    }
    238+
    239+
    try {
    240+
    return $objectManager->getRepository($class)->findOneBy($criteria);
    241+
    } catch (NoResultException|ConversionException) {
    242+
    return null;
    243+
    }
    244+
    }
    245+
    246+
    private function findViaExpression(string $class, Request $request, string $expression, array $options): ?object
    247+
    {
    248+
    if (null === $this->language) {
    249+
    throw new \LogicException(sprintf('You cannot use the "%s" if the ExpressionLanguage component is not available. Try running "composer require symfony/expression-language".', __CLASS__));
    250+
    }
    251+
    252+
    $repository = $this->getManager($options['object_manager'], $class)->getRepository($class);
    253+
    $variables = array_merge($request->attributes->all(), ['repository' => $repository]);
    254+
    255+
    try {
    256+
    return $this->language->evaluate($expression, $variables);
    257+
    } catch (NoResultException|ConversionException) {
    258+
    return null;
    259+
    }
    260+
    }
    261+
    262+
    private function getOptions(ArgumentMetadata $argument): array
    263+
    {
    264+
    /** @var ?MapEntity $configuration */
    265+
    $configuration = $argument->getAttributes(MapEntity::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null;
    266+
    267+
    $argumentClass = $argument->getType();
    268+
    if ($argumentClass && !class_exists($argumentClass)) {
    269+
    $argumentClass = null;
    270+
    }
    271+
    272+
    if (null === $configuration) {
    273+
    return array_merge($this->defaultOptions, [
    274+
    'class' => $argumentClass,
    275+
    'has_attribute' => false,
    276+
    ]);
    277+
    }
    278+
    279+
    return [
    280+
    'class' => $configuration->class ?? $argumentClass,
    281+
    'object_manager' => $configuration->objectManager ?? $this->defaultOptions['object_manager'],
    282+
    'expr' => $configuration->expr ?? $this->defaultOptions['expr'],
    283+
    'mapping' => $configuration->mapping ?? $this->defaultOptions['mapping'],
    284+
    'exclude' => $configuration->exclude ?? $this->defaultOptions['exclude'],
    285+
    'strip_null' => $configuration->stripNull ?? $this->defaultOptions['strip_null'],
    286+
    'id' => $configuration->id ?? $this->defaultOptions['id'],
    287+
    'evict_cache' => $configuration->evictCache ?? $this->defaultOptions['evict_cache'],
    288+
    'has_attribute' => true,
    289+
    'auto_mapping' => $this->defaultOptions['auto_mapping'],
    290+
    'attribute_only' => $this->defaultOptions['attribute_only'],
    291+
    ];
    292+
    }
    293+
    }
    Lines changed: 31 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,31 @@
    1+
    <?php
    2+
    3+
    /*
    4+
    * This file is part of the Symfony package.
    5+
    *
    6+
    * (c) Fabien Potencier <fabien@symfony.com>
    7+
    *
    8+
    * For the full copyright and license information, please view the LICENSE
    9+
    * file that was distributed with this source code.
    10+
    */
    11+
    12+
    namespace Symfony\Bridge\Doctrine\Attribute;
    13+
    14+
    /**
    15+
    * Indicates that a controller argument should receive an Entity.
    16+
    */
    17+
    #[\Attribute(\Attribute::TARGET_PARAMETER)]
    18+
    class MapEntity
    19+
    {
    20+
    public function __construct(
    21+
    public readonly ?string $class = null,
    22+
    public readonly ?string $objectManager = null,
    23+
    public readonly ?string $expr = null,
    24+
    public readonly array $mapping = [],
    25+
    public readonly array $exclude = [],
    26+
    public readonly bool $stripNull = false,
    27+
    public readonly array|string|null $id = null,
    28+
    public readonly bool $evictCache = false,
    29+
    ) {
    30+
    }
    31+
    }

    0 commit comments

    Comments
     (0)
    0