Skip to content

Commit

Permalink
Improve Input/Output with normalizers
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Feb 5, 2019
1 parent 1a9335e commit 89dfb20
Show file tree
Hide file tree
Showing 25 changed files with 745 additions and 152 deletions.
31 changes: 31 additions & 0 deletions features/bootstrap/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDate;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
Expand Down Expand Up @@ -1182,6 +1183,36 @@ public function thereIsAMaxDepthDummyWithLevelOfDescendants(int $level)
$this->manager->flush();
}

/**
* @Given there is a DummyCustomDto
*/
public function thereIsADummyCustomDto()
{
$dto = new DummyDtoCustom();
$dto->lorem = 'test';
$dto->ipsum = '0';
$this->manager->persist($dto);

$this->manager->flush();
$this->manager->clear();
}

/**
* @Given there are :nb DummyCustomDto
*/
public function thereAreNbDummyCustomDto($nb)
{
for ($i = 1; $i <= $nb; ++$i) {
$dto = new DummyDtoCustom();
$dto->lorem = 'test';
$dto->ipsum = (string) $i;
$this->manager->persist($dto);
}

$this->manager->flush();
$this->manager->clear();
}

private function isOrm(): bool
{
return null !== $this->schemaTool;
Expand Down
181 changes: 181 additions & 0 deletions features/main/dto.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
Feature: DTO input and output
In order to use an hypermedia API
As a client software developer
I need to be able to use DTOs on my resources as Input or Output objects.

@createSchema
Scenario: Create a resource with a custom Input.
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummy_dto_customs" with body:
"""
{
"foo": "test",
"bar": 1
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/DummyDtoCustom",
"@id": "/dummy_dto_customs/1",
"@type": "DummyDtoCustom",
"lorem": "test",
"ipsum": "1",
"id": 1
}
"""

@createSchema
Scenario: Get an item with a custom output
Given there is a DummyCustomDto
When I send a "GET" request to "/dummy_dto_custom_output/1"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be a superset of:
"""
{
"@context": "/contexts/CustomOutputDto",
"@type": "CustomOutputDto",
"foo": "test",
"bar": 0
}
"""

@createSchema
Scenario: Get a collection with a custom output
Given there are 2 DummyCustomDto
When I send a "GET" request to "/dummy_dto_custom_output"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be a superset of:
"""
{
"@context": "/contexts/DummyDtoCustom",
"@id": "/dummy_dto_customs",
"@type": "hydra:Collection",
"hydra:member": [
{
"@type": "CustomOutputDto",
"foo": "test",
"bar": 1
},
{
"@type": "CustomOutputDto",
"foo": "test",
"bar": 2
}
],
"hydra:totalItems": 2
}
"""

@createSchema
Scenario: Create a DummyCustomDto object without output
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummy_dto_custom_post_without_output" with body:
"""
{
"lorem": "test",
"ipsum": "1"
}
"""
Then the response status code should be 201
And the response should be empty

@createSchema
Scenario: Create and update a DummyInputOutput
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummy_dto_input_outputs" with body:
"""
{
"foo": "test",
"bar": 1
}
"""
Then the response status code should be 201
And the JSON should be a superset of:
"""
{
"@context": "/contexts/OutputDto",
"@type": "OutputDto",
"baz": 1,
"bat": "test"
}
"""
Then I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/dummy_dto_input_outputs/1" with body:
"""
{
"foo": "test",
"bar": 2
}
"""
Then the response status code should be 200
And the JSON should be a superset of:
"""
{
"@context": "/contexts/OutputDto",
"@type": "OutputDto",
"baz": 2,
"bat": "test"
}
"""

@createSchema
Scenario: Use DTO with relations on User
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/users" with body:
"""
{
"username": "soyuka",
"plainPassword": "a real password",
"email": "soyuka@example.com"
}
"""
Then the response status code should be 201
Then I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/users/recover/1" with body:
"""
{
"user": "/users/1"
}
"""
Then the response status code should be 200
And the JSON should be a superset of:
"""
{
"@context": "/contexts/RecoverPasswordOutput",
"@type": "RecoverPasswordOutput",
"user": {
"@id": "/users/1",
"@type": "User",
"email": "soyuka@example.com",
"fullname": null,
"username": "soyuka"
}
}
"""

# @createSchema
# Scenario: Execute a GraphQL query on DTO
# Given there are 2 DummyCustomDto
# When I send the following GraphQL request:
# """
# {
# dummyDtoCustom(id: "/dummy_dto_customs/1") {
# lorem
# ipsum
# }
# }
# """
# Then the response status code should be 200
# And the response should be in JSON
# And the header "Content-Type" should be equal to "application/json"
# Then print last JSON response
# And the JSON node "data.dummy.id" should be equal to "/dummies/1"
# And the JSON node "data.dummy.name" should be equal to "Dummy #1"
2 changes: 1 addition & 1 deletion src/Api/ResourceClassResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
return $type;
}

throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $type));
return $type;
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/Bridge/Symfony/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ public function getItemFromIri(string $iri, array $context = [])
public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
{
$resourceClass = $this->getObjectClass($item);
$routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM);

try {
$routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM);
} catch (InvalidArgumentException $e) {
return '_:'.md5(serialize($item));
}

try {
$identifiers = $this->generateIdentifiersUrl($this->identifiersExtractor->getIdentifiersFromItem($item), $resourceClass);
Expand Down
4 changes: 2 additions & 2 deletions src/EventListener/DeserializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function onKernelRequest(GetResponseEvent $event)
}

$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
if (false === $context['input_class']) {
if (false === ($context['input_class'] ?? null)) {
return;
}

Expand All @@ -97,7 +97,7 @@ public function onKernelRequest(GetResponseEvent $event)
$request->attributes->set(
'data',
$this->serializer->deserialize(
$requestContent, $context['input_class'], $format, $context
$requestContent, $context['resource_class'], $format, $context
)
);
}
Expand Down
11 changes: 3 additions & 8 deletions src/EventListener/SerializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
$request->attributes->set('_api_respond', true);
$context = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);

if (isset($context['output_class'])) {
if (false === $context['output_class']) {
// If the output class is explicitly set to false, the response must be empty
$event->setControllerResult('');
if (false === ($context['output_class'] ?? null)) {
$event->setControllerResult('');

return;
}

$context['resource_class'] = $context['output_class'];
return;
}

if ($included = $request->attributes->get('_api_included')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ private function handleNotFound(ResourceMetadata $parentPropertyMetadata = null,
return $parentPropertyMetadata;
}

if (false !== $pos = strrpos($resourceClass, '\\')) {
return new ResourceMetadata(substr($resourceClass, $pos + 1));
}

throw new ResourceClassNotFoundException(sprintf('Resource "%s" not found.', $resourceClass));
}

Expand Down
4 changes: 2 additions & 2 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function supportsNormalization($data, $format = null)
return false;
}

return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
return true;
}

/**
Expand Down Expand Up @@ -119,7 +119,7 @@ public function normalize($object, $format = null, array $context = [])
*/
public function supportsDenormalization($data, $type, $format = null)
{
return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
return true;
}

/**
Expand Down

This file was deleted.

27 changes: 27 additions & 0 deletions tests/Fixtures/TestBundle/Dto/CustomInputDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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\Dto;

class CustomInputDto
{
/**
* @var string
*/
public $foo;

/**
* @var int
*/
public $bar;
}
Loading

0 comments on commit 89dfb20

Please sign in to comment.