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

Skip to content

Commit eb06080

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 eb06080

File tree

4 files changed

+236
-0
lines changed

4 files changed

+236
-0
lines changed

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.metadata.resource.metadata_factory" />
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: 84 additions & 0 deletions
+
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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\Metadata\Property\Factory;
15+
16+
use ApiPlatform\Core\Exception\PropertyNotFoundException;
17+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
18+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
19+
20+
/**
21+
* Set properties available in the constructor as writable.
22+
*
23+
* @author Maxime Veber <maxime.veber@nekland.fr>
24+
*/
25+
class ConstructorPropertyMetadataFactory implements PropertyMetadataFactoryInterface
26+
{
27+
/**
28+
* @var PropertyMetadataFactoryInterface
29+
*/
30+
private $decorated;
31+
32+
/**
33+
* @var ResourceMetadataFactoryInterface
34+
*/
35+
private $resourceMetadataFactory;
36+
37+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyMetadataFactoryInterface $decorated = null)
38+
{
39+
$this->decorated = $decorated;
40+
$this->resourceMetadataFactory = $resourceMetadataFactory;
41+
}
42+
43
44+
* {@inheritdoc}
45+
*/
46+
public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata
47+
{
48+
if (null === $this->decorated) {
49+
$propertyMetadata = new PropertyMetadata();
50+
} else {
51+
try {
52+
$propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
53+
} catch (PropertyNotFoundException $propertyNotFoundException) {
54+
$propertyMetadata = new PropertyMetadata();
55+
}
56+
}
57+
58+
if (!isset($options['collection_operation_name'])) {
59+
return $propertyMetadata;
60+
}
61+
62+
// Constructor arguments are obviously accessible only on post operation, put will result in an error.
63+
$method = $this->resourceMetadataFactory
64+
->create($resourceClass)
65+
->getCollectionOperationAttribute($options['collection_operation_name'], 'method')
66+
;
67+
if ('post' !== strtolower($method)) {
68+
return $propertyMetadata;
69+
}
70+
71+
$ref = new \ReflectionClass($resourceClass);
72+
if (!$ref->isInstantiable() || !$constructor = $ref->getConstructor()) {
73+
return $propertyMetadata;
74+
}
75+
76+
foreach ($constructor->getParameters() as $constructorParameter) {
77+
if ($constructorParameter->getName() === $property) {
78+
return $propertyMetadata->withWritable(true);
79+
}
80+
}
81+
82+
return $propertyMetadata;
83+
}
84+
}

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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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\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 ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
21+
use PHPUnit\Framework\TestCase;
22+
use Prophecy\Argument;
23+
use Symfony\Component\PropertyInfo\Type;
24+
25+
/**
26+
* @author Maxime Veber <maxime.veber@nekland.fr>
27+
*/
28+
class ConstructorPropertyMetadataFactoryTest extends TestCase
29+
{
30+
private $resourceMetadataFactory;
31+
32+
public function setUp()
33+
{
34+
}
35+
36+
public function testItIsAnInstanceOfPropertyMetadataFactory()
37+
{
38+
$factory = new ConstructorPropertyMetadataFactory($this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class));
39+
$this->assertInstanceOf(PropertyMetadataFactoryInterface::class, $factory);
40+
}
41+
42+
public function testItAddsWritableForConstructorProperties()
43+
{
44+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
45+
46+
$type = new Type(Type::BUILTIN_TYPE_STRING);
47+
$fooMetadata = new PropertyMetadata($type, 'foo', true, false, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
48+
$propertyMetadataFactory->create(DummyObjectWithConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
49+
50+
$factory = new ConstructorPropertyMetadataFactory(
51+
$this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class),
52+
$propertyMetadataFactory->reveal()
53+
);
54+
55+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'foo', [
56+
'collection_operation_name' => 'hello',
57+
]);
58+
$this->assertTrue($fooMetadata->isWritable());
59+
}
60+
61+
public function testItDoesntAddWritableOnWrongOperations()
62+
{
63+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
64+
65+
$type = new Type(Type::BUILTIN_TYPE_STRING);
66+
$fooMetadata = new PropertyMetadata($type, 'foo', true, false, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
67+
$propertyMetadataFactory->create(DummyObjectWithConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
68+
69+
$factory = new ConstructorPropertyMetadataFactory(
70+
$this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class, 'GET'),
71+
$propertyMetadataFactory->reveal()
72+
);
73+
74+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'foo', [
75+
'collection_operation_name' => 'get',
76+
]);
77+
$this->assertFalse($fooMetadata->isWritable());
78+
}
79+
80+
public function testItCreateAndSetRightMetadataIfNoFactoryGiven()
81+
{
82+
$factory = new ConstructorPropertyMetadataFactory($this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class));
83+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'bar', [
84+
'collection_operation_name' => 'post',
85+
]);
86+
87+
$this->assertTrue($fooMetadata->isWritable());
88+
}
89+
90+
public function testItDoesntThrowErrorForAnyKindOfObject()
91+
{
92+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
93+
94+
$type = new Type(Type::BUILTIN_TYPE_STRING);
95+
$fooMetadata = new PropertyMetadata($type, 'foo', true, true, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
96+
$propertyMetadataFactory->create(DummyObjectWithoutConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
97+
98+
$factory = new ConstructorPropertyMetadataFactory(
99+
$this->getResourceMetadataFactoryMock(DummyObjectWithoutConstructor::class),
100+
$propertyMetadataFactory->reveal()
101+
);
102+
103+
$fooMetadata = $factory->create(DummyObjectWithoutConstructor::class, 'foo', [
104+
'collection_operation_name' => 'post',
105+
]);
106+
$this->assertTrue($fooMetadata->isWritable());
107+
}
108+
109+
private function getResourceMetadataFactoryMock($resource, $method = 'post')
110+
{
111+
$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
112+
$metadata = $this->prophesize(ResourceMetadata::class);
113+
$metadata->getCollectionOperationAttribute(Argument::any(), 'method')->willReturn($method);
11 179B 4+
115+
$resourceMetadataFactory->create($resource)->willReturn($metadata->reveal());
116+
117+
return $this->resourceMetadataFactory->reveal();
118+
}
119+
}
120+
121+
class DummyObjectWithConstructor
122+
{
123+
private $foo;
124+
private $bar;
125+
126+
public function __construct(string $foo, \stdClass $bar)
127+
{
128+
$this->foo = $foo;
129+
$this->bar = $bar;
130+
}
131+
}
132+
133+
class DummyObjectWithoutConstructor
134+
{
135+
private $foo;
136+
137+
public function getFoo()
138+
{
139+
return $this->foo;
140+
}
141+
142+
public function setFoo($foo)
143+
{
144+
$this->foo = $foo;
145+
}
146+
}

0 commit comments

Comments
 (0)
0