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

Skip to content

Commit 3c55e3d

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 3c55e3d

File tree

4 files changed

+225
-0
lines changed

4 files changed

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

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

0 commit comments

Comments
 (0)
0