Skip to content

Commit c81543a

Browse files
authored
Merge pull request #1749 from Nek-/feature/make-constructor-parameters-writable
Make resource constructor parameters writables
2 parents 89878de + da500b2 commit c81543a

File tree

9 files changed

+257
-4
lines changed

9 files changed

+257
-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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ 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+
$ref = new \ReflectionClass($resourceClass);
78+
if ($ref->isInstantiable() && $constructor = $ref->getConstructor()) {
79+
foreach ($constructor->getParameters() as $constructorParameter) {
80+
if ($constructorParameter->name === $name && null === $propertyMetadata->isInitializable()) {
81+
$propertyMetadata = $propertyMetadata->withInitializable(true);
82+
}
83+
}
84+
}
85+
}
86+
7187
return $propertyMetadata;
7288
}
7389
}

src/Hydra/Serializer/DocumentationNormalizer.php

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

514514
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
@@ -150,8 +150,10 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
150150

151151
if (
152152
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
153-
((isset($context['api_normalize']) && $propertyMetadata->isReadable()) ||
154-
(isset($context['api_denormalize']) && $propertyMetadata->isWritable()))
153+
(
154+
isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
155+
isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !is_object($classOrObject) && $propertyMetadata->isInitializable())
156+
)
155157
) {
156158
$allowedAttributes[] = $propertyName;
157159
}
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
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)