Skip to content

Commit

Permalink
Merge 3.3
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jun 28, 2024
2 parents 0d5f356 + 6daec82 commit d30d707
Show file tree
Hide file tree
Showing 34 changed files with 302 additions and 79 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## v3.3.7

### Bug fixes

* [0ed1b637a](https://github.com/api-platform/core/commit/0ed1b637a84d0860c2e014e2473b52d17de9276b) fix(metadata): wrong schema generated if openapicontext set on array (#6431)
* [57f930c65](https://github.com/api-platform/core/commit/57f930c65cb067b1727488ba6db6188c6c5a01c6) fix(openapi): parameters can disable openapi (#6440)
* [69b4d35b9](https://github.com/api-platform/core/commit/69b4d35b9764e36dab0484e1a7332886e1de9b2c) fix(state): provider as ParameterProviderInterface (#6430)
* [842091ddf](https://github.com/api-platform/core/commit/842091ddf5b41c7a0d76bfbcddccff2920a84c35) fix(jsonschema): make all required properties optional in PATCH operation with 'json' format (#6394)
* [af34e72ed](https://github.com/api-platform/core/commit/af34e72ed6bb0d5f7235fcc9c2a6c7810ba4f9c2) fix(openapi): yaml openapi export should have numeric keys as string (#6436)
* [b42e25f2d](https://github.com/api-platform/core/commit/b42e25f2dcb5e0c1759c9482c08c9929ef86dc90) fix(state): parameter decorates main chain (#6434)
* [c922ba3f5](https://github.com/api-platform/core/commit/c922ba3f5cf6117e85548697a982fb1cbe3dde2e) fix(symfony): check method for readonly routes (#6437)

## v3.3.6

### Bug fixes
Expand Down Expand Up @@ -223,6 +235,12 @@ api_platform:
form: ['multipart/form-data']
```

## v3.2.25

### Bug fixes

* [0ed1b637a](https://github.com/api-platform/core/commit/0ed1b637a84d0860c2e014e2473b52d17de9276b) fix(metadata): wrong schema generated if openapicontext set on array (#6431)

## v3.2.24

### Bug fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ public function create(string $resourceClass, string $property, array $options =
$propertySchema['example'] = $propertySchema['default'];
}

// never override the following keys if at least one is already set
// never override the following keys if at least one is already set or if there's a custom openapi context
if ([] === $types
|| ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
|| ($propertyMetadata->getOpenapiContext() ?? false)
) {
return $propertyMetadata->withSchema($propertySchema);
}
Expand Down
12 changes: 11 additions & 1 deletion src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand All @@ -33,6 +34,9 @@
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
use ResourceMetadataTrait;

private const PATCH_SCHEMA_POSTFIX = '.patch';

private ?TypeFactoryInterface $typeFactory = null;
private ?SchemaFactoryInterface $schemaFactory = null;
// Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object
Expand Down Expand Up @@ -88,6 +92,12 @@ public function buildSchema(string $className, string $format = 'json', string $
return $schema;
}

$isJsonMergePatch = 'json' === $format && $operation instanceof Patch && Schema::TYPE_INPUT === $type;

if ($isJsonMergePatch) {
$definitionName .= self::PATCH_SCHEMA_POSTFIX;
}

if (!isset($schema['$ref']) && !isset($schema['type'])) {
$ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) {
Expand Down Expand Up @@ -136,7 +146,7 @@ public function buildSchema(string $className, string $format = 'json', string $
}

$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;
if ($propertyMetadata->isRequired()) {
if ($propertyMetadata->isRequired() && !$isJsonMergePatch) {
$definition['required'][] = $normalizedPropertyName;
}

Expand Down
33 changes: 33 additions & 0 deletions src/JsonSchema/Tests/Fixtures/DummyWithCustomOpenApiContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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\JsonSchema\Tests\Fixtures;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;

/*
* 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.
*/

#[ApiResource]
class DummyWithCustomOpenApiContext
{
#[ApiProperty(openapiContext: ['type' => 'object', 'properties' => ['alpha' => ['type' => 'integer']]])]
public $acme;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\JsonSchema\Tests\Metadata\Property\Factory;

use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory;
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithCustomOpenApiContext;
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithEnum;
use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\IntEnumAsIdentifier;
use ApiPlatform\Metadata\ApiProperty;
Expand All @@ -34,4 +35,18 @@ public function testEnum(): void
$apiProperty = $schemaPropertyMetadataFactory->create(DummyWithEnum::class, 'intEnumAsIdentifier');
$this->assertEquals(['type' => ['integer', 'null'], 'enum' => [1, 2, null]], $apiProperty->getSchema());
}

public function testWithCustomOpenApiContext(): void
{
$resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
$apiProperty = new ApiProperty(
builtinTypes: [new Type(builtinType: 'object', nullable: true, class: IntEnumAsIdentifier::class)],
openapiContext: ['type' => 'object', 'properties' => ['alpha' => ['type' => 'integer']]],
);
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
$decorated->expects($this->once())->method('create')->with(DummyWithCustomOpenApiContext::class, 'acme')->willReturn($apiProperty);
$schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated);
$apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'acme');
$this->assertEquals([], $apiProperty->getSchema());
}
}
4 changes: 2 additions & 2 deletions src/Metadata/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ abstract class Parameter
public function __construct(
protected ?string $key = null,
protected ?array $schema = null,
protected ?OpenApi\Model\Parameter $openApi = null,
protected OpenApi\Model\Parameter|bool|null $openApi = null, // TODO: use false as type instead of bool
protected mixed $provider = null,
protected mixed $filter = null,
protected ?string $property = null,
Expand All @@ -57,7 +57,7 @@ public function getSchema(): ?array
return $this->schema;
}

public function getOpenApi(): ?OpenApi\Model\Parameter
public function getOpenApi(): OpenApi\Model\Parameter|bool|null
{
return $this->openApi;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
}
}

$schema = $parameter->getSchema() ?? $parameter->getOpenApi()?->getSchema();
$schema = $parameter->getSchema() ?? (($openApi = $parameter->getOpenApi()) ? $openApi->getSchema() : null);

// Only add validation if the Symfony Validator is installed
if (interface_exists(ValidatorInterface::class) && !$parameter->getConstraints()) {
$parameter = $this->addSchemaValidation($parameter, $schema, $parameter->getRequired() ?? $description['required'] ?? false, $parameter->getOpenApi());
$parameter = $this->addSchemaValidation($parameter, $schema, $parameter->getRequired() ?? $description['required'] ?? false, $parameter->getOpenApi() ?: null);
}

return $parameter;
Expand Down
2 changes: 1 addition & 1 deletion src/OpenApi/Command/OpenApiCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'spec_version' => $input->getOption('spec-version'),
]);
$content = $input->getOption('yaml')
? Yaml::dump($data, 10, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)
? Yaml::dump($data, 10, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | Yaml::DUMP_NUMERIC_KEY_AS_STRING)
: (json_encode($data, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) ?: '');

$filename = $input->getOption('output');
Expand Down
4 changes: 4 additions & 0 deletions src/OpenApi/Factory/OpenApiFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection

$openapiParameters = $openapiOperation->getParameters();
foreach ($operation->getParameters() ?? [] as $key => $p) {
if (false === $p->getOpenApi()) {
continue;
}

$in = $p instanceof HeaderParameterInterface ? 'header' : 'query';
$parameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']);

Expand Down
4 changes: 2 additions & 2 deletions src/OpenApi/Model/PathItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class PathItem

public static array $methods = ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE'];

public function __construct(private ?string $ref = null, private ?string $summary = null, private ?string $description = null, private ?Operation $get = null, private ?Operation $put = null, private ?Operation $post = null, private ?Operation $delete = null, private ?Operation $options = null, private ?Operation $head = null, private ?Operation $patch = null, private ?Operation $trace = null, private ?array $servers = null, private array $parameters = [])
public function __construct(private ?string $ref = null, private ?string $summary = null, private ?string $description = null, private ?Operation $get = null, private ?Operation $put = null, private ?Operation $post = null, private ?Operation $delete = null, private ?Operation $options = null, private ?Operation $head = null, private ?Operation $patch = null, private ?Operation $trace = null, private ?array $servers = null, private ?array $parameters = null)
{
}

Expand Down Expand Up @@ -184,7 +184,7 @@ public function withServers(?array $servers = null): self
return $clone;
}

public function withParameters(array $parameters): self
public function withParameters(?array $parameters = null): self
{
$clone = clone $this;
$clone->parameters = $parameters;
Expand Down
44 changes: 14 additions & 30 deletions src/State/Provider/ParameterProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@

namespace ApiPlatform\State\Provider;

use ApiPlatform\Metadata\HeaderParameterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\Parameters;
use ApiPlatform\State\Exception\ProviderNotFoundException;
use ApiPlatform\State\ParameterProviderInterface;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\State\Util\ParameterParserTrait;
use ApiPlatform\State\Util\RequestParser;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

/**
* Loops over parameters to:
Expand All @@ -33,6 +32,8 @@
*/
final class ParameterProvider implements ProviderInterface
{
use ParameterParserTrait;

public function __construct(private readonly ?ProviderInterface $decorated = null, private readonly ?ContainerInterface $locator = null)
{
}
Expand All @@ -50,31 +51,27 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
}

$context = ['operation' => $operation] + $context;
$parameters = $operation->getParameters() ?? [];
$operationParameters = $parameters instanceof Parameters ? iterator_to_array($parameters) : $parameters;
foreach ($operationParameters as $parameter) {
$p = $operation->getParameters() ?? [];
$parameters = $p instanceof Parameters ? iterator_to_array($p) : $p;
foreach ($parameters as $parameter) {
$key = $parameter->getKey();
$parameters = $this->extractParameterValues($parameter, $request, $context);
$parsedKey = explode('[:property]', $key);

if (isset($parsedKey[0]) && isset($parameters[$parsedKey[0]])) {
$key = $parsedKey[0];
}
$values = $this->extractParameterValues($parameter, $request, $context);
$key = $this->getParameterFlattenKey($key, $values);

if (!isset($parameters[$key])) {
if (!isset($values[$key])) {
continue;
}

$operationParameters[$parameter->getKey()] = $parameter = $parameter->withExtraProperties(
$parameter->getExtraProperties() + ['_api_values' => [$key => $parameters[$key]]]
$parameters[$parameter->getKey()] = $parameter = $parameter->withExtraProperties(
$parameter->getExtraProperties() + ['_api_values' => [$key => $values[$key]]]
);

if (null === ($provider = $parameter->getProvider())) {
continue;
}

if (\is_callable($provider)) {
if (($op = $provider($parameter, $parameters, $context)) instanceof Operation) {
if (($op = $provider($parameter, $values, $context)) instanceof Operation) {
$operation = $op;
}

Expand All @@ -87,28 +84,15 @@ public function provide(Operation $operation, array $uriVariables = [], array $c

/** @var ParameterProviderInterface $providerInstance */
$providerInstance = $this->locator->get($provider);
if (($op = $providerInstance->provide($parameter, $parameters, $context)) instanceof Operation) {
if (($op = $providerInstance->provide($parameter, $values, $context)) instanceof Operation) {
$operation = $op;
}
}

$operation = $operation->withParameters(new Parameters($operationParameters));
$operation = $operation->withParameters(new Parameters($parameters));
$request?->attributes->set('_api_operation', $operation);
$context['operation'] = $operation;

return $this->decorated?->provide($operation, $uriVariables, $context);
}

/**
* @param array<string, mixed> $context
*/
private function extractParameterValues(Parameter $parameter, ?Request $request, array $context)
{
if ($request) {
return $parameter instanceof HeaderParameterInterface ? $request->attributes->get('_api_header_parameters') : $request->attributes->get('_api_query_parameters');
}

// GraphQl
return $context['args'] ?? [];
}
}
53 changes: 53 additions & 0 deletions src/State/Util/ParameterParserTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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\State\Util;

use ApiPlatform\Metadata\HeaderParameterInterface;
use ApiPlatform\Metadata\Parameter;
use Symfony\Component\HttpFoundation\Request;

/**
* @internal
*/
trait ParameterParserTrait
{
/**
* @param array<string, mixed> $values
*/
private function getParameterFlattenKey(string $key, array $values): string
{
$parsedKey = explode('[:property]', $key);

if (isset($parsedKey[0]) && isset($values[$parsedKey[0]])) {
return $parsedKey[0];
}

return $key;
}

/**
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*/
private function extractParameterValues(Parameter $parameter, ?Request $request, array $context): array
{
if ($request) {
return ($parameter instanceof HeaderParameterInterface ? $request->attributes->get('_api_header_parameters') : $request->attributes->get('_api_query_parameters')) ?? [];
}

// GraphQl
return $context['args'] ?? [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,6 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr
$container->setParameter('api_platform.validator.legacy_validation_exception', $config['validator']['legacy_validation_exception'] ?? true);
$loader->load('metadata/validator.xml');
$loader->load('validator/validator.xml');
$loader->load('symfony/parameter_validator.xml');

if ($this->isConfigEnabled($container, $config['graphql'])) {
$loader->load('graphql/validator.xml');
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/routing/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="api_entrypoint" path="/{index}.{_format}">
<route id="api_entrypoint" path="/{index}.{_format}" methods="GET|HEAD">
<default key="_controller">api_platform.action.entrypoint</default>
<default key="_format" />
<default key="_api_respond">true</default>
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/routing/docs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="api_doc" path="/docs.{_format}">
<route id="api_doc" path="/docs.{_format}" methods="GET|HEAD">
<default key="_controller">api_platform.action.documentation</default>
<default key="_format" />
<default key="_api_respond">true</default>
Expand Down
Loading

0 comments on commit d30d707

Please sign in to comment.