mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-05 20:57:12 +00:00
Use ResourceClassResolver in IriConverter to fix discriminator subcla… (#19046)
Some checks failed
Continuous Integration (Minimal) / Static checks (push) Has been cancelled
Continuous Integration (Minimal) / Javascript Tests (MySQL) (push) Has been cancelled
Continuous Integration (Minimal) / Tests (PostgreSQL) (push) Has been cancelled
Continuous Integration (Minimal) / Frontend (push) Has been cancelled
Continuous Integration (Minimal) / Packages (push) Has been cancelled
Continuous Integration (Minimal) / Tests (MariaDB) (push) Has been cancelled
Continuous Integration (Minimal) / Tests (MySQL) (push) Has been cancelled
Some checks failed
Continuous Integration (Minimal) / Static checks (push) Has been cancelled
Continuous Integration (Minimal) / Javascript Tests (MySQL) (push) Has been cancelled
Continuous Integration (Minimal) / Tests (PostgreSQL) (push) Has been cancelled
Continuous Integration (Minimal) / Frontend (push) Has been cancelled
Continuous Integration (Minimal) / Packages (push) Has been cancelled
Continuous Integration (Minimal) / Tests (MariaDB) (push) Has been cancelled
Continuous Integration (Minimal) / Tests (MySQL) (push) Has been cancelled
…ss IRI generation | Q | A |-----------------|----- | Branch? | 2.2 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Related tickets | fixes https://github.com/Sylius/Sylius/issues/18781 | License | MIT <!-- - Bug fixes must be submitted against the 2.2 branch - Features and deprecations must be submitted against the 2.3 branch - Make sure that the correct base branch is set To be sure you are not breaking any Backward Compatibilities, check the documentation: https://docs.sylius.com/en/latest/book/organization/backward-compatibility-promise.html -->
This commit is contained in:
commit
92ef8a2200
12 changed files with 329 additions and 1 deletions
|
|
@ -1,5 +1,19 @@
|
|||
# UPGRADE FROM `2.2.6` TO `2.2.7`
|
||||
|
||||
## Constructor Signature Changes
|
||||
|
||||
1. The constructor of `Sylius\Bundle\ApiBundle\ApiPlatform\Routing\IriConverter` has been extended with an optional `ApiPlatform\Metadata\ResourceClassResolverInterface` argument.
|
||||
|
||||
```php
|
||||
public function __construct(
|
||||
IriConverterInterface $decoratedIriConverter,
|
||||
PathPrefixProviderInterface $pathPrefixProvider,
|
||||
OperationResolverInterface $operationResolver,
|
||||
RouterInterface $router,
|
||||
+ ?ResourceClassResolverInterface $resourceClassResolver = null,
|
||||
)
|
||||
```
|
||||
|
||||
## Bahavior changes
|
||||
|
||||
1. The `LiveComponentTagPass` and `TwigComponentTagPass` in `SyliusUiBundle` were registered with a priority of `500`,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"ApiPlatform\\Metadata\\QueryParameter",
|
||||
"ApiPlatform\\Metadata\\Resource\\Factory\\ResourceMetadataCollectionFactoryInterface",
|
||||
"ApiPlatform\\Metadata\\Resource\\ResourceMetadataCollection",
|
||||
"ApiPlatform\\Metadata\\ResourceClassResolverInterface",
|
||||
"ApiPlatform\\Metadata\\UriVariablesConverterInterface",
|
||||
"ApiPlatform\\Metadata\\UrlGeneratorInterface",
|
||||
"ApiPlatform\\Metadata\\Util\\ClassInfoTrait",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Sylius\Bundle\ApiBundle\ApiPlatform\Routing;
|
|||
|
||||
use ApiPlatform\Metadata\IriConverterInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\Metadata\ResourceClassResolverInterface;
|
||||
use ApiPlatform\Metadata\UrlGeneratorInterface;
|
||||
use ApiPlatform\Metadata\Util\ClassInfoTrait;
|
||||
use Sylius\Bundle\ApiBundle\Provider\PathPrefixProviderInterface;
|
||||
|
|
@ -30,6 +31,7 @@ final readonly class IriConverter implements IriConverterInterface
|
|||
private PathPrefixProviderInterface $pathPrefixProvider,
|
||||
private OperationResolverInterface $operationResolver,
|
||||
private RouterInterface $router,
|
||||
private ?ResourceClassResolverInterface $resourceClassResolver = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +46,16 @@ final readonly class IriConverter implements IriConverterInterface
|
|||
?Operation $operation = null,
|
||||
array $context = [],
|
||||
): ?string {
|
||||
$resourceClass = $context['force_resource_class'] ?? (\is_string($resource) ? $resource : $this->getObjectClass($resource));
|
||||
if (isset($context['force_resource_class'])) {
|
||||
$resourceClass = $context['force_resource_class'];
|
||||
} elseif (\is_string($resource)) {
|
||||
$resourceClass = $resource;
|
||||
} else {
|
||||
$objectClass = $this->getObjectClass($resource);
|
||||
$resourceClass = $this->resourceClassResolver !== null && $this->resourceClassResolver->isResourceClass($objectClass)
|
||||
? $this->resourceClassResolver->getResourceClass($resource)
|
||||
: $objectClass;
|
||||
}
|
||||
$pathPrefix = $this->pathPrefixProvider->getPathPrefix($context['request_uri'] ?? $this->router->getContext()->getPathInfo());
|
||||
$operation = $this->operationResolver->resolve($resourceClass, $pathPrefix, $operation);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
<argument type="service" id="sylius_api.provider.path_prefix" />
|
||||
<argument type="service" id="sylius_api.operation_resolver.path_prefix_based" />
|
||||
<argument type="service" id="api_platform.router" />
|
||||
<argument type="service" id="api_platform.resource_class_resolver" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"php": "^8.2",
|
||||
"doctrine/dbal": "^3.9",
|
||||
"api-platform/doctrine-orm": "^4.2.1",
|
||||
"api-platform/metadata": "^4.2.1",
|
||||
"api-platform/symfony": "^4.2.1",
|
||||
"lexik/jwt-authentication-bundle": "^3.1",
|
||||
"sylius/core-bundle": "^2.0",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Tests\Sylius\Bundle\ApiBundle\ApiPlatform\Routing;
|
|||
|
||||
use ApiPlatform\Metadata\IriConverterInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\Metadata\ResourceClassResolverInterface;
|
||||
use ApiPlatform\Metadata\UrlGeneratorInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
|
@ -35,6 +36,8 @@ final class IriConverterTest extends TestCase
|
|||
|
||||
private MockObject&RouterInterface $router;
|
||||
|
||||
private MockObject&ResourceClassResolverInterface $resourceClassResolver;
|
||||
|
||||
private IriConverter $iriConverter;
|
||||
|
||||
private CountryInterface&MockObject $country;
|
||||
|
|
@ -46,11 +49,13 @@ final class IriConverterTest extends TestCase
|
|||
$this->pathPrefixProvider = $this->createMock(PathPrefixProviderInterface::class);
|
||||
$this->operationResolver = $this->createMock(OperationResolverInterface::class);
|
||||
$this->router = $this->createMock(RouterInterface::class);
|
||||
$this->resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
|
||||
$this->iriConverter = new IriConverter(
|
||||
$this->decoratedIriConverter,
|
||||
$this->pathPrefixProvider,
|
||||
$this->operationResolver,
|
||||
$this->router,
|
||||
$this->resourceClassResolver,
|
||||
);
|
||||
$this->country = $this->createMock(CountryInterface::class);
|
||||
}
|
||||
|
|
@ -104,4 +109,92 @@ final class IriConverterTest extends TestCase
|
|||
],
|
||||
));
|
||||
}
|
||||
|
||||
public function testResolvesDiscriminatorSubclassToParentResourceClassBeforeResolvingOperation(): void
|
||||
{
|
||||
/** @var Operation&MockObject $shopOperation */
|
||||
$shopOperation = $this->createMock(Operation::class);
|
||||
|
||||
$concreteSubclassInstance = new Country();
|
||||
|
||||
$this->resourceClassResolver->expects(self::once())
|
||||
->method('isResourceClass')
|
||||
->with(Country::class)
|
||||
->willReturn(true);
|
||||
|
||||
$this->resourceClassResolver->expects(self::once())
|
||||
->method('getResourceClass')
|
||||
->with($concreteSubclassInstance)
|
||||
->willReturn(CountryInterface::class);
|
||||
|
||||
$this->pathPrefixProvider->expects(self::once())
|
||||
->method('getPathPrefix')
|
||||
->with('api/v2/shop/countries')
|
||||
->willReturn('shop');
|
||||
|
||||
$this->operationResolver->expects(self::once())
|
||||
->method('resolve')
|
||||
->with(CountryInterface::class, 'shop', null)
|
||||
->willReturn($shopOperation);
|
||||
|
||||
$this->decoratedIriConverter->expects(self::once())
|
||||
->method('getIriFromResource')
|
||||
->with(self::identicalTo($concreteSubclassInstance), UrlGeneratorInterface::ABS_PATH, $shopOperation, [
|
||||
'request_uri' => 'api/v2/shop/countries',
|
||||
])
|
||||
->willReturn('api/v2/shop/countries/CODE');
|
||||
|
||||
self::assertSame(
|
||||
'api/v2/shop/countries/CODE',
|
||||
$this->iriConverter->getIriFromResource(
|
||||
$concreteSubclassInstance,
|
||||
UrlGeneratorInterface::ABS_PATH,
|
||||
null,
|
||||
['request_uri' => 'api/v2/shop/countries'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function testUsesConcreteClassDirectlyWhenItIsNotAKnownResourceClass(): void
|
||||
{
|
||||
/** @var Operation&MockObject $operation */
|
||||
$operation = $this->createMock(Operation::class);
|
||||
|
||||
$concreteInstance = new Country();
|
||||
|
||||
$this->resourceClassResolver->expects(self::once())
|
||||
->method('isResourceClass')
|
||||
->with(Country::class)
|
||||
->willReturn(false);
|
||||
|
||||
$this->resourceClassResolver->expects(self::never())
|
||||
->method('getResourceClass');
|
||||
|
||||
$this->pathPrefixProvider->expects(self::once())
|
||||
->method('getPathPrefix')
|
||||
->with('api/v2/shop/countries')
|
||||
->willReturn('shop');
|
||||
|
||||
$this->operationResolver->expects(self::once())
|
||||
->method('resolve')
|
||||
->with(Country::class, 'shop', null)
|
||||
->willReturn($operation);
|
||||
|
||||
$this->decoratedIriConverter->expects(self::once())
|
||||
->method('getIriFromResource')
|
||||
->with(self::identicalTo($concreteInstance), UrlGeneratorInterface::ABS_PATH, $operation, [
|
||||
'request_uri' => 'api/v2/shop/countries',
|
||||
])
|
||||
->willReturn('api/v2/shop/countries/CODE');
|
||||
|
||||
self::assertSame(
|
||||
'api/v2/shop/countries/CODE',
|
||||
$this->iriConverter->getIriFromResource(
|
||||
$concreteInstance,
|
||||
UrlGeneratorInterface::ABS_PATH,
|
||||
null,
|
||||
['request_uri' => 'api/v2/shop/countries'],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
|
||||
This file is part of the Sylius package.
|
||||
|
||||
(c) Sylius Sp. z o.o.
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
|
||||
-->
|
||||
|
||||
<resources
|
||||
xmlns="https://api-platform.com/schema/metadata/resources-3.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0 https://api-platform.com/schema/metadata/resources-3.0.xsd"
|
||||
>
|
||||
<resource class="Sylius\Bundle\ApiBundle\Application\Resource\SampleProduct">
|
||||
<operations>
|
||||
<operation
|
||||
class="ApiPlatform\Metadata\GetCollection"
|
||||
uriTemplate="/admin/sample-products"
|
||||
name="sylius_api_admin_sample_product_get_collection"
|
||||
provider="Sylius\Bundle\ApiBundle\Application\Resource\SampleProductProvider"
|
||||
/>
|
||||
|
||||
<operation
|
||||
class="ApiPlatform\Metadata\Get"
|
||||
uriTemplate="/admin/sample-products/{id}"
|
||||
name="sylius_api_admin_sample_product_get"
|
||||
provider="Sylius\Bundle\ApiBundle\Application\Resource\SampleProductProvider"
|
||||
/>
|
||||
|
||||
<operation
|
||||
class="ApiPlatform\Metadata\GetCollection"
|
||||
uriTemplate="/shop/sample-products"
|
||||
name="sylius_api_shop_sample_product_get_collection"
|
||||
provider="Sylius\Bundle\ApiBundle\Application\Resource\SampleProductProvider"
|
||||
/>
|
||||
|
||||
<operation
|
||||
class="ApiPlatform\Metadata\Get"
|
||||
uriTemplate="/shop/sample-products/{id}"
|
||||
name="sylius_api_shop_sample_product_get"
|
||||
provider="Sylius\Bundle\ApiBundle\Application\Resource\SampleProductProvider"
|
||||
/>
|
||||
</operations>
|
||||
</resource>
|
||||
</resources>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
services:
|
||||
Sylius\Bundle\ApiBundle\Application\Resource\SampleProductProvider:
|
||||
tags: ['api_platform.state_provider']
|
||||
|
||||
Sylius\Bundle\ApiBundle\Application\CommandHandler\FooHandler:
|
||||
tags:
|
||||
- { name: messenger.message_handler, bus: sylius.command_bus }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Application\Resource;
|
||||
|
||||
class SampleProduct
|
||||
{
|
||||
public function __construct(
|
||||
private int $id,
|
||||
private string $name,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Application\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\CollectionOperationInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
|
||||
/** @implements ProviderInterface<SampleProduct> */
|
||||
final readonly class SampleProductProvider implements ProviderInterface
|
||||
{
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|object
|
||||
{
|
||||
if ($operation instanceof CollectionOperationInterface) {
|
||||
return [new SpecialSampleProduct(1, 'Special Product')];
|
||||
}
|
||||
|
||||
return new SpecialSampleProduct((int) ($uriVariables['id'] ?? 1), 'Special Product');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Application\Resource;
|
||||
|
||||
/**
|
||||
* Simulates an entity subclass (e.g. a Doctrine discriminator map subclass)
|
||||
* that is not an API resource itself, but extends a registered API resource.
|
||||
*/
|
||||
class SpecialSampleProduct extends SampleProduct
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Application\Tests;
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
|
||||
final class SampleProductTest extends ApiTestCase
|
||||
{
|
||||
use SetUpTestsTrait;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->setFixturesFiles([]);
|
||||
$this->setUpTest();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_generates_correct_iris_for_subclass_instances_on_admin_endpoint(): void
|
||||
{
|
||||
static::createClient()->request(
|
||||
'GET',
|
||||
'api/v2/admin/sample-products',
|
||||
['auth_bearer' => $this->JWTAdminUserToken],
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertJsonContains([
|
||||
'@context' => '/api/v2/contexts/SampleProduct',
|
||||
'@id' => '/api/v2/admin/sample-products',
|
||||
'@type' => 'hydra:Collection',
|
||||
'hydra:member' => [[
|
||||
'@id' => '/api/v2/admin/sample-products/1',
|
||||
'@type' => 'SampleProduct',
|
||||
'name' => 'Special Product',
|
||||
]],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_generates_correct_iris_for_subclass_instances_on_shop_endpoint(): void
|
||||
{
|
||||
static::createClient()->request('GET', 'api/v2/shop/sample-products');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertJsonContains([
|
||||
'@context' => '/api/v2/contexts/SampleProduct',
|
||||
'@id' => '/api/v2/shop/sample-products',
|
||||
'@type' => 'hydra:Collection',
|
||||
'hydra:member' => [[
|
||||
'@id' => '/api/v2/shop/sample-products/1',
|
||||
'@type' => 'SampleProduct',
|
||||
'name' => 'Special Product',
|
||||
]],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue