Skip to content

Commit

Permalink
fix: the stateOptions::entityClass should be used when present while …
Browse files Browse the repository at this point in the history
…building Links (#5550)

* fix: the stateOptions::entityClass should be used when present while building Links
Fixes #5510

* proper fix

---------

Co-authored-by: Manuel Rossard <manuel.rossard@u-bordeaux.fr>
Co-authored-by: soyuka <soyuka@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 21, 2023
1 parent 60082d7 commit f393574
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 18 deletions.
12 changes: 12 additions & 0 deletions features/main/crud_uri_variables.feature
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,15 @@ Feature: Uri Variables
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/companies/1/employees/2"
Then the response status code should be 404

@!mongodb
Scenario: Get all EntityClassAndCustomProviderResources
Given there are 1 separated entities
When I send a "GET" request to "/entityClassAndCustomProviderResources"
Then the response status code should be 200

@!mongodb
Scenario: Get one EntityClassAndCustomProviderResource
Given there are 1 separated entities
When I send a "GET" request to "/entityClassAndCustomProviderResources/1"
Then the response status code should be 200
2 changes: 1 addition & 1 deletion src/Api/IdentifiersExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private function getIdentifiersFromOperation(object $item, Operation $operation,
}

$parameterName = $link->getParameterName();
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty());
$identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty());
}

return $identifiers;
Expand Down
39 changes: 35 additions & 4 deletions src/Doctrine/Orm/State/LinksHandlerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Doctrine\Common\State\LinksHandlerTrait as CommonLinksHandlerTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;
Expand Down Expand Up @@ -49,12 +50,17 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
continue;
}

$fromClassMetadata = $manager->getClassMetadata($link->getFromClass());
$fromClass = $link->getFromClass();
if (!$this->managerRegistry->getManagerForClass($fromClass)) {
$fromClass = $this->getLinkFromClass($link, $operation);
}

$fromClassMetadata = $manager->getClassMetadata($fromClass);
$identifierProperties = $link->getIdentifiers();
$hasCompositeIdentifiers = 1 < \count($identifierProperties);

if (!$link->getFromProperty() && !$link->getToProperty()) {
$currentAlias = $link->getFromClass() === $entityClass ? $alias : $queryNameGenerator->generateJoinAlias($alias);
$currentAlias = $fromClass === $entityClass ? $alias : $queryNameGenerator->generateJoinAlias($alias);

foreach ($identifierProperties as $identifierProperty) {
$placeholder = $queryNameGenerator->generateParameterName($identifierProperty);
Expand Down Expand Up @@ -85,7 +91,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que

$property = $associationMapping['mappedBy'] ?? $joinProperties[0];
$select = isset($associationMapping['mappedBy']) ? "IDENTITY($joinAlias.$property)" : "$joinAlias.$property";
$expressions["$previousAlias.{$property}"] = "SELECT $select FROM {$link->getFromClass()} $nextAlias INNER JOIN $nextAlias.{$associationMapping['fieldName']} $joinAlias WHERE ".implode(' AND ', $whereClause);
$expressions["$previousAlias.{$property}"] = "SELECT $select FROM {$fromClass} $nextAlias INNER JOIN $nextAlias.{$associationMapping['fieldName']} $joinAlias WHERE ".implode(' AND ', $whereClause);
$previousAlias = $nextAlias;
continue;
}
Expand All @@ -95,7 +101,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
$queryBuilder->innerJoin("$previousAlias.".$associationMapping['mappedBy'], $joinAlias);
} else {
$queryBuilder->join(
$link->getFromClass(),
$fromClass,
$joinAlias,
'WITH',
"$previousAlias.{$previousJoinProperties[0]} = $joinAlias.{$associationMapping['fieldName']}"
Expand Down Expand Up @@ -143,4 +149,29 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que
$queryBuilder->andWhere($clause.str_repeat(')', $i));
}
}

private function getLinkFromClass(Link $link, Operation $operation): string
{
$fromClass = $link->getFromClass();
if ($fromClass === $operation->getClass() && $entityClass = $this->getStateOptionsEntityClass($operation)) {
return $entityClass;
}

$operation = $this->resourceMetadataCollectionFactory->create($fromClass)->getOperation();

if ($entityClass = $this->getStateOptionsEntityClass($operation)) {
return $entityClass;
}

throw new \Exception('Can not found a doctrine class for this link.');
}

private function getStateOptionsEntityClass(Operation $operation): ?string
{
if (($options = $operation->getStateOptions()) && $options instanceof Options && $entityClass = $options->getEntityClass()) {
return $entityClass;
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

namespace ApiPlatform\Metadata\Resource\Factory;

use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\HttpOperation;
Expand Down Expand Up @@ -182,14 +181,9 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap
continue;
}

$entityClass = $operation->getClass();
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$entityClass = $options->getEntityClass();
}

$newUriVariables[$variable] = (new Link())
->withFromClass($entityClass)
->withIdentifiers([property_exists($entityClass, $variable) ? $variable : 'id'])
->withFromClass($operation->getClass())
->withIdentifiers([property_exists($operation->getClass(), $variable) ? $variable : 'id'])
->withParameterName($variable);
}

Expand Down
3 changes: 3 additions & 0 deletions tests/Doctrine/Orm/State/ItemProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ public function testGetSubresourceFromProperty(): void
Employee::class => $employeeClassMetadataProphecy->reveal(),
]);

$managerProphecy = $this->prophesize(EntityManagerInterface::class);
$managerRegistryProphecy->getManagerForClass(Employee::class)->willReturn($managerProphecy->reveal());

/** @var HttpOperation */
$operation = (new Get())->withUriVariables([
'employeeId' => (new Link())->withFromClass(Employee::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;

use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity;
use ApiPlatform\Tests\Fixtures\TestBundle\State\EntityClassAndCustomProviderResourceProvider;

#[ApiResource(
operations: [
new Get(
uriTemplate: '/entityClassAndCustomProviderResources/{id}',
uriVariables: ['id']
),
new GetCollection(
uriTemplate: '/entityClassAndCustomProviderResources'
),
],
provider: EntityClassAndCustomProviderResourceProvider::class,
stateOptions: new Options(entityClass: SeparatedEntity::class)
)]
class EntityClassAndCustomProviderResource
{
#[ApiProperty(identifier: true)]
public ?int $id;

public ?string $value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\State;

use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EntityClassAndCustomProviderResource;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity;

class EntityClassAndCustomProviderResourceProvider implements ProviderInterface
{
/**
* Should probably be ProviderInterface for both with a binding in services.yaml in a real app.
*/
public function __construct(
private readonly ItemProvider $itemProvider,
private readonly CollectionProvider $collectionProvider
) {
}

/**
* @return EntityClassAndCustomProviderResource[]|EntityClassAndCustomProviderResource|null
*/
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$operation = ($stateOptions = $operation->getStateOptions()) instanceof Options ? $operation->withClass($stateOptions->getEntityClass()) : $operation;
if ($operation instanceof CollectionOperationInterface) {
$data = $this->collectionProvider->provide(
$operation,
$uriVariables,
$context);

$processed = [];

foreach ($data as $item) {
$processed[] = $this->transform($item);
}

return $processed;
}

$data = $this->itemProvider->provide(
$operation,
$uriVariables,
$context
);

if (null === $data) {
throw new ItemNotFoundException();
}

return $this->transform($data);
}

/**
* Would do more in a real app...
*/
private function transform(SeparatedEntity $data): EntityClassAndCustomProviderResource
{
$resource = new EntityClassAndCustomProviderResource();
$resource->id = $data->id;
$resource->value = $data->value;

return $resource;
}
}
17 changes: 12 additions & 5 deletions tests/Fixtures/app/config/config_doctrine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,27 @@ services:
tags:
- name: 'api_platform.state_provider'

ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProvider:
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProvider:
arguments:
$decorated: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
tags:
- name: 'api_platform.state_provider'

ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoNoInputsProvider:
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoNoInputsProvider:
arguments:
$itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
$collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'
tags:
- name: 'api_platform.state_provider'

ApiPlatform\Tests\Fixtures\TestBundle\State\CustomOutputDtoProvider:
ApiPlatform\Tests\Fixtures\TestBundle\State\CustomOutputDtoProvider:
arguments:
$itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
$collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'
tags:
- name: 'api_platform.state_provider'

ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProcessor:
ApiPlatform\Tests\Fixtures\TestBundle\State\DummyDtoInputOutputProcessor:
arguments:
$registry: '@doctrine'
tags:
Expand All @@ -107,9 +107,16 @@ services:
tags:
- name: 'api_platform.state_processor'

ApiPlatform\Tests\Fixtures\TestBundle\State\CustomInputDtoProcessor:
ApiPlatform\Tests\Fixtures\TestBundle\State\CustomInputDtoProcessor:
arguments:
$decorated: '@ApiPlatform\Doctrine\Common\State\PersistProcessor'
tags:
- name: 'api_platform.state_processor'

ApiPlatform\Tests\Fixtures\TestBundle\State\EntityClassAndCustomProviderResourceProvider:
class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\EntityClassAndCustomProviderResourceProvider'
tags:
- { name: 'api_platform.state_provider' }
arguments:
$itemProvider: '@ApiPlatform\Doctrine\Orm\State\ItemProvider'
$collectionProvider: '@ApiPlatform\Doctrine\Orm\State\CollectionProvider'

0 comments on commit f393574

Please sign in to comment.