feature #15063 [API] Allow overriding xml resource mapping (NoResponseMate)

This PR was merged into the 1.13 branch.

Discussion
----------

| Q               | A                                                            |
|-----------------|--------------------------------------------------------------|
| Branch?         | 1.13 |
| Bug fix?        | no                                                       |
| New feature?    | yes                                                       |
| BC breaks?      | no                                                       |
| Deprecations?   | no |
| Related tickets | replaces #13292                      |
| License         | MIT                                                          |

Commits
-------

4f5e141611 [API] Test overriding xml mapping
3db63e41ee [API] Allow overriding xml mapping
This commit is contained in:
Grzegorz Sadowski 2023-06-09 14:54:07 +02:00 committed by GitHub
commit 0d8d558808
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 900 additions and 44 deletions

View file

@ -54,11 +54,12 @@
"Payum\\Core\\Security\\TokenInterface",
"Payum\\Core\\Security\\Util\\Random",
"Payum\\Core\\Storage\\AbstractStorage",
"Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface",
"PHPUnit\\Framework\\ExpectationFailedException",
"Psr\\Container\\ContainerInterface",
"Psr\\Http\\Message\\RequestFactoryInterface",
"Psr\\Http\\Message\\StreamFactoryInterface",
"Stripe\\Stripe"
"Stripe\\Stripe",
"Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface"
],
"php-core-extensions" : [
"Core",

View file

@ -29,6 +29,7 @@
"ext-hash": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-simplexml": "*",
"api-platform/core": "^2.7.10",
"babdev/pagerfanta-bundle": "^3.0",
"behat/transliterator": "^1.3",

View file

@ -37,6 +37,7 @@
<DeprecatedClass>
<errorLevel type="info">
<referencedClass name="ApiPlatform\Core\Metadata\Extractor\XmlExtractor" /> <!-- deprecated in ApiPlatform 2.7 -->
<referencedClass name="Http\Message\MessageFactory" /> <!-- deprecated in HttpMessage 1.1 -->
<referencedClass name="Payum\Core\Action\GatewayAwareAction" />
<referencedClass name="Payum\Core\Security\GenericTokenFactoryInterface" />

View file

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\ApiPlatform\Metadata\Merger;
final class LegacyResourceMetadataMerger implements MetadataMergerInterface
{
public function merge(array $oldMetadata, array $newMetadata): array
{
if ([] === $newMetadata || [] === $oldMetadata) {
return [] === $newMetadata ? $oldMetadata : $newMetadata;
}
foreach ($newMetadata as $key => $value) {
if ('properties' === $key) {
continue;
}
if (is_array($value) && str_contains($key, 'Operations')) {
foreach ($value as $operationKey => $operationData) {
if (isset($operationData['enabled']) && false === $operationData['enabled']) {
unset($oldMetadata[$key][$operationKey]);
continue;
}
$oldMetadata[$key][$operationKey] = array_merge(
$oldMetadata[$key][$operationKey] ?? [],
$operationData,
);
}
continue;
}
$oldMetadata[$key] = $value;
}
return $oldMetadata;
}
}

View file

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\ApiPlatform\Metadata\Merger;
interface MetadataMergerInterface
{
public function merge(array $oldMetadata, array $newMetadata): array;
}

View file

@ -0,0 +1,246 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\ApiPlatform\Metadata;
use ApiPlatform\Core\Metadata\Extractor\XmlExtractor;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Extractor\AbstractResourceExtractor;
use ApiPlatform\Metadata\Extractor\PropertyExtractorInterface;
use ApiPlatform\Metadata\Extractor\XmlPropertyExtractor;
use ApiPlatform\Metadata\Extractor\XmlResourceExtractor;
use Psr\Container\ContainerInterface;
use Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\Merger\MetadataMergerInterface;
use Symfony\Component\Config\Util\XmlUtils;
/**
* @experimental
*
* @see XmlExtractor
*/
final class MergingXmlExtractor extends AbstractResourceExtractor implements PropertyExtractorInterface
{
private array $properties = [];
/**
* @param string[] $paths
*/
public function __construct(
array $paths,
ContainerInterface $container = null,
private ?MetadataMergerInterface $merger = null,
) {
parent::__construct($paths, $container);
}
/**
* @inheritdoc
*/
public function getResources(): array
{
if (!empty($this->resources)) {
return $this->resources;
}
$this->resources = [];
foreach ($this->paths as $path) {
$this->extractPath($path);
}
return $this->resources;
}
/**
* @inheritdoc
*/
public function getProperties(): array
{
if (!empty($this->properties)) {
return $this->properties;
}
$this->properties = [];
foreach ($this->paths as $path) {
$this->extractPath($path);
}
return $this->properties;
}
protected function extractPath(string $path): void
{
try {
/** @var \SimpleXMLElement $xml */
$xml = simplexml_import_dom(XmlUtils::loadFile($path, XmlExtractor::RESOURCE_SCHEMA));
} catch (\InvalidArgumentException $e) {
// Test if this is a new resource
try {
$xml = XmlUtils::loadFile($path, XmlResourceExtractor::SCHEMA);
return;
} catch (\InvalidArgumentException) {
try {
$xml = XmlUtils::loadFile($path, XmlPropertyExtractor::SCHEMA);
return;
} catch (\InvalidArgumentException) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
}
foreach ($xml->resource as $resource) {
$resourceClass = $this->resolve((string) $resource['class']);
$resourceMetadata = $this->buildResource($resource);
if (null !== $this->merger && isset($this->resources[$resourceClass])) {
$this->resources[$resourceClass] = $this->merger->merge(
$this->resources[$resourceClass],
$resourceMetadata,
);
$this->properties[$resourceClass] = array_merge(
$this->properties[$resourceClass],
$resourceMetadata['properties'],
);
continue;
}
$this->resources[$resourceClass] = $resourceMetadata;
$this->properties[$resourceClass] = $this->resources[$resourceClass]['properties'];
}
}
private function buildResource(\SimpleXMLElement $resource): array
{
return [
'shortName' => $this->phpizeAttribute($resource, 'shortName', 'string'),
'description' => $this->phpizeAttribute($resource, 'description', 'string'),
'iri' => $this->phpizeAttribute($resource, 'iri', 'string'),
'itemOperations' => $this->extractOperations($resource, 'itemOperation'),
'collectionOperations' => $this->extractOperations($resource, 'collectionOperation'),
'subresourceOperations' => $this->extractOperations($resource, 'subresourceOperation'),
'graphql' => $this->extractOperations($resource, 'operation'),
'attributes' => $this->extractAttributes($resource, 'attribute') ?: null,
'properties' => $this->extractProperties($resource) ?: null,
];
}
/**
* Returns the array containing configured operations. Returns NULL if there is no operation configuration.
*/
private function extractOperations(\SimpleXMLElement $resource, string $operationType): ?array
{
$graphql = 'operation' === $operationType;
if (!$graphql && $legacyOperations = $this->extractAttributes($resource, $operationType)) {
@trigger_error(
sprintf('Configuring "%1$s" tags without using a parent "%1$ss" tag is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', $operationType),
\E_USER_DEPRECATED,
);
return $legacyOperations;
}
$operationsParent = $graphql ? 'graphql' : "{$operationType}s";
if (!isset($resource->{$operationsParent})) {
return null;
}
return $this->extractAttributes($resource->{$operationsParent}, $operationType, true);
}
/**
* Recursively transforms an attribute structure into an associative array.
*/
private function extractAttributes(\SimpleXMLElement $resource, string $elementName, bool $topLevel = false): array
{
$attributes = [];
foreach ($resource->{$elementName} as $attribute) {
$value = isset($attribute->attribute[0]) ? $this->extractAttributes($attribute, 'attribute') : $this->phpizeContent($attribute);
// allow empty operations definition, like <collectionOperation name="post" />
if ($topLevel && '' === $value) {
$value = [];
}
if (isset($attribute['name'])) {
$attributes[(string) $attribute['name']] = $value;
} else {
$attributes[] = $value;
}
}
return $attributes;
}
/**
* Gets metadata of a property.
*/
private function extractProperties(\SimpleXMLElement $resource): array
{
$properties = [];
foreach ($resource->property as $property) {
$properties[(string) $property['name']] = [
'description' => $this->phpizeAttribute($property, 'description', 'string'),
'readable' => $this->phpizeAttribute($property, 'readable', 'bool'),
'writable' => $this->phpizeAttribute($property, 'writable', 'bool'),
'readableLink' => $this->phpizeAttribute($property, 'readableLink', 'bool'),
'writableLink' => $this->phpizeAttribute($property, 'writableLink', 'bool'),
'required' => $this->phpizeAttribute($property, 'required', 'bool'),
'identifier' => $this->phpizeAttribute($property, 'identifier', 'bool'),
'iri' => $this->phpizeAttribute($property, 'iri', 'string'),
'attributes' => $this->extractAttributes($property, 'attribute'),
'subresource' => $property->subresource ? [
'collection' => $this->phpizeAttribute($property->subresource, 'collection', 'bool'),
'resourceClass' => $this->resolve($this->phpizeAttribute($property->subresource, 'resourceClass', 'string')),
'maxDepth' => $this->phpizeAttribute($property->subresource, 'maxDepth', 'integer'),
] : null,
];
}
return $properties;
}
/**
* Transforms an XML attribute's value in a PHP value.
*/
private function phpizeAttribute(\SimpleXMLElement $array, string $key, string $type): bool|int|string|null
{
if (!isset($array[$key])) {
return null;
}
return match ($type) {
'string' => (string) $array[$key],
'integer' => (int) $array[$key],
'bool' => (bool) XmlUtils::phpize($array[$key]),
default => null,
};
}
/**
* Transforms an XML element's content in a PHP value.
*/
private function phpizeContent(\SimpleXMLElement $array)
{
$type = $array['type'] ?? null;
$value = (string) $array;
switch ($type) {
case 'string':
return $value;
case 'constant':
return \constant($value);
default:
return XmlUtils::phpize($value);
}
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* 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\DependencyInjection\Compiler;
use Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\Merger\LegacyResourceMetadataMerger;
use Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\MergingXmlExtractor;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
final class ExtractorMergingCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if ($container->hasDefinition('api_platform.metadata.extractor.xml.legacy')) {
$definition = $container->getDefinition('api_platform.metadata.extractor.xml.legacy');
$definition->setClass(MergingXmlExtractor::class);
$definition->addArgument(new Reference(LegacyResourceMetadataMerger::class));
}
}
}

View file

@ -137,6 +137,8 @@
<argument type="service" id="Sylius\Bundle\ApiBundle\ApiPlatform\ApiResourceConfigurationMerger" />
</service>
<service id="Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\Merger\LegacyResourceMetadataMerger" />
<service
id="api_platform.metadata.resource.metadata_factory.yaml"
class="Sylius\Bundle\ApiBundle\ApiPlatform\Factory\MergingExtractorResourceMetadataFactory"

View file

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle;
use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\CommandDataTransformerPass;
use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\ExtractorMergingCompilerPass;
use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\FlattenExceptionNormalizerDecoratorCompilerPass;
use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\LegacyErrorHandlingCompilerPass;
use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\SyliusPriceHistoryLegacyAliasesPass;
@ -29,5 +30,6 @@ final class SyliusApiBundle extends Bundle
$container->addCompilerPass(new FlattenExceptionNormalizerDecoratorCompilerPass());
$container->addCompilerPass(new LegacyErrorHandlingCompilerPass());
$container->addCompilerPass(new SyliusPriceHistoryLegacyAliasesPass());
$container->addCompilerPass(new ExtractorMergingCompilerPass());
}
}

View file

@ -0,0 +1,68 @@
<?xml version="1.0" ?>
<!--
This file is part of the Sylius package.
(c) Paweł Jędrzejewski
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"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd"
>
<resource class="%sylius.model.country.class%" shortName="Country">
<collectionOperations>
<collectionOperation name="another_admin_get">
<attribute name="method">GET</attribute>
<attribute name="path">/admin/updated/countries</attribute>
<attribute name="filters">
<attribute>test.country.id_filter</attribute>
</attribute>
</collectionOperation>
<collectionOperation name="shop_get">
<attribute name="enabled">false</attribute>
</collectionOperation>
</collectionOperations>
<itemOperations>
<itemOperation name="shop_get">
<attribute name="method">GET</attribute>
<attribute name="path">/shop/countries/new/{code}</attribute>
<attribute name="normalization_context">
<attribute name="groups">shop:country:read</attribute>
</attribute>
</itemOperation>
<itemOperation name="admin_put">
<attribute name="enabled">false</attribute>
</itemOperation>
<itemOperation name="custom_delete">
<attribute name="method">DELETE</attribute>
<attribute name="path">/admin/countries/{code}</attribute>
</itemOperation>
</itemOperations>
<subresourceOperations>
<subresourceOperation name="provinces_get_subresource">
<attribute name="enabled">false</attribute>
</subresourceOperation>
<subresourceOperation name="provinces_delete_subresource">
<attribute name="method">DELETE</attribute>
<attribute name="path">/admin/countries/{code}/provinces/{id}</attribute>
</subresourceOperation>
</subresourceOperations>
<property name="id" identifier="false" writable="false" />
<property name="code" identifier="true" required="true" />
<property name="name" writable="false" readable="false" />
<property name="provinces" readable="false" writable="true" />
</resource>
</resources>

View file

@ -85,6 +85,11 @@ services:
arguments: [{id: 'exact', hostname: 'partial'}]
tags: ['api_platform.filter']
test.country.id_filter:
parent: api_platform.doctrine.orm.search_filter
arguments: [{id: 'exact', code: 'partial'}]
tags: ['api_platform.filter']
Sylius\Bundle\ApiBundle\Application\CommandHandler\FooHandler:
tags:
- { name: messenger.message_handler, bus: sylius.command_bus }

View file

@ -18,3 +18,7 @@ Sylius\Component\Currency\Model\Currency:
Sylius\Component\Locale\Model\Locale:
locale:
code: "en_US"
Sylius\Component\Addressing\Model\Country:
country_US:
code: "US"

View file

@ -27,10 +27,8 @@ final class SyliusConfigMergeTest extends ApiTestCase
$this->setUpTest();
}
/**
* @test
*/
public function it_removes_api_method_to_endpoint(): void
/** @test */
public function it_removes_api_method_to_endpoint_with_yaml(): void
{
static::createClient()->request(
'GET',
@ -38,60 +36,111 @@ final class SyliusConfigMergeTest extends ApiTestCase
['auth_bearer' => $this->JWTAdminUserToken],
);
$this->assertResponseStatusCodeSame(404);
self::assertResponseStatusCodeSame(404);
}
/**
* @test
*/
public function it_allows_to_add_new_operation(): void
/** @test */
public function it_removes_api_method_to_endpoint_with_xml(): void
{
static::createClient()->request(
'GET',
'/api/v2/shop/countries',
);
self::assertResponseStatusCodeSame(404);
}
/** @test */
public function it_allows_to_add_new_operation_with_yaml(): void
{
static::createClient()->request(
'GET',
'/api/v2/shop/channels-new-path',
);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['@type' => 'hydra:Collection']);
self::assertResponseIsSuccessful();
self::assertJsonContains(['@type' => 'hydra:Collection']);
}
/**
* @test
*/
public function it_allows_to_add_new_filter(): void
/** @test */
public function it_allows_to_add_new_operation_with_xml(): void
{
static::createClient()->request(
'DELETE',
'/api/v2/admin/countries/US',
['auth_bearer' => $this->JWTAdminUserToken],
);
self::assertResponseIsSuccessful();
}
/** @test */
public function it_allows_to_add_new_filter_with_yaml(): void
{
static::createClient()->request(
'GET',
'/api/v2/shop/channels-new-path?id=20',
);
$this->assertJsonContains(['hydra:totalItems' => 0]);
self::assertResponseIsSuccessful();
self::assertJsonContains(['hydra:totalItems' => 0]);
static::createClient()->request(
'GET',
'/api/v2/shop/channels-new-path',
);
$this->assertJsonContains(['hydra:totalItems' => 1]);
self::assertResponseIsSuccessful();
self::assertJsonContains(['hydra:totalItems' => 1]);
}
/**
* @test
*/
public function it_merges_configs(): void
/** @test */
public function it_allows_to_add_new_filter_with_xml(): void
{
static::createClient()->request(
'GET',
'/api/v2/admin/updated/countries?id=42',
['auth_bearer' => $this->JWTAdminUserToken],
);
self::assertResponseIsSuccessful();
self::assertJsonContains(['hydra:totalItems' => 0]);
static::createClient()->request(
'GET',
'/api/v2/admin/updated/countries',
['auth_bearer' => $this->JWTAdminUserToken],
);
self::assertResponseIsSuccessful();
self::assertJsonContains(['hydra:totalItems' => 1]);
}
/** @test */
public function it_merges_configs_with_yaml(): void
{
static::createClient()->request(
'GET',
'/api/v2/shop/channels/WEB',
);
$this->assertResponseIsSuccessful();
self::assertResponseIsSuccessful();
}
/**
* @test
*/
public function it_allows_to_overwrite_endpoint(): void
/** @test */
public function it_merges_configs_with_xml(): void
{
static::createClient()->request(
'GET',
'/api/v2/admin/countries',
['auth_bearer' => $this->JWTAdminUserToken],
);
self::assertResponseIsSuccessful();
}
/** @test */
public function it_allows_to_overwrite_endpoint_with_yaml(): void
{
static::createClient()->request(
'GET',
@ -99,7 +148,7 @@ final class SyliusConfigMergeTest extends ApiTestCase
['auth_bearer' => $this->JWTAdminUserToken],
);
$this->assertResponseStatusCodeSame(404);
self::assertResponseStatusCodeSame(404);
static::createClient()->request(
'GET',
@ -107,25 +156,42 @@ final class SyliusConfigMergeTest extends ApiTestCase
['auth_bearer' => $this->JWTAdminUserToken],
);
$this->assertResponseIsSuccessful();
self::assertResponseIsSuccessful();
}
/**
* @test
*/
public function it_allows_to_remove_non_crud_endpoint(): void
/** @test */
public function it_allows_to_overwrite_endpoint_with_xml(): void
{
$response =
json_decode(
static::createClient()
->request(
'PATCH',
'/api/v2/shop/orders/TOKEN/shipments/TEST',
)->getContent(false),
true,
);
static::createClient()->request(
'GET',
'/api/v2/shop/countries/US',
);
$this->assertResponseStatusCodeSame(404);
self::assertResponseStatusCodeSame(404);
static::createClient()->request(
'GET',
'/api/v2/shop/countries/new/US',
);
self::assertResponseIsSuccessful();
}
/** @test */
public function it_allows_to_remove_non_crud_endpoint_with_yaml(): void
{
$response = json_decode(
static::createClient()
->request(
'PATCH',
'/api/v2/shop/orders/TOKEN/shipments/TEST',
)->getContent(false),
true,
512,
\JSON_THROW_ON_ERROR,
);
self::assertResponseStatusCodeSame(404);
Assert::contains($response['hydra:description'], 'No route found');
}
}

View file

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace DependencyInjection\Compiler;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase;
use Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\Merger\LegacyResourceMetadataMerger;
use Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\MergingXmlExtractor;
use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\ExtractorMergingCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
final class ExtractorMergingCompilerPassTest extends AbstractCompilerPassTestCase
{
/** @test */
public function it_does_nothing_when_container_has_no_xml_extractor(): void
{
$this->compile();
$this->assertContainerBuilderNotHasService('api_platform.metadata.extractor.xml.legacy');
}
/** @test */
public function it_overwrites_xml_extractor(): void
{
$this->setDefinition(
'api_platform.metadata.extractor.xml.legacy',
new Definition(null, [[], new Reference('service_container')]),
);
$this->setDefinition(LegacyResourceMetadataMerger::class, new Definition());
$this->compile();
$this->assertContainerBuilderHasService(
'api_platform.metadata.extractor.xml.legacy',
MergingXmlExtractor::class,
);
$this->assertContainerBuilderHasServiceDefinitionWithArgument(
'api_platform.metadata.extractor.xml.legacy',
2,
new Reference(LegacyResourceMetadataMerger::class),
);
}
protected function registerCompilerPass(ContainerBuilder $container): void
{
$container->addCompilerPass(new ExtractorMergingCompilerPass());
}
}

View file

@ -0,0 +1,298 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\Merger;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\ApiPlatform\Metadata\Merger\MetadataMergerInterface;
final class LegacyResourceMetadataMergerSpec extends ObjectBehavior
{
function it_is_a_metadata_merger(): void
{
$this->shouldImplement(MetadataMergerInterface::class);
}
function it_does_nothing_when_both_new_and_old_metadata_are_epmty(): void
{
$this->merge([], [])->shouldReturn([]);
}
function it_returns_old_metadata_as_is_when_new_metadata_is_empty(): void
{
$this->merge(['foo' => 'bar'], [])->shouldReturn(['foo' => 'bar']);
}
function it_returns_new_metadata_as_is_when_old_metadata_is_empty(): void
{
$this->merge([], ['foo' => 'bar'])->shouldReturn(['foo' => 'bar']);
}
function it_ignores_properties_when_merging(): void
{
$this->merge(
['properties' => ['foo' => 'bar']],
['properties' => ['foo' => 'baz']],
)->shouldReturn(['properties' => ['foo' => 'bar']]);
}
function it_adds_metadata_missing_from_old_metadata_as_they_are(): void
{
$this->merge(
['foo' => 'bar'],
['baz' => 'qux'],
)->shouldReturn(['foo' => 'bar', 'baz' => 'qux']);
}
function it_overrides_metadata_present_in_both_old_and_new_metadata(): void
{
$this->merge(
['foo' => 'bar'],
['foo' => 'baz'],
)->shouldReturn(['foo' => 'baz']);
}
function it_adds_new_collection_operations_when_old_metadata_has_none(): void
{
$this->merge(
[],
['collectionOperations' => ['get' => ['baz' => 'qux']]],
)->shouldReturn(['collectionOperations' => ['get' => ['baz' => 'qux']]]);
}
function it_adds_new_collection_operations_when_old_metadata_did_not_have_them(): void
{
$this->merge(
['collectionOperations' => ['post' => ['foo' => 'bar']]],
['collectionOperations' => ['get' => ['baz' => 'qux']]],
)->shouldReturn(['collectionOperations' => [
'post' => ['foo' => 'bar'],
'get' => ['baz' => 'qux'],
]]);
}
function it_merges_collection_operations(): void
{
$this->merge([
'collectionOperations' => [
'get' => ['foo' => 'bar'],
],
], [
'collectionOperations' => [
'get' => ['baz' => 'qux'],
],
])->shouldReturn([
'collectionOperations' => [
'get' => ['foo' => 'bar', 'baz' => 'qux'],
],
]);
}
function it_adds_new_item_operations_when_old_metadata_has_none(): void
{
$this->merge(
[],
['itemOperations' => ['get' => ['baz' => 'qux']]],
)->shouldReturn(['itemOperations' => ['get' => ['baz' => 'qux']]]);
}
function it_adds_new_item_operations_when_old_metadata_did_not_have_them(): void
{
$this->merge(
['itemOperations' => ['post' => ['foo' => 'bar']]],
['itemOperations' => ['get' => ['baz' => 'qux']]],
)->shouldReturn(['itemOperations' => [
'post' => ['foo' => 'bar'],
'get' => ['baz' => 'qux'],
]]);
}
function it_merges_item_operations(): void
{
$this->merge([
'itemOperations' => [
'get' => ['foo' => 'bar'],
],
], [
'itemOperations' => [
'get' => ['baz' => 'qux'],
],
])->shouldReturn([
'itemOperations' => [
'get' => ['foo' => 'bar', 'baz' => 'qux'],
],
]);
}
function it_adds_new_subresource_operations_when_old_metadata_has_none(): void
{
$this->merge(
[],
['subresourceOperations' => ['get' => ['baz' => 'qux']]],
)->shouldReturn(['subresourceOperations' => ['get' => ['baz' => 'qux']]]);
}
function it_adds_new_subresource_operations_when_old_metadata_did_not_have_them(): void
{
$this->merge(
['subresourceOperations' => ['post' => ['foo' => 'bar']]],
['subresourceOperations' => ['get' => ['baz' => 'qux']]],
)->shouldReturn(['subresourceOperations' => [
'post' => ['foo' => 'bar'],
'get' => ['baz' => 'qux'],
]]);
}
function it_merges_subresource_operations(): void
{
$this->merge([
'subresourceOperations' => [
'get' => ['foo' => 'bar'],
],
], [
'subresourceOperations' => [
'get' => ['baz' => 'qux'],
],
])->shouldReturn([
'subresourceOperations' => [
'get' => ['foo' => 'bar', 'baz' => 'qux'],
],
]);
}
function it_merges_complex_metadata(): void
{
$this->merge([
'validation_groups' => ['Default', 'sylius'],
'route_prefix' => 'old',
'collectionOperations' => [
'get' => [
'method' => 'GET',
'path' => '/old',
'controller' => 'old',
'defaults' => ['_controller' => 'old'],
'requirements' => ['_format' => 'old'],
'options' => ['foo' => 'bar'],
'openapi_context' => ['foo' => 'bar'],
],
],
'itemOperations' => [
'get' => [
'method' => 'GET',
'path' => '/old/{id}',
'controller' => 'old',
'defaults' => ['_controller' => 'old'],
'requirements' => ['_format' => 'old'],
'options' => ['foo' => 'bar'],
'openapi_context' => ['foo' => 'bar'],
],
],
'subresourceOperations' => [
'get' => [
'method' => 'GET',
'path' => '/old/{id}/old',
'controller' => 'old',
'defaults' => ['_controller' => 'old'],
'requirements' => ['_format' => 'old'],
'options' => ['foo' => 'bar'],
'openapi_context' => ['foo' => 'bar'],
],
],
'properties' => [
'foo' => [
'description' => 'old',
'readable' => true,
'writable' => true,
'required' => true,
'identifier' => true,
],
],
], [
'validation_groups' => 'sylius',
'collectionOperations' => [
'get' => [
'method' => 'GET',
'path' => '/new',
'controller' => 'new',
],
],
'itemOperations' => [
'post' => [
'method' => 'POST',
'path' => '/new/{id}',
'controller' => 'new',
],
],
'properties' => [
'bar' => [
'description' => 'new',
'readable' => true,
'writable' => true,
'required' => false,
'readableLink' => true,
'identifier' => false,
],
],
])->shouldIterateLike([
'validation_groups' => 'sylius',
'route_prefix' => 'old',
'collectionOperations' => [
'get' => [
'method' => 'GET',
'path' => '/new',
'controller' => 'new',
'defaults' => ['_controller' => 'old'],
'requirements' => ['_format' => 'old'],
'options' => ['foo' => 'bar'],
'openapi_context' => ['foo' => 'bar'],
],
],
'itemOperations' => [
'get' => [
'method' => 'GET',
'path' => '/old/{id}',
'controller' => 'old',
'defaults' => ['_controller' => 'old'],
'requirements' => ['_format' => 'old'],
'options' => ['foo' => 'bar'],
'openapi_context' => ['foo' => 'bar'],
],
'post' => [
'method' => 'POST',
'path' => '/new/{id}',
'controller' => 'new',
],
],
'subresourceOperations' => [
'get' => [
'method' => 'GET',
'path' => '/old/{id}/old',
'controller' => 'old',
'defaults' => ['_controller' => 'old'],
'requirements' => ['_format' => 'old'],
'options' => ['foo' => 'bar'],
'openapi_context' => ['foo' => 'bar'],
],
],
'properties' => [
'foo' => [
'description' => 'old',
'readable' => true,
'writable' => true,
'required' => true,
'identifier' => true,
],
],
]);
}
}