Skip to content

Commit ee9125a

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 9ea5d37 commit ee9125a

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
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.metadata.property.metadata_factory.constructor.inner" />
74+
</service>
75+
7276
<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">
7377
<argument type="service" id="api_platform.cache.metadata.property" />
7478
<argument type="service" id="api_platform.metadata.property.metadata_factory.cached.inner" />
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\Exception\PropertyNotFoundException;
17+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
18+
19+
/**
20+
* Set properties available in the constructor as writable.
21+
*
22+
* @author Maxime Veber <[email protected]>
23+
*/
24+
class ConstructorPropertyMetadataFactory implements PropertyMetadataFactoryInterface
25+
{
26+
/**
27+
* @var PropertyMetadataFactoryInterface
28+
*/
29+
private $decorated;
30+
31+
public function __construct(PropertyMetadataFactoryInterface $decorated = null)
32+
{
33+
$this->decorated = $decorated;
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata
40+
{
41+
if (null === $this->decorated) {
42+
$propertyMetadata = new PropertyMetadata();
43+
} else {
44+
try {
45+
$propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
46+
} catch (PropertyNotFoundException $propertyNotFoundException) {
47+
$propertyMetadata = new PropertyMetadata();
48+
}
49+
}
50+
51+
// Constructor arguments are obviously accessible only on post operation, put will result in an error.
52+
if (!isset($options['collection_operation_name']) || 'post' !== $options['collection_operation_name']) {
53+
return $propertyMetadata;
54+
}
55+
56+
$ref = new \ReflectionClass($resourceClass);
57+
if (!$ref->isInstantiable() || !$constructor = $ref->getConstructor()) {
58+
return $propertyMetadata;
59+
}
60+
61+
foreach ($constructor->getParameters() as $constructorParameter) {
62+
if ($constructorParameter->getName() === $property) {
63+
return $propertyMetadata->withWritable(true);
64+
}
65+
}
66+
67+
return $propertyMetadata;
68+
}
69+
}

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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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\Metadata\Property\Factory;
15+
16+
use ApiPlatform\Core\Metadata\Property\Factory\ConstructorPropertyMetadataFactory;
17+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
19+
use PHPUnit\Framework\TestCase;
20+
use Prophecy\Argument;
21+
use Symfony\Component\PropertyInfo\Type;
22+
23+
/**
24+
* @author Maxime Veber <[email protected]>
25+
*/
26+
class ConstructorPropertyMetadataFactoryTest extends TestCase
27+
{
28+
public function testItIsAnInstanceOfPropertyMetadataFactory()
29+
{
30+
$factory = new ConstructorPropertyMetadataFactory();
31+
$this->assertInstanceOf(PropertyMetadataFactoryInterface::class, $factory);
32+
}
33+
34+
public function testItAddsWritableForConstructorProperties()
35+
{
36+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
37+
38+
$type = new Type(Type::BUILTIN_TYPE_STRING);
39+
$fooMetadata = new PropertyMetadata($type, 'foo', true, false, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
40+
$propertyMetadataFactory->create(DummyObjectWithConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
41+
$factory = new ConstructorPropertyMetadataFactory($propertyMetadataFactory->reveal());
42+
43+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'foo', [
44+
'collection_operation_name' => 'post',
45+
]);
46+
$this->assertTrue($fooMetadata->isWritable());
47+
}
48+
49+
public function testItDoesntAddWritableOnWrongOperations()
50+
{
51+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
52+
53+
$type = new Type(Type::BUILTIN_TYPE_STRING);
54+
$fooMetadata = new PropertyMetadata($type, 'foo', true, false, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
55+
$propertyMetadataFactory->create(DummyObjectWithConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
56+
$factory = new ConstructorPropertyMetadataFactory($propertyMetadataFactory->reveal());
57+
58+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'foo', [
59+
'collection_operation_name' => 'get',
60+
]);
61+
$this->assertFalse($fooMetadata->isWritable());
62+
}
63+
64+
public function testItCreateAndSetRightMetadataIfNoFactoryGiven()
65+
{
66+
$factory = new ConstructorPropertyMetadataFactory();
67+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'bar', [
68+
'collection_operation_name' => 'post',
69+
]);
70+
71+
$this->assertTrue($fooMetadata->isWritable());
72+
}
73+
74+
public function testItDoesntThrowErrorForAnyKindOfObject()
75+
{
76+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
77+
78+
$type = new Type(Type::BUILTIN_TYPE_STRING);
79+
$fooMetadata = new PropertyMetadata($type, 'foo', true, true, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
80+
$propertyMetadataFactory->create(DummyObjectWithoutConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
81+
$factory = new ConstructorPropertyMetadataFactory($propertyMetadataFactory->reveal());
82+
83+
$fooMetadata = $factory->create(DummyObjectWithoutConstructor::class, 'foo', [
84+
'collection_operation_name' => 'post',
85+
]);
86+
$this->assertTrue($fooMetadata->isWritable());
87+
}
88+
}
89+
90+
class DummyObjectWithConstructor
91+
{
92+
private $foo;
93+
private $bar;
94+
95+
public function __construct(string $foo, \stdClass $bar)
96+
{
97+
$this->foo = $foo;
98+
$this->bar = $bar;
99+
}
100+
}
101+
102+
class DummyObjectWithoutConstructor
103+
{
104+
private $foo;
105+
106+
public function getFoo()
107+
{
108+
return $this->foo;
109+
}
110+
111+
public function setFoo($foo)
112+
{
113+
$this->foo = $foo;
114+
}
115+
}

0 commit comments

Comments
 (0)