Skip to content
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
28 changes: 28 additions & 0 deletions features/main/relation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,34 @@ Feature: Relations support
}
"""

Scenario: Create a related dummy with a relation (json)
When I add "Content-Type" header equal to "application/json"
And I send a "POST" request to "/related_dummies" with body:
"""
{"thirdLevel": "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/RelatedDummy",
"@id": "/related_dummies/6",
"@type": "https://schema.org/Product",
"id": 6,
"name": null,
"symfony": "symfony",
"dummyDate": null,
"thirdLevel": "/third_levels/1",
"relatedToDummyFriend": [],
"dummyBoolean": null,
"embeddedDummy": null,
"symfony": "symfony",
"age": null
}
"""

@dropSchema
Scenario: Issue #1222
Given there are people having pets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
$container->setParameter('api_platform.formats', $formats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.allow_plain_identifiers', $config['allow_plain_identifiers']);
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function getConfigTreeBuilder()
->end()
->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
->scalarNode('path_segment_name_generator')->defaultValue('api_platform.path_segment_name_generator.underscore')->info('Specify a path name generator to use.')->end()
->booleanNode('allow_plain_identifiers')->defaultFalse()->info('Allow plain identifiers, for example "id" instead of "@id" when denormalizing a relation.')->end()
->arrayNode('validator')
->addDefaultsIfNotSet()
->children()
Expand Down
2 changes: 2 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
<argument type="service" id="api_platform.property_accessor" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
<argument type="service" id="serializer.mapping.class_metadata_factory" on-invalid="ignore" />
<argument type="service" id="api_platform.item_data_provider" on-invalid="ignore" />
<argument>%api_platform.allow_plain_identifiers%</argument>

<tag name="serializer.normalizer" />
</service>
Expand Down
18 changes: 17 additions & 1 deletion src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
Expand Down Expand Up @@ -44,8 +45,10 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
protected $resourceClassResolver;
protected $propertyAccessor;
protected $localCache = [];
protected $itemDataProvider;
protected $allowPlainIdentifiers;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null)
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false)
{
parent::__construct($classMetadataFactory, $nameConverter);

Expand All @@ -54,6 +57,8 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
$this->iriConverter = $iriConverter;
$this->resourceClassResolver = $resourceClassResolver;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->itemDataProvider = $itemDataProvider;
$this->allowPlainIdentifiers = $allowPlainIdentifiers;

$this->setCircularReferenceHandler(function ($object) {
return $this->iriConverter->getIriFromItem($object);
Expand Down Expand Up @@ -299,6 +304,17 @@ private function denormalizeRelation(string $attributeName, PropertyMetadata $pr
}

if (!is_array($value)) {
// repeat the code so that IRIs keep working with the json format

@soyuka soyuka Sep 12, 2017

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to test that Content-Type is application/json here, not sure if it's possible or wanted...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just check the $format var.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, json is the format for hal, jsonld and json no?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's jsonld for JSON-LD and jsonhal for HAL.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, my bad dunno why I thought so...

if (true === $this->allowPlainIdentifiers && $this->itemDataProvider) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is $this->itemDataProvider === null and true === $this->allowPlainIdentifiers ? Instead of silently not doing anything, should we throw an exception instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, maybe a bit overkill, this dependency should be a hard dep, it's just nullable to avoid breaking.

try {
return $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]);
} catch (ItemNotFoundException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
} catch (InvalidArgumentException $e) {
// Give a chance to other normalizers (e.g.: DateTimeNormalizer)
}
}

throw new InvalidArgumentException(sprintf(
'Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, gettype($value)
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ private function getPartialContainerBuilderProphecy()
'api_platform.exception_to_status' => [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST],
'api_platform.title' => 'title',
'api_platform.version' => 'version',
'api_platform.allow_plain_identifiers' => false,
'api_platform.eager_loading.enabled' => Argument::type('bool'),
'api_platform.eager_loading.max_joins' => 30,
'api_platform.eager_loading.force_eager' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public function testDefaultConfig()
'vary' => ['Accept'],
'public' => null,
],
'allow_plain_identifiers' => false,
], $config);
}

Expand Down
1 change: 1 addition & 0 deletions tests/Fixtures/app/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ doctrine:
api_platform:
title: 'My Dummy API'
description: 'This is a test API.'
allow_plain_identifiers: true
formats:
jsonld: ['application/ld+json']
jsonhal: ['application/hal+json']
Expand Down
99 changes: 99 additions & 0 deletions tests/Serializer/AbstractItemNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
Expand Down Expand Up @@ -644,4 +645,102 @@ public function testChildInheritedProperty()
'nickname' => null,
], $normalizer->normalize($dummy, null, ['resource_class' => DummyTableInheritance::class, 'resources' => []]));
}

public function testDenormalizeRelationWithPlainId()
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(
new PropertyNameCollection(['relatedDummy'])
)->shouldBeCalled();

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn(
new PropertyMetadata(
new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class),
'',
false,
true,
false,
false
)
)->shouldBeCalled();

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled();

$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->willImplement(DenormalizerInterface::class);

$itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class);
$itemDataProviderProphecy->getItem(RelatedDummy::class, 1, null, Argument::type('array'))->shouldBeCalled();

$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
null,
null,
$itemDataProviderProphecy->reveal(),
true,
]);
$normalizer->setSerializer($serializerProphecy->reveal());

$normalizer->denormalize(['relatedDummy' => 1], Dummy::class, 'jsonld');
}

/**
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected IRI or nested document for attribute "relatedDummy", "integer" given.
*/
public function testDoNotDenormalizeRelationWithPlainIdWhenPlainIdentifiersAreNotAllowed()
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(
new PropertyNameCollection(['relatedDummy'])
)->shouldBeCalled();

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn(
new PropertyMetadata(
new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class),
'',
false,
true,
false,
false
)
)->shouldBeCalled();

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled();

$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->willImplement(DenormalizerInterface::class);

$itemDataProviderProphecy = $this->prophesize(ItemDataProviderInterface::class);
$itemDataProviderProphecy->getItem(RelatedDummy::class, 1, null, Argument::type('array'))->shouldNotBeCalled();

$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
null,
null,
$itemDataProviderProphecy->reveal(),
false,
]);
$normalizer->setSerializer($serializerProphecy->reveal());

$normalizer->denormalize(['relatedDummy' => 1], Dummy::class, 'jsonld');
}
}