Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow an input and an output for a given resource class #2235

Merged
merged 1 commit into from
Nov 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Annotation/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@
* @Attribute("filters", type="string[]"),
* @Attribute("graphql", type="array"),
* @Attribute("hydraContext", type="array"),
* @Attribute("inputClass", type="string"),
* @Attribute("iri", type="string"),
* @Attribute("itemOperations", type="array"),
* @Attribute("maximumItemsPerPage", type="int"),
* @Attribute("normalizationContext", type="array"),
* @Attribute("order", type="array"),
* @Attribute("outputClass", type="string"),
* @Attribute("paginationClientEnabled", type="bool"),
* @Attribute("paginationClientItemsPerPage", type="bool"),
* @Attribute("paginationClientPartial", type="bool"),
Expand Down Expand Up @@ -264,6 +266,20 @@ final class ApiResource
*/
private $sunset;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var string
*/
private $inputClass;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var string
*/
private $outputClass;

/**
* @throws InvalidArgumentException
*/
Expand Down
4 changes: 4 additions & 0 deletions src/Bridge/Symfony/Routing/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ public function load($data, $type = null): RouteCollection
'_controller' => $controller,
'_format' => null,
'_api_resource_class' => $operation['resource_class'],
'_api_input_class' => $operation['input_class'],
'_api_output_class' => $operation['output_class'],
'_api_subresource_operation_name' => $operation['route_name'],
'_api_subresource_context' => [
'property' => $operation['property'],
Expand Down Expand Up @@ -210,6 +212,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
'_controller' => $controller,
'_format' => null,
'_api_resource_class' => $resourceClass,
'_api_input_class' => $resourceMetadata->getAttribute('input_class', $resourceClass),
'_api_output_class' => $resourceMetadata->getAttribute('output_class', $resourceClass),
sprintf('_api_%s_operation_name', $operationType) => $operationName,
] + ($operation['defaults'] ?? []),
$operation['requirements'] ?? [],
Expand Down
8 changes: 4 additions & 4 deletions src/DataProvider/OperationDataProviderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ trait OperationDataProviderTrait
*/
private function getCollectionData(array $attributes, array $context)
{
return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name'], $context);
return $this->collectionDataProvider->getCollection($attributes['output_class'], $attributes['collection_operation_name'], $context);
}

/**
Expand All @@ -59,7 +59,7 @@ private function getCollectionData(array $attributes, array $context)
*/
private function getItemData($identifiers, array $attributes, array $context)
{
return $this->itemDataProvider->getItem($attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context);
return $this->itemDataProvider->getItem($attributes['output_class'], $identifiers, $attributes['item_operation_name'], $context);
}

/**
Expand All @@ -75,7 +75,7 @@ private function getSubresourceData($identifiers, array $attributes, array $cont
throw new RuntimeException('Subresources not supported');
}

return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
return $this->subresourceDataProvider->getSubresource($attributes['output_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
}

/**
Expand All @@ -93,7 +93,7 @@ private function extractIdentifiers(array $parameters, array $attributes)
$id = $parameters['id'];

if (null !== $this->identifierConverter) {
return $this->identifierConverter->convert((string) $id, $attributes['resource_class']);
return $this->identifierConverter->convert((string) $id, $attributes['output_class']);
}

return $id;
Expand Down
5 changes: 4 additions & 1 deletion src/EventListener/DeserializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public function onKernelRequest(GetResponseEvent $event)

$format = $this->getFormat($request);
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
if (isset($context['input_class'])) {
$context['resource_class'] = $context['input_class'];
}

$data = $request->attributes->get('data');
if (null !== $data) {
Expand All @@ -90,7 +93,7 @@ public function onKernelRequest(GetResponseEvent $event)
$request->attributes->set(
'data',
$this->serializer->deserialize(
$requestContent, $attributes['resource_class'], $format, $context
$requestContent, $attributes['input_class'], $format, $context
)
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/EventListener/SerializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
}
$resources = new ResourceList();
$context['resources'] = &$resources;
if (isset($context['output_class'])) {
$context['resource_class'] = $context['output_class'];
}

$request->attributes->set('_api_normalization_context', $context);

$event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $context));
Expand Down
5 changes: 4 additions & 1 deletion src/EventListener/WriteListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)

$event->setControllerResult($persistResult ?? $controllerResult);

if (null !== $this->iriConverter) {
// Controller result must be immutable for _api_write_item_iri
// if it's class changed compared to the base class let's avoid calling the IriConverter
// especially that the Output class could be a DTO that's not referencing any route
if (null !== $this->iriConverter && \get_class($controllerResult) === \get_class($event->getControllerResult())) {
soyuka marked this conversation as resolved.
Show resolved Hide resolved
$request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
soyuka marked this conversation as resolved.
Show resolved Hide resolved
}
break;
Expand Down
2 changes: 2 additions & 0 deletions src/Operation/Factory/SubresourceOperationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
'collection' => $subresource->isCollection(),
'resource_class' => $subresourceClass,
'shortNames' => [$subresourceMetadata->getShortName()],
'input_class' => $subresourceMetadata->getAttribute('input_class', $subresourceClass),
'output_class' => $subresourceMetadata->getAttribute('output_class', $subresourceClass),
];

if (null === $parentOperation) {
Expand Down
2 changes: 2 additions & 0 deletions src/Serializer/SerializerContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public function createFromRequest(Request $request, bool $normalization, array $
}

$context['resource_class'] = $attributes['resource_class'];
$context['input_class'] = $attributes['input_class'] ?? $attributes['resource_class'];
$context['output_class'] = $attributes['output_class'] ?? $attributes['resource_class'];
$context['request_uri'] = $request->getRequestUri();
$context['uri'] = $request->getUri();

Expand Down
2 changes: 2 additions & 0 deletions src/Util/AttributesExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ private function __construct()
public static function extractAttributes(array $attributes): array
{
$result = ['resource_class' => $attributes['_api_resource_class'] ?? null];
$result['input_class'] = $attributes['_api_input_class'] ?? $result['resource_class'];
$result['output_class'] = $attributes['_api_output_class'] ?? $result['resource_class'];

if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) {
$result['subresource_context'] = $subresourceContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public function testWithResource()
$this->response
);

$this->assertSame(['resource_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes());
$this->assertSame(['resource_class' => DummyEntity::class, 'input_class' => DummyEntity::class, 'output_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes());
$this->assertSame(['foo', 'bar'], $dataCollector->getAcceptableContentTypes());
$this->assertSame(DummyEntity::class, $dataCollector->getResourceClass());
$this->assertSame(['foo' => null, 'a_filter' => \stdClass::class], $dataCollector->getFilters());
Expand Down
4 changes: 4 additions & 0 deletions tests/Bridge/Symfony/Routing/ApiLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ private function getRoute(string $path, string $controller, string $resourceClas
'_controller' => $controller,
'_format' => null,
'_api_resource_class' => $resourceClass,
'_api_input_class' => $resourceClass,
'_api_output_class' => $resourceClass,
sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName,
] + $extraDefaults,
$requirements,
Expand All @@ -324,6 +326,8 @@ private function getSubresourceRoute(string $path, string $controller, string $r
'_controller' => $controller,
'_format' => null,
'_api_resource_class' => $resourceClass,
'_api_input_class' => $resourceClass,
'_api_output_class' => $resourceClass,
'_api_subresource_operation_name' => $operationName,
'_api_subresource_context' => $context,
],
Expand Down
2 changes: 1 addition & 1 deletion tests/EventListener/AddFormatListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public function testResourceClassSupportedRequestFormat()
$event = $eventProphecy->reveal();

$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled();
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_class' => 'Foo', 'output_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled();

$listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal());
$listener->onKernelRequest($event);
Expand Down
2 changes: 1 addition & 1 deletion tests/EventListener/DeserializeListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public function testDeserializeResourceClassSupportedFormat(string $method, bool
$serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled();

$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled();
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_class' => 'Foo', 'output_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled();

$listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\Core\Tests\Fixtures\TestBundle\DataPersister;

use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyInput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOutput;

class DummyInputDataPersister implements DataPersisterInterface
{
public function supports($data): bool
{
return $data instanceof DummyInput;
}

/**
* {@inheritdoc}
*/
public function persist($data)
{
$output = new DummyOutput();
$output->name = $data->name;
$output->id = 1;

return $output;
}

/**
* {@inheritdoc}
*/
public function remove($data)
{
return null;
}
}
40 changes: 40 additions & 0 deletions tests/Fixtures/TestBundle/Entity/DummyInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Core\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;

/**
* Dummy Input.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @ApiResource
*/
class DummyInput
{
/**
* @var int The id
* @ApiProperty(identifier=true)
*/
public $id;

/**
* @var string The dummy name
*
* @ApiProperty
*/
public $name;
}
33 changes: 33 additions & 0 deletions tests/Fixtures/TestBundle/Entity/DummyInputOutput.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\Core\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;

/**
* Dummy InputOutput.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @ApiResource(attributes={"input_class"=DummyInput::class, "output_class"=DummyOutput::class})
*/
class DummyInputOutput
{
/**
* @var int The id
* @ApiProperty(identifier=true)
*/
public $id;
}
46 changes: 46 additions & 0 deletions tests/Fixtures/TestBundle/Entity/DummyOutput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\Core\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
* Dummy Output.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @ApiResource
* @ORM\Entity
*/
class DummyOutput
{
/**
* @var int The id
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;

/**
* @var string The dummy name
*
* @ORM\Column
* @ApiProperty(iri="http://schema.org/name")
*/
public $name;
}
6 changes: 6 additions & 0 deletions tests/Fixtures/app/config/config_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,9 @@ services:
app.dummy_validation.group_generator:
class: ApiPlatform\Core\Tests\Fixtures\TestBundle\Validator\DummyValidationGroupsGenerator
public: true

app.dummy_input_data_persister:
class: ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister\DummyInputDataPersister
public: false
tags:
- { name: 'api_platform.data_persister' }
Loading