Skip to content

Commit

Permalink
improve
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Aug 9, 2023
1 parent fff97c9 commit 9edb6c7
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/Action/ExceptionAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated
* @deprecated since API Platform 3 and Error resource is used {@see ApiPlatform\Symfony\EventListener\ErrorListener}
*/
final class ExceptionAction
{
Expand Down
4 changes: 1 addition & 3 deletions src/Doctrine/Common/State/PersistProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,14 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
return $data;
}

$request = $context['request'] ?? null;

// PUT: reset the existing object managed by Doctrine and merge data sent by the user in it
// This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported:
// https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555
if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false)) {
\assert(method_exists($manager, 'getReference'));
// TODO: the call to getReference is most likely to fail with complex identifiers
$newData = $data;
if ($previousData = $context['previous_data'] ?? $request?->attributes->get('previous_data')) {
if ($previousData = $context['previous_data']) {
$newData = 1 === \count($uriVariables) ? $manager->getReference($class, current($uriVariables)) : clone $previousData;
}

Expand Down
69 changes: 42 additions & 27 deletions src/Documentation/Action/DocumentationAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use ApiPlatform\Util\ContentNegotiationTrait;
use Negotiation\Negotiator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* Generates the API documentation.
Expand All @@ -49,45 +50,59 @@ public function __construct(
}

/**
* @return DocumentationInterface|OpenApi
* @return DocumentationInterface|OpenApi|Response
*/
public function __invoke(Request $request = null)
{
$context = [];
if (null !== $request) {
$isGateway = $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY);
$context['api_gateway'] = $isGateway;
$context['base_url'] = $request->getBaseUrl();
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
$format = $this->getRequestFormat($request, ['json' => ['application/json'], 'jsonld' => ['application/ld+json'], 'html' => ['text/html']]);

if ('html' === $format || 'json' === $format && null !== $this->openApiFactory) {
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(class: OpenApi::class, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $isGateway]);
if ('html' === $format) {
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
}

$body = $this->provider->provide($operation, [], $context);

return $this->processor->process($body, $operation, [], $context);
}

return $this->openApiFactory->__invoke($context);
if (null === $request) {
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
}

$context = ['api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), 'base_url' => $request->getBaseUrl()];
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
$format = $this->getRequestFormat($request, ['json' => ['application/json'], 'jsonld' => ['application/ld+json'], 'html' => ['text/html']]);

if (null !== $this->openApiFactory && ('html' === $format || 'json' === $format)) {
return $this->getOpenApiDocumentation($context, $format, $request);
}

return $this->getHydraDocumentation($context, $request);
}

/**
* @param array<string,mixed> $context
*/
private function getOpenApiDocumentation(array $context, string $format, Request $request): OpenApi|Response
{
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(class: OpenApi::class, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null]);
if ('html' === $format) {
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
}

return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context);
}

return $this->openApiFactory->__invoke($context);
}

/**
* TODO: the logic behind the Hydra Documentation is done in a ApiPlatform\Hydra\Serializer\DocumentationNormalizer.
* We should transform this to a provider, it'd improve performances also by a bit.
*
* @param array<string,mixed> $context
*/
private function getHydraDocumentation(array $context, Request $request): DocumentationInterface|Response
{
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(
class: Documentation::class,
provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version),
normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $isGateway ?? false]
provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version)
);
$body = $this->provider->provide($operation, [], $context);

return $this->processor->process($body, $operation, [], $context);
return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context);
}

return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/EventListener/ErrorListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
/**
* This error listener extends the Symfony one in order to add
* the `_api_operation` attribute when the request is duplicated.
* It will later be used to retrieve the exceptionToStatus from the operation ({@see ExceptionAction}).
* It will later be used to retrieve the exceptionToStatus from the operation ({@see ApiPlatform\Action\ExceptionAction}).
*/
final class ErrorListener extends SymfonyErrorListener
{
Expand Down
15 changes: 15 additions & 0 deletions tests/Action/EntrypointActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use ApiPlatform\Api\Entrypoint;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;

Expand All @@ -34,4 +36,17 @@ public function testGetEntrypoint(): void
$entrypoint = new EntrypointAction($resourceNameCollectionFactoryProphecy->reveal());
$this->assertEquals(new Entrypoint(new ResourceNameCollection(['dummies'])), $entrypoint());
}

public function testGetEntrypointWithProviderProcessor(): void
{
$expected = new Entrypoint(new ResourceNameCollection(['dummies']));
$resourceNameCollectionFactory = $this->createStub(ResourceNameCollectionFactoryInterface::class);
$resourceNameCollectionFactory->method('create')->willReturn(new ResourceNameCollection(['dummies']));
$provider = $this->createMock(ProviderInterface::class);
$provider->expects($this->once())->method('provide')->willReturn($expected);
$processor = $this->createMock(ProcessorInterface::class);
$processor->expects($this->once())->method('process')->willReturnArgument(0);
$entrypoint = new EntrypointAction($resourceNameCollectionFactory, $provider, $processor);
$this->assertEquals($expected, $entrypoint());
}
}
2 changes: 2 additions & 0 deletions tests/Doctrine/Common/State/PersistProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,6 @@ public function testTrackingPolicy(string $metadataClass, bool $deferredExplicit
$result = (new PersistProcessor($managerRegistryProphecy->reveal()))->process($dummy, new Get());
$this->assertSame($dummy, $result);
}

public function z
}

Check failure on line 123 in tests/Doctrine/Common/State/PersistProcessorTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Syntax error, unexpected '}', expecting '(' on line 123
37 changes: 37 additions & 0 deletions tests/Documentation/Action/DocumentationActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use ApiPlatform\OpenApi\Model\Info;
use ApiPlatform\OpenApi\Model\Paths;
use ApiPlatform\OpenApi\OpenApi;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
Expand Down Expand Up @@ -81,4 +83,39 @@ public function testDocumentationActionWithoutOpenApiFactory(): void
$documentation = new DocumentationAction($resourceNameCollectionFactoryProphecy->reveal(), 'my api', '', '1.0.0');
$this->assertInstanceOf(Documentation::class, $documentation($requestProphecy->reveal()));
}

public static function getOpenApiContentTypes(): array
{
return [['application/json'], ['application/html']];
}

/**
* @dataProvider getOpenApiContentTypes
*/
public function testGetOpenApi($contentType): void
{
$request = new Request(server: ['CONTENT_TYPE' => $contentType]);
$openApiFactory = $this->createMock(OpenApiFactoryInterface::class);
$openApiFactory->expects($this->once())->method('__invoke')->willReturn(new OpenApi(new Info('a', 'v'), [], new Paths()));
$resourceNameCollectionFactory = $this->createStub(ResourceNameCollectionFactoryInterface::class);
$provider = $this->createMock(ProviderInterface::class);
$provider->expects($this->once())->method('provide')->willReturnCallback(fn ($operation, $uriVariables, $context) => $operation->getProvider()(...\func_get_args()));
$processor = $this->createMock(ProcessorInterface::class);
$processor->expects($this->once())->method('process')->willReturnArgument(0);
$entrypoint = new DocumentationAction($resourceNameCollectionFactory, provider: $provider, processor: $processor, openApiFactory: $openApiFactory);
$entrypoint($request);
}

public function testGetHydraDocumentation(): void
{
$request = new Request();
$resourceNameCollectionFactory = $this->createStub(ResourceNameCollectionFactoryInterface::class);
$resourceNameCollectionFactory->expects($this->once())->method('create')->willReturn(new ResourceNameCollection([]));

Check failure on line 113 in tests/Documentation/Action/DocumentationActionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Call to an undefined method ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface&PHPUnit\Framework\MockObject\Stub::expects().
$provider = $this->createMock(ProviderInterface::class);
$provider->expects($this->once())->method('provide')->willReturnCallback(fn ($operation, $uriVariables, $context) => $operation->getProvider()(...\func_get_args()));
$processor = $this->createMock(ProcessorInterface::class);
$processor->expects($this->once())->method('process')->willReturnArgument(0);
$entrypoint = new DocumentationAction($resourceNameCollectionFactory, provider: $provider, processor: $processor);
$entrypoint($request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ public function __construct(private readonly ManagerRegistry $registry)
/**
* {@inheritDoc}
*
* @param InputDto $data
* @param InputDto|InputDtoDocument|mixed $data
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (!$data instanceof InputDto && !$data instanceof InputDtoDocument) {
if (!($data instanceof InputDto || $data instanceof InputDtoDocument)) {
throw new \RuntimeException('Data is not an InputDto');
}

Expand Down

0 comments on commit 9edb6c7

Please sign in to comment.