Skip to content

Commit

Permalink
Merge 4.0
Browse files Browse the repository at this point in the history
Merge 4.0
  • Loading branch information
soyuka authored Dec 13, 2024
2 parents 6bdc01c + 4fac6f5 commit c59e275
Show file tree
Hide file tree
Showing 24 changed files with 349 additions and 35 deletions.
8 changes: 4 additions & 4 deletions features/filter/filter_validation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ Feature: Validate filters based upon filter description
Scenario: Required filter should throw an error if not set
When I am on "/array_filter_validators"
Then the response status code should be 422
And the JSON node "detail" should be equal to 'arrayRequired: This value should not be blank.\nindexedArrayRequired: This value should not be blank.'
And the JSON node "detail" should be equal to 'arrayRequired[]: This value should not be blank.\nindexedArrayRequired[foo]: This value should not be blank.'

When I am on "/array_filter_validators?arrayRequired[foo]=foo"
Then the response status code should be 422
And the JSON node "detail" should be equal to 'indexedArrayRequired: This value should not be blank.'
And the JSON node "detail" should be equal to 'indexedArrayRequired[foo]: This value should not be blank.'

When I am on "/array_filter_validators?arrayRequired[]=foo"
Then the response status code should be 422
And the JSON node "detail" should be equal to 'indexedArrayRequired: This value should not be blank.'
And the JSON node "detail" should be equal to 'indexedArrayRequired[foo]: This value should not be blank.'

Scenario: Test filter bounds: maximum
When I am on "/filter_validators?required=foo&required-allow-empty&maximum=10"
Expand All @@ -49,7 +49,7 @@ Feature: Validate filters based upon filter description

When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMaximum=10"
Then the response status code should be 422
And the JSON node "detail" should be equal to 'maximum: This value should be less than 10.'
And the JSON node "detail" should be equal to 'exclusiveMaximum: This value should be less than 10.'

Scenario: Test filter bounds: minimum
When I am on "/filter_validators?required=foo&required-allow-empty&minimum=5"
Expand Down
1 change: 0 additions & 1 deletion src/Doctrine/Odm/Extension/ParameterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public function __construct(
private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass = null, ?Operation $operation = null, array &$context = []): void
{
foreach ($operation->getParameters() ?? [] as $parameter) {
// TODO: remove the null equality as a parameter can have a null value
if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) {
continue;
}
Expand Down
1 change: 0 additions & 1 deletion src/Doctrine/Orm/Extension/ParameterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public function __construct(
private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
foreach ($operation?->getParameters() ?? [] as $parameter) {
// TODO: remove the null equality as a parameter can have a null value
if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) {
continue;
}
Expand Down
57 changes: 57 additions & 0 deletions src/JsonApi/Serializer/ErrorNormalizerTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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\JsonApi\Serializer;

use ApiPlatform\Exception\ErrorCodeSerializableInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Response;

/**
* @deprecated
*/
trait ErrorNormalizerTrait
{
private function getErrorMessage($object, array $context, bool $debug = false): string
{
$message = $object->getMessage();

if ($debug) {
return $message;
}

if ($object instanceof FlattenException) {
$statusCode = $context['statusCode'] ?? $object->getStatusCode();
if ($statusCode >= 500 && $statusCode < 600) {
$message = Response::$statusTexts[$statusCode] ?? Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR];
}
}

return $message;
}

private function getErrorCode(object $object): ?string
{
if ($object instanceof FlattenException) {
$exceptionClass = $object->getClass();
} else {
$exceptionClass = $object::class;
}

if (is_a($exceptionClass, ErrorCodeSerializableInterface::class, true)) {
return $exceptionClass::getErrorCode();
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInte

public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';

public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null)
{
public function __construct(
ResourceClassResolverInterface $resourceClassResolver,
private readonly ?PropertyMetadataFactoryInterface $decorated = null,
) {
$this->resourceClassResolver = $resourceClassResolver;
}

Expand Down Expand Up @@ -198,6 +200,8 @@ private function typeToArray(Type $type, ?bool $readableLink = null): array
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
*
* Note: if the class is not part of exceptions listed above, any class is considered as a resource.
*
* @throws PropertyNotFoundException
*/
private function getClassType(?string $className, bool $nullable, ?bool $readableLink): array
{
Expand Down Expand Up @@ -240,7 +244,8 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
];
}

if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) {
$isResourceClass = $this->isResourceClass($className);
if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) {
$enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());

$type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
Expand All @@ -255,15 +260,14 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
];
}

if (true !== $readableLink && $this->isResourceClass($className)) {
if (true !== $readableLink && $isResourceClass) {
return [
'type' => 'string',
'format' => 'iri-reference',
'example' => 'https://example.com/',
];
}

// TODO: add propertyNameCollectionFactory and recurse to find the underlying schema? Right now SchemaFactory does the job so we don't compute anything here.
return ['type' => Schema::UNKNOWN_TYPE];
}

Expand Down
8 changes: 5 additions & 3 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
$propertySchemaType = $propertySchema['type'] ?? false;

$isUnknown = Schema::UNKNOWN_TYPE === $propertySchemaType
|| ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null));
|| ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null))
|| ('object' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['additionalProperties']['type'] ?? null));

if (
!$isUnknown && (
Expand Down Expand Up @@ -231,8 +232,9 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
}

if ($isCollection) {
$propertySchema['items']['$ref'] = $subSchema['$ref'];
unset($propertySchema['items']['type']);
$key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
$propertySchema[$key]['$ref'] = $subSchema['$ref'];
unset($propertySchema[$key]['type']);
break;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Metadata/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ public function getSecurityMessage(): ?string
*
* @readonly
*/
public function getValue(): mixed
public function getValue(mixed $default = new ParameterNotFound()): mixed
{
return $this->extraProperties['_api_values'] ?? new ParameterNotFound();
return $this->extraProperties['_api_values'] ?? $default;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Metadata/Parameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function add(string $key, Parameter $value): self
/**
* @param class-string $parameterClass
*/
public function remove(string $key, string $parameterClass): self
public function remove(string $key, string $parameterClass = QueryParameter::class): self
{
foreach ($this->parameters as $i => [$parameterName, $parameter]) {
if ($parameterName === $key && $parameterClass === $parameter::class) {
Expand All @@ -86,7 +86,7 @@ public function remove(string $key, string $parameterClass): self
/**
* @param class-string $parameterClass
*/
public function get(string $key, string $parameterClass): ?Parameter
public function get(string $key, string $parameterClass = QueryParameter::class): ?Parameter
{
foreach ($this->parameters as [$parameterName, $parameter]) {
if ($parameterName === $key && $parameterClass === $parameter::class) {
Expand All @@ -100,7 +100,7 @@ public function get(string $key, string $parameterClass): ?Parameter
/**
* @param class-string $parameterClass
*/
public function has(string $key, string $parameterClass): bool
public function has(string $key, string $parameterClass = QueryParameter::class): bool
{
foreach ($this->parameters as [$parameterName, $parameter]) {
if ($parameterName === $key && $parameterClass === $parameter::class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ private function addFilterMetadata(Parameter $parameter): Parameter
return $parameter;
}

if (!\is_object($filterId) && !$this->filterLocator->has($filterId)) {
return $parameter;
}

$filter = \is_object($filterId) ? $filterId : $this->filterLocator->get($filterId);

if (!$filter) {
Expand All @@ -162,7 +166,7 @@ private function addFilterMetadata(Parameter $parameter): Parameter
$parameter = $parameter->withSchema($schema);
}

if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter)) && $openApiParameter instanceof OpenApiParameter) {
if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter))) {
$parameter = $parameter->withOpenApi($openApiParameter);
}

Expand All @@ -186,7 +190,6 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) {
$parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider');
}

$currentKey = $key;
if (null === $parameter->getProperty() && isset($properties[$key])) {
$parameter = $parameter->withProperty($key);
Expand Down
28 changes: 28 additions & 0 deletions src/Metadata/Tests/ParameterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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\Metadata\Tests;

use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\State\ParameterNotFound;
use PHPUnit\Framework\TestCase;

class ParameterTest extends TestCase
{
public function testDefaultValue(): void
{
$parameter = new QueryParameter();
$this->assertSame('test', $parameter->getValue('test'));
$this->assertInstanceOf(ParameterNotFound::class, $parameter->getValue());
}
}
28 changes: 28 additions & 0 deletions src/Metadata/Tests/ParametersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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\Metadata\Tests;

use ApiPlatform\Metadata\Parameters;
use ApiPlatform\Metadata\QueryParameter;
use PHPUnit\Framework\TestCase;

class ParametersTest extends TestCase
{
public function testDefaultValue(): void
{
$r = new QueryParameter();
$parameters = new Parameters(['a' => $r]);
$this->assertSame($r, $parameters->get('a'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

namespace ApiPlatform\Metadata\Tests\Resource\Factory;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\FilterInterface;
use ApiPlatform\Metadata\Parameters;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory;
use ApiPlatform\Metadata\Resource\Factory\ParameterResourceMetadataCollectionFactory;
Expand All @@ -25,12 +27,14 @@
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;

class ParameterResourceMetadataCollectionFactoryTests extends TestCase
class ParameterResourceMetadataCollectionFactoryTest extends TestCase
{
public function testParameterFactory(): void
{
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true));
$filterLocator = $this->createStub(ContainerInterface::class);
$filterLocator->method('has')->willReturn(true);
$filterLocator->method('get')->willReturn(new class implements FilterInterface {
Expand Down Expand Up @@ -64,6 +68,24 @@ public function getDescription(string $resourceClass): array
$this->assertEquals(['type' => 'foo'], $hydraParameter->getSchema());
$this->assertEquals(new Parameter('test', 'query'), $hydraParameter->getOpenApi());
$everywhere = $parameters->get('everywhere', QueryParameter::class);
$this->assertEquals(new Parameter('everywhere', 'query', allowEmptyValue: true), $everywhere->getOpenApi());
$this->assertNull($everywhere->getOpenApi());
}

public function testParameterFactoryNoFilter(): void
{
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true));
$filterLocator = $this->createStub(ContainerInterface::class);
$filterLocator->method('has')->willReturn(false);
$parameter = new ParameterResourceMetadataCollectionFactory(
$nameCollection,
$propertyMetadata,
new AttributesResourceMetadataCollectionFactory(),
$filterLocator
);
$operation = $parameter->create(WithParameter::class)->getOperation('collection');
$this->assertInstanceOf(Parameters::class, $parameters = $operation->getParameters());
}
}
15 changes: 9 additions & 6 deletions src/OpenApi/Factory/OpenApiFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,15 +269,18 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection

if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator && $this->filterLocator->has($f)) {
$filter = $this->filterLocator->get($f);
foreach ($filter->getDescription($entityClass) as $name => $description) {
if ($prop = $p->getProperty()) {
$name = str_replace($prop, $key, $name);

if ($d = $filter->getDescription($entityClass)) {
foreach ($d as $name => $description) {
if ($prop = $p->getProperty()) {
$name = str_replace($prop, $key, $name);
}

$openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
}

$openapiParameters[] = $this->getFilterParameter($name, $description, $operation->getShortName(), $f);
continue;
}

continue;
}

$in = $p instanceof HeaderParameterInterface ? 'header' : 'query';
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/elasticsearch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
<argument type="service" id="api_platform.elasticsearch.client" />
<argument type="service" id="serializer" />
<argument type="service" id="api_platform.pagination" />
<argument type="tagged" tag="api_platform.elasticsearch.request_body_search_extension.collection" />
<argument type="tagged_iterator" tag="api_platform.elasticsearch.request_body_search_extension.collection" />
<argument type="service" id="api_platform.inflector" on-invalid="null" />

<tag name="api_platform.state_provider" priority="-100" key="ApiPlatform\Elasticsearch\State\CollectionProvider"/>
Expand Down
Loading

0 comments on commit c59e275

Please sign in to comment.