Skip to content

Commit d6b1556

Browse files
committed
Make resource constructor parameters writables
This is motivated because other tools are already constructor friendly (ie Symfony serializer and Doctrine). Also using constructors must be recommended and not supporting them is a serious feature missing.
1 parent d22857d commit d6b1556

File tree

8 files changed

+425
-0
lines changed

8 files changed

+425
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Feature: Resource with constructor deserializable
2+
In order to build non anemic resource object
3+
As a developer
4+
I should be able to deserialize data into objects with constructors
5+
6+
@createSchema
7+
Scenario: post a resource built with constructor
8+
When I add "Content-Type" header equal to "application/ld+json"
9+
And I send a "POST" request to "/dummy_entity_with_constructors" with body:
10+
"""
11+
{
12+
"foo": "hello",
13+
"bar": "world"
14+
}
15+
"""
16+
Then the response status code should be 201
17+
And the response should be in JSON
18+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
And the JSON should be equal to:
20+
"""
21+
{
22+
"@context": "/contexts/DummyEntityWithConstructor",
23+
"@id": "/dummy_entity_with_constructors/1",
24+
"@type": "DummyEntityWithConstructor",
25+
"id": 1,
26+
"foo": "hello",
27+
"bar": "world",
28+
"baz": null
29+
}
30+
"""
31+

src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
<argument type="service" id="api_platform.metadata.property.metadata_factory.serializer.inner" />
7070
</service>
7171

72+
<service id="api_platform.metadata.property.metadata_factory.constructor" class="ApiPlatform\Core\Metadata\Property\Factory\ConstructorPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="35" public="false">
73+
<argument type="service" id="api_platform.operation_method_resolver" />
74+
<argument type="service" id="api_platform.metadata.property.metadata_factory.constructor.inner" />
75+
</service>
76+
7277
<service id="api_platform.metadata.property.metadata_factory.cached" class="ApiPlatform\Core\Metadata\Property\Factory\CachedPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="-10" public="false">
7378
<argument type="service" id="api_platform.cache.metadata.property" />
7479
<argument type="service" id="api_platform.metadata.property.metadata_factory.cached.inner" />
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Metadata\Property\Factory;
15+
16+
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
17+
use ApiPlatform\Core\Exception\PropertyNotFoundException;
18+
use ApiPlatform\Core\Exception\RuntimeException;
19+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
20+
21+
/**
22+
* Set properties available in the constructor as writable.
23+
*
24+
* @author Maxime Veber <[email protected]>
25+
*/
26+
final class ConstructorPropertyMetadataFactory implements PropertyMetadataFactoryInterface
27+
{
28+
private $decorated;
29+
private $operationMethodResolver;
30+
31+
public function __construct(OperationMethodResolverInterface $operationMethodResolver, PropertyMetadataFactoryInterface $decorated = null)
32+
{
33+
$this->decorated = $decorated;
34+
$this->operationMethodResolver = $operationMethodResolver;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata
41+
{
42+
if (null === $this->decorated) {
43+
$propertyMetadata = new PropertyMetadata();
44+
} else {
45+
try {
46+
$propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
47+
} catch (PropertyNotFoundException $propertyNotFoundException) {
48+
$propertyMetadata = new PropertyMetadata();
49+
}
50+
}
51+
52+
if (!isset($options['collection_operation_name'])) {
53+
return $propertyMetadata;
54+
}
55+
56+
// Constructor arguments are obviously accessible only on post operation, put will result in an error.
57+
try {
58+
$method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $options['collection_operation_name']);
59+
} catch (RuntimeException $e) {
60+
return $propertyMetadata;
61+
}
62+
63+
if ('POST' !== $method) {
64+
return $propertyMetadata;
65+
}
66+
67+
$ref = new \ReflectionClass($resourceClass);
68+
if (!$ref->isInstantiable() || !$constructor = $ref->getConstructor()) {
69+
return $propertyMetadata;
70+
}
71+
72+
foreach ($constructor->getParameters() as $constructorParameter) {
73+
if ($constructorParameter->name === $property) {
74+
return $propertyMetadata->withWritable(true);
75+
}
76+
}
77+
78+
return $propertyMetadata;
79+
}
80+
}

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ private function getPartialContainerBuilderProphecy($test = false)
506506
'api_platform.metadata.property.name_collection_factory.inherited',
507507
'api_platform.metadata.property.name_collection_factory.property_info',
508508
'api_platform.metadata.property.name_collection_factory.xml',
509+
'api_platform.metadata.property.metadata_factory.constructor',
509510
'api_platform.metadata.resource.metadata_factory.cached',
510511
'api_platform.metadata.resource.metadata_factory.operation',
511512
'api_platform.metadata.resource.metadata_factory.short_name',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures;
15+
16+
/**
17+
* @author Maxime Veber <[email protected]>
18+
*/
19+
class DummyObjectWithConstructor
20+
{
21+
private $foo;
22+
private $bar;
23+
24+
public function __construct(string $foo, \stdClass $bar)
25+
{
26+
$this->foo = $foo;
27+
$this->bar = $bar;
28+
}
29+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures;
15+
16+
/**
17+
* @author Maxime Veber <[email protected]>
18+
*/
19+
class DummyObjectWithoutConstructor
20+
{
21+
private $foo;
22+
23+
public function getFoo()
24+
{
25+
return $this->foo;
26+
}
27+
28+
public function setFoo($foo)
29+
{
30+
$this->foo = $foo;
31+
}
32+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Symfony\Component\Serializer\Annotation\Groups;
19+
20+
/**
21+
* Dummy entity built with constructor.
22+
* https://github.com/api-platform/core/issues/1747.
23+
*
24+
* @author Maxime Veber <[email protected]>
25+
*
26+
* @ApiResource(
27+
* itemOperations={
28+
* "get",
29+
* "put"={"denormalization_context"={"groups"={"put"}}}
30+
* }
31+
* )
32+
* @ORM\Entity
33+
*/
34+
class DummyEntityWithConstructor
35+
{
36+
/**
37+
* @var int The id
38+
*
39+
* @ORM\Column(type="integer")
40+
* @ORM\Id
41+
* @ORM\GeneratedValue(strategy="AUTO")
42+
*/
43+
private $id;
44+
45+
/**
46+
* @var string The tour name
47+
*
48+
* @ORM\Column
49+
*/
50+
private $foo;
51+
52+
/**
53+
* @var string The tour name
54+
*
55+
* @ORM\Column
56+
*/
57+
private $bar;
58+
59+
/**
60+
* @var string The tour name
61+
*
62+
* @ORM\Column(nullable=true)
63+
* @Groups({"put"})
64+
*/
65+
private $baz;
66+
67+
public function __construct(string $foo, string $bar)
68+
{
69+
$this->foo = $foo;
70+
$this->bar = $bar;
71+
}
72+
73+
/**
74+
* @return int
75+
*/
76+
public function getId(): int
77+
{
78+
return $this->id;
79+
}
80+
81+
/**
82+
* @return string
83+
*/
84+
public function getFoo(): string
85+
{
86+
return $this->foo;
87+
}
88+
89+
/**
90+
* @return string
91+
*/
92+
public function getBar(): string
93+
{
94+
return $this->bar;
95+
}
96+
97+
/**
98+
* @return string
99+
*/
100+
public function getBaz()
101+
{
102+
return $this->baz;
103+
}
104+
105+
/**
106+
* @param string $baz
107+
*/
108+
public function setBaz(string $baz)
109+
{
110+
$this->baz = $baz;
111+
}
112+
}

0 commit comments

Comments
 (0)