8000 Make resource constructor parameters writables · api-platform/core@d542e8c · GitHub
[go: up one dir, main page]

Skip to content

Commit d542e8c

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 6a8f942 commit d542e8c

File tree

9 files changed

+258
-4
lines changed

9 files changed

+258
-4
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/PropertyInfo/Metadata/Property/PropertyInfoPropertyMetadataFactory.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ public function create(string $resourceClass, string $name, array $options = [])
6868
$propertyMetadata = $propertyMetadata->withWritable($writable);
6969
}
7070

71+
if (method_exists($this->propertyInfo, 'isInitializable')) {
72+
if (null === $propertyMetadata->isInitializable() && null !== $initializable = $this->propertyInfo->isInitializable($resourceClass, $name, $options)) {
73+
$propertyMetadata = $propertyMetadata->withInitializable($initializable);
74+
}
75+
} else {
76+
// BC layer for Symfony < 4.2
77+
// To be removed in EOF of Symfony 3.4
78+
$ref = new \ReflectionClass($resourceClass);
79+
if ($ref->isInstantiable() && $constructor = $ref->getConstructor()) {
80+
foreach ($constructor->getParameters() as $constructorParameter) {
81+
if ($constructorParameter->name === $name && null === $propertyMetadata->isInitializable()) {
82+
$propertyMetadata = $propertyMetadata->withInitializable(true);
83+
}
84+
}
85+
}
86+
}
87+
7188
return $propertyMetadata;
7289
}
7390
}

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ private function getProperty(PropertyMetadata $propertyMetadata, string $propert
507507
'hydra:title' => $propertyName,
508508
'hydra:required' => $propertyMetadata->isRequired(),
509509
'hydra:readable' => $propertyMetadata->isReadable(),
510-
'hydra:writable' => $propertyMetadata->isWritable(),
510+
'hydra:writable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
511511
];
512512

513513
if (null !== $range = $this->getRange($propertyMetadata)) {

src/Metadata/Property/PropertyMetadata.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ final class PropertyMetadata
3434
private $childInherited;
3535
private $attributes;
3636
private $subresource;
37+
private $initializable;
3738

38-
public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null)
39+
public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null, bool $initializable = null)
3940
{
4041
$this->type = $type;
4142
$this->description = $description;
@@ -49,6 +50,7 @@ public function __construct(Type $type = null, string $description = null, bool
4950
$this->childInherited = $childInherited;
5051
$this->attributes = $attributes;
5152
$this->subresource = $subresource;
53+
$this->initializable = $initializable;
5254
}
5355

5456
/**
@@ -381,4 +383,29 @@ public function withSubresource(SubresourceMetadata $subresource = null): self
381383

382384
return $metadata;
383385
}
386+
387+
/**
388+
* Is initializable?
389+
*
390+
* @return bool|null
391+
*/
392+
public function isInitializable()
393+
{
394+
return $this->initializable;
395+
}
396+
397+
/**
398+
* Returns a new instance with the given initializable flag.
399+
*
400+
* @param bool $initializable
401+
*
402+
* @return self
403+
*/
404+
public function withInitializable(bool $initializable): self
405+
{
406+
$metadata = clone $this;
407+
$metadata->initializable = $initializable;
408+
409+
return $metadata;
410+
}
384411
}

src/Serializer/AbstractItemNormalizer.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,10 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
142142

143143
if (
144144
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
145-
((isset($context['api_normalize']) && $propertyMetadata->isReadable()) ||
146-
(isset($context['api_denormalize']) && $propertyMetadata->isWritable()))
145+
(
146+
isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
147+
isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !is_object($classOrObject) && $propertyMetadata->isInitializable())
148+
)
147149
) {
148150
$allowedAttributes[] = $propertyName;
149151
}
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 <dunglas@gmail.com>
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 <maxime.veber@nekland.fr>
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 <dunglas@gmail.com>
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 <maxime.veber@nekland.fr>
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 <dunglas@gmail.com>
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 <maxime.veber@nekland.fr>
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
47+
*
48+
* @ORM\Column
49+
*/
50+
private $foo;
51+
52+
/**
53+
* @var string
54+
*
55+
* @ORM\Column
56+
*/
57+
private $bar;
58+
59+
/**
60+
* @var string
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+
}

tests/Metadata/Property/PropertyMetadataTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public function testValueObject()
7878
$this->assertNotSame($metadata, $newMetadata);
7979
$this->assertEquals(['a' => 'b'], $newMetadata->getAttributes());
8080
$this->assertEquals('b', $newMetadata->getAttribute('a'));
81+
82+
$newMetadata = $metadata->withInitializable(true);
83+
$this->assertNotSame($metadata, $newMetadata);
84+
$this->assertTrue($newMetadata->isInitializable());
8185
}
8286

8387
public function testShouldReturnRequiredFalseWhenRequiredTrueIsSetButMaskedByWritableFalse()

0 commit comments

Comments
 (0)
0