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

Skip to content

Commit 05c4a15

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 05c4a15

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-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+
final 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 = (string) $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

+1Lines 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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
public function testItIsAnInstanceOfPropertyMetadataFactory()
31+
{
32+
$factory = new ConstructorPropertyMetadataFactory($this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class));
33+
$this->assertInstanceOf(PropertyMetadataFactoryInterface::class, $factory);
34+
}
35+
36+
public function testItAddsWritableForConstructorProperties()
37+
{
38+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
39+
40+
$type = new Type(Type::BUILTIN_TYPE_STRING);
41+
$fooMetadata = new PropertyMetadata($type, 'foo', true, false, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
42+
$propertyMetadataFactory->create(DummyObjectWithConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
43+
44+
$factory = new ConstructorPropertyMetadataFactory(
45+
$this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class),
46+
$propertyMetadataFactory->reveal()
47+
);
48+
49+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'foo', [
50+
'collection_operation_name' => 'hello',
51+
]);
52+
$this->assertTrue($fooMetadata->isWritable());
53+
}
54+
55+
public function testItDoesntAddWritableOnWrongOperations()
56+
{
57+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
58+
59+
$type = new Type(Type::BUILTIN_TYPE_STRING);
60+
$fooMetadata = new PropertyMetadata($type, 'foo', true, false, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
61+
$propertyMetadataFactory->create(DummyObjectWithConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
62+
63+
$factory = new ConstructorPropertyMetadataFactory(
64+
$this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class, 'GET'),
65+
$propertyMetadataFactory->reveal()
66+
);
67+
68+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'foo', [
69+
'collection_operation_name' => 'hello',
70+
]);
71+
$this->assertFalse($fooMetadata->isWritable());
72+
}
73+
74+
public function testItCreateAndSetRightMetadataIfNoFactoryGiven()
75+
{
76+
$factory = new ConstructorPropertyMetadataFactory($this->getResourceMetadataFactoryMock(DummyObjectWithConstructor::class));
77+
$fooMetadata = $factory->create(DummyObjectWithConstructor::class, 'bar', [
78+
'collection_operation_name' => 'hello',
79+
]);
80+
81+
$this->assertTrue($fooMetadata->isWritable());
82+
}
83+
84+
public function testItDoesntThrowErrorForAnyKindOfObject()
85+
{
86+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
87+
88+
$type = new Type(Type::BUILTIN_TYPE_STRING);
89+
$fooMetadata = new PropertyMetadata($type, 'foo', true, true, false, false, true, false, 'http://example.com/foo', null, ['foo' => 'bar']);
90+
$propertyMetadataFactory->create(DummyObjectWithoutConstructor::class, 'foo', Argument::any())->willReturn($fooMetadata)->shouldBeCalled();
91+
92+
$factory = new ConstructorPropertyMetadataFactory(
93+
$this->getResourceMetadataFactoryMock(DummyObjectWithoutConstructor::class),
94+
$propertyMetadataFactory->reveal()
95+
);
96+
97+
$fooMetadata = $factory->create(DummyObjectWithoutConstructor::class, 'foo', [
98+
'collection_operation_name' => 'hello',
99+
]);
100+
$this->assertTrue($fooMetadata->isWritable());
101+
}
102+
103+
private function getResourceMetadataFactoryMock($resource, $method = 'post')
104+
{
105+
$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
106+
$metadata = (new ResourceMetadata())->withCollectionOperations(['hello' => ['method' => $method]]);
107+
108+
$resourceMetadataFactory->create($resource)->willReturn($metadata);
109+
110+
return $resourceMetadataFactory->reveal();
111+
}
112+
}
113+
114+
class DummyObjectWithConstructor
115+
{
116+
private $foo;
117+
private $bar;
118+
119+
public function __construct(string $foo, \stdClass $bar)
120+
{
121+
$this->foo = $foo;
122+
$this->bar = $bar;
123+
}
124+
}
125+
126+
class DummyObjectWithoutConstructor
127+
{
128+
private $foo;
129+
130+
public function getFoo()
131+
{
132+
return $this->foo;
133+
}
134+
135+
public function setFoo($foo)
136+
{
137+
$this->foo = $foo;
138+
}
139+
}

0 commit comments

Comments
 (0)
0