Skip to content

Commit

Permalink
more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jan 15, 2024
1 parent 5732cb1 commit ab5d474
Show file tree
Hide file tree
Showing 16 changed files with 1,025 additions and 118 deletions.
4 changes: 1 addition & 3 deletions src/Symfony/Bundle/Resources/config/symfony/events.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

<service id="api_platform.listener.request.deserialize" class="ApiPlatform\Symfony\EventListener\DeserializeListener">
<argument type="service" id="api_platform.state_provider.deserialize" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />

<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="2" />
</service>
Expand All @@ -68,16 +69,13 @@

<service id="api_platform.listener.view.write" class="ApiPlatform\Symfony\EventListener\WriteListener">
<argument type="service" id="api_platform.state_processor.write" />
<argument>null</argument>
<argument>null</argument>
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />

<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="32" />
</service>

<service id="api_platform.listener.view.serialize" class="ApiPlatform\Symfony\EventListener\SerializeListener">
<argument type="service" id="api_platform.state_processor.serialize" />
<argument>null</argument>
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />

<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="16" />
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/EventListener/AddFormatListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ public function onKernelRequest(RequestEvent $event): void
return;
}

if (!($request->attributes->has('_api_resource_class')
if (!(
$request->attributes->has('_api_resource_class')
|| $request->attributes->getBoolean('_api_respond', false)
|| $request->attributes->getBoolean('_graphql', false)
)) {
Expand Down
11 changes: 7 additions & 4 deletions src/Symfony/EventListener/DeserializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ final class DeserializeListener
private SerializerInterface $serializer;
private ?ProviderInterface $provider = null;

/**
* @param ProviderInterface|SerializerInterface $serializer
*/
public function __construct($serializer, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null)
public function __construct(ProviderInterface|SerializerInterface $serializer, private readonly null|SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null)
{
if ($serializer instanceof ProviderInterface) {
$this->provider = $serializer;
Expand All @@ -62,6 +59,12 @@ public function __construct($serializer, private readonly ?SerializerContextBuil
$this->serializer = $serializer;
}

if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) {
$resourceMetadataFactory = $serializerContextBuilder;
} else {
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class);
}

$this->resourceMetadataCollectionFactory = $resourceMetadataFactory;
if (null === $this->translator) {
$this->translator = new class() implements TranslatorInterface, LocaleAwareInterface {
Expand Down
95 changes: 56 additions & 39 deletions src/Symfony/EventListener/ErrorListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,45 +102,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
}

$dup = parent::duplicateRequest($exception, $request);
if ($this->resourceMetadataCollectionFactory) {
if ($this->resourceClassResolver?->isResourceClass($exception::class)) {
$resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);

$operation = null;
foreach ($resourceCollection as $resource) {
foreach ($resource->getOperations() as $op) {
foreach ($op->getOutputFormats() as $key => $value) {
if ($key === $format) {
$operation = $op;
break 3;
}
}
}
}

// No operation found for the requested format, we take the first available
if (!$operation) {
$operation = $resourceCollection->getOperation();
}
if ($exception instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
$statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception);
$operation = $operation->withStatus($statusCode);
}
} else {
// Create a generic, rfc7807 compatible error according to the wanted format
$operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
// status code may be overriden by the exceptionToStatus option
$statusCode = 500;
if ($operation instanceof HttpOperation) {
$statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception);
$operation = $operation->withStatus($statusCode);
}
}
} else {
/** @var HttpOperation $operation */
$operation = new ErrorOperation(name: '_api_errors_problem', class: Error::class, outputFormats: ['jsonld' => ['application/problem+json']], normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]);
$operation = $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
}
$operation = $this->initializeExceptionOperation($request, $exception, $format, $apiOperation);

if (null === $operation->getProvider()) {
$operation = $operation->withProvider('api_platform.state.error_provider');
Expand Down Expand Up @@ -171,6 +133,9 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
return $dup;
}

/**
* @return array<int, array<class-string, int>>
*/
private function getOperationExceptionToStatus(Request $request): array
{
$attributes = RequestAttributesExtractor::extractAttributes($request);
Expand Down Expand Up @@ -244,4 +209,56 @@ private function getFormatOperation(?string $format): string
default => '_api_errors_problem'
};
}

private function initializeExceptionOperation(?Request $request, \Throwable $exception, string $format, ?HttpOperation $apiOperation): HttpOperation
{
if (!$this->resourceMetadataCollectionFactory) {
/** @var HttpOperation $operation */
$operation = new ErrorOperation(
name: '_api_errors_problem',
class: Error::class,
outputFormats: ['jsonld' => ['application/problem+json']],
normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]
);

return $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
}

if ($this->resourceClassResolver?->isResourceClass($exception::class)) {
$resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);

$operation = null;
// TODO: move this to ResourceMetadataCollection?
foreach ($resourceCollection as $resource) {
foreach ($resource->getOperations() as $op) {
foreach ($op->getOutputFormats() as $key => $value) {
if ($key === $format) {
$operation = $op;
break 3;
}
}
}
}

// No operation found for the requested format, we take the first available
$operation ??= $resourceCollection->getOperation();

if ($exception instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
return $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
}

return $operation;
}

// Create a generic, rfc7807 compatible error according to the wanted format
$operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
// status code may be overriden by the exceptionToStatus option
$statusCode = 500;
if ($operation instanceof HttpOperation) {
$statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception);
$operation = $operation->withStatus($statusCode);
}

return $operation;

Check failure on line 262 in src/Symfony/EventListener/ErrorListener.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.3)

Method ApiPlatform\Symfony\EventListener\ErrorListener::initializeExceptionOperation() should return ApiPlatform\Metadata\HttpOperation but returns ApiPlatform\Metadata\Operation.
}
}
12 changes: 10 additions & 2 deletions src/Symfony/EventListener/SerializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ final class SerializeListener
public const OPERATION_ATTRIBUTE_KEY = 'serialize';
private ?SerializerInterface $serializer = null;
private ?ProcessorInterface $processor = null;
private ?SerializerContextBuilderInterface $serializerContextBuilder = null;

public function __construct(
$serializer,
private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null,
SerializerInterface|ProcessorInterface $serializer,
SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface $serializerContextBuilder = null,
ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
private readonly array $errorFormats = [],
// @phpstan-ignore-next-line we don't need this anymore
Expand All @@ -62,6 +63,13 @@ public function __construct(
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProcessorInterface::class, self::class, SerializerInterface::class);
}

if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) {
$resourceMetadataFactory = $serializerContextBuilder;
} else {
$this->serializerContextBuilder = $serializerContextBuilder;
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class);
}

$this->resourceMetadataCollectionFactory = $resourceMetadataFactory;
}

Expand Down
9 changes: 3 additions & 6 deletions src/Symfony/EventListener/ValidateListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,10 @@ final class ValidateListener

public const OPERATION_ATTRIBUTE_KEY = 'validate';

private ?ValidatorInterface $validator = null;
private ?ProviderInterface $provider = null;
private ValidatorInterface $validator;
private ProviderInterface $provider;

/**
* @param ProviderInterface|ValidatorInterface $validator
*/
public function __construct($validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory)
public function __construct(ProviderInterface|ValidatorInterface $validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory)
{
if ($validator instanceof ProviderInterface) {
$this->provider = $validator;
Expand Down
27 changes: 24 additions & 3 deletions src/Symfony/EventListener/WriteListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface;
use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface;
use ApiPlatform\Exception\InvalidIdentifierException;
use ApiPlatform\Metadata\Error;
use ApiPlatform\Metadata\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
Expand Down Expand Up @@ -46,22 +49,32 @@ final class WriteListener
use OperationRequestInitiatorTrait;
use UriVariablesResolverTrait;

private LegacyIriConverterInterface|IriConverterInterface|null $iriConverter;

/**
* @param ProcessorInterface<mixed> $processor
*/
public function __construct(
private readonly ProcessorInterface $processor,
private readonly null|LegacyIriConverterInterface|IriConverterInterface $iriConverter = null,
LegacyIriConverterInterface|IriConverterInterface|ResourceMetadataCollectionFactoryInterface $iriConverter = null,
private readonly null|ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver = null,
ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
LegacyUriVariablesConverterInterface|UriVariablesConverterInterface $uriVariablesConverter = null,
) {
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->uriVariablesConverter = $uriVariablesConverter;

if ($processor instanceof CallableProcessor) {
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', WriteProcessor::class, self::class, $processor::class);
}

if ($iriConverter instanceof ResourceMetadataCollectionFactoryInterface) {
$resourceMetadataCollectionFactory = $iriConverter;
} else {
$this->iriConverter = $iriConverter;
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, IriConverterInterface::class);
}

$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}

/**
Expand All @@ -77,12 +90,20 @@ public function onKernelView(ViewEvent $event): void
return;
}

if ($operation && $this->processor instanceof WriteProcessor) {
if ($operation && !$this->processor instanceof CallableProcessor) {
if (null === $operation->canWrite()) {
$operation = $operation->withWrite(!$request->isMethodSafe());
}

$uriVariables = $request->attributes->get('_api_uri_variables') ?? [];
if (!$uriVariables && !$operation instanceof Error && $operation instanceof HttpOperation) {
try {
$uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass());
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
}
}

$request->attributes->set('original_data', $this->clone($controllerResult));
$data = $this->processor->process($controllerResult, $operation, $uriVariables, [
'request' => $request,
Expand Down
21 changes: 15 additions & 6 deletions src/Symfony/Tests/EventListener/AddFormatListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ public function testFetchOperation(): void

$listener = new AddFormatListener($provider, $metadata);
$listener->onKernelRequest(
new RequestEvent($this->createStub(HttpKernelInterface::class),
new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']), HttpKernelInterface::MAIN_REQUEST)
new RequestEvent(
$this->createStub(HttpKernelInterface::class),
new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']),
HttpKernelInterface::MAIN_REQUEST
)
);
}

Expand All @@ -53,8 +56,11 @@ public function testCallProvider(): void

$listener = new AddFormatListener($provider, $metadata);
$listener->onKernelRequest(
new RequestEvent($this->createStub(HttpKernelInterface::class),
new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']), HttpKernelInterface::MAIN_REQUEST)
new RequestEvent(
$this->createStub(HttpKernelInterface::class),
new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']),
HttpKernelInterface::MAIN_REQUEST
)
);
}

Expand All @@ -66,8 +72,11 @@ public function testNoCallProvider(...$attributes): void
$metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
$listener = new AddFormatListener($provider, $metadata);
$listener->onKernelRequest(
new RequestEvent($this->createStub(HttpKernelInterface::class),
new Request([], [], $attributes), HttpKernelInterface::MAIN_REQUEST)
new RequestEvent(
$this->createStub(HttpKernelInterface::class),
new Request([], [], $attributes),
HttpKernelInterface::MAIN_REQUEST
)
);
}

Expand Down
Loading

0 comments on commit ab5d474

Please sign in to comment.