Make resource constructor parameters writables#1749
Make resource constructor parameters writables#1749dunglas merged 1 commit intoapi-platform:masterfrom
Conversation
6f74b32 to
addec68
Compare
| $constructorParameters = $ref->getConstructor()->getParameters(); | ||
| foreach ($constructorParameters as $constructorParameter) { | ||
| if ($constructorParameter->getName() === $property) { | ||
| return $propertyMetadata->withWritable(true); |
There was a problem hiding this comment.
Okay but this doesn't mean that the PropertyAccess component will be able to write to these properties right (denormalization process)?
There was a problem hiding this comment.
The PropertyAccessor component directly no, but the Serializer component knows how to use constructors, and constructor support has been dramatically improved in 4.1.
There was a problem hiding this comment.
Constructors are already supported in the serializer for a while.
4e512a2 to
1ea3cca
Compare
| } | ||
|
|
||
| // Constructor arguments are obviously accessible only on post operation, put will result in an error. | ||
| if (!isset($options['collection_operation_name']) || 'post' !== $options['collection_operation_name']) { |
There was a problem hiding this comment.
What if we have custom collection operations that don't have the post name?
There was a problem hiding this comment.
I don't understand the use case, can you be more specific? It doesn't make sense to allow constructor in another request type than post.
There was a problem hiding this comment.
He meant this:
collectionOperations={
"foo" = {"method" = "POST"}
}
There was a problem hiding this comment.
I guess the best way to fix this is to add the method to the context, what do you think about?
There was a problem hiding this comment.
Huum, ResourceMetadata should do the job. For example:
$this->resourceMetadataFactory->create($resourceClass)->getCollectionOperationAttribute($options['collection_operation_name'], 'method')There was a problem hiding this comment.
Fixed. Thanks for the tip, I was not aware of that :) .
| <argument type="service" id="api_platform.metadata.property.metadata_factory.serializer.inner" /> | ||
| </service> | ||
|
|
||
| <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="20" public="false"> |
There was a problem hiding this comment.
Could we use 30 as a priority as well?
There was a problem hiding this comment.
Actually 35 makes even more sense (as the serializer SerializerPropertyMetadataFactory will potentially modify this accessibility) !
Nice catch.
ee9125a to
eb06080
Compare
|
In what cases is this possible? $method = $this->resourceMetadataFactory
->create($resourceClass)
->getCollectionOperationAttribute($options['collection_operation_name'], 'method')
;
// $method === null |
e9cfcd8 to
05c4a15
Compare
|
There is a better way to do that with the $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $options['collection_operation_name']);Sorry, I missed this interface... |
3960779 to
3c55e3d
Compare
|
Indeed. Fixed! |
3c55e3d to
c0bbcaf
Compare
|
I thought it would be harder! Good job! Would you mind adding an integration test to be sure we support this new feature everywhere? |
| // Constructor arguments are obviously accessible only on post operation, put will result in an error. | ||
| try { | ||
| $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $options['collection_operation_name']); | ||
| } catch (RuntimeException $e) { |
There was a problem hiding this comment.
when does this throw a RuntimeException?
There was a problem hiding this comment.
Here:
There was a problem hiding this comment.
It was about how the method can be null (well, ok, the route is null, but it's not the answer).
| } | ||
| } | ||
|
|
||
| class DummyObjectWithConstructor |
There was a problem hiding this comment.
Wasn't best practice to put these in another file?
There was a problem hiding this comment.
TL;DR: I don't think it's a big deal.
Well, that depends on your POV. IMO re-using dummy classes in many tests is not a good practice because it means your test is dependent on something outside (see FIRST). I do it only in case I need doctrine entities to avoid configuration duplication.
There was a problem hiding this comment.
I agree but it's how we choose to do in this project and we have to follow the project rules :).
There was a problem hiding this comment.
Is it actually a rule or just something people did without taking care?
There was a problem hiding this comment.
I know for a fact that I did the same as you and I was told to move my classes out of the test file that's all :p.
There was a problem hiding this comment.
So I guess it's a rule :) .
There was a problem hiding this comment.
It's a rule, because it's what is recommended by the PSRs. Anyway, we try to enforce not reusing dummy classes, because of first and - especially - because it's a pain to maintain such tests.
Both can be done together: a test = a dummy class in a separate file. It's what we should enforce.
There was a problem hiding this comment.
Couldn't an anonymous class be used ?
There was a problem hiding this comment.
Putting them outside doesn't mean reusing them. We should not have more than 1 class in a file, as that wouldn't work for PSR-4 autoloading.
| */ | ||
| private $operationMethodResolver; | ||
|
|
||
| public function __construct(OperationMethodResolverInterface $operationMethodResolver, PropertyMetadataFactoryInterface $decorated = null) |
There was a problem hiding this comment.
Why would the decorated object be null ?
There was a problem hiding this comment.
so that it can be used without decoration
There was a problem hiding this comment.
IMO it's either you decorate something or you don't, Not both at the same time (S from Solid, anyone ?).
Having a basic decorator that just returns a new PropertyMetadata() would be simpler.
There was a problem hiding this comment.
@Taluu loaders can be chained in any order (and sometimes you have to change the order, to change the priority). The first registered loader will not be a decorator, but you cannot guess which one will be the first when writing the code (it depends of the config).
There was a problem hiding this comment.
Then it means it is the wrong design pattern for these. Shouldn't a strategy be better for that ? of course, nothing prevents some elements in this pattern to be decorators...
There was a problem hiding this comment.
Or a chain of responsability would also do it
There was a problem hiding this comment.
They was tried, and they introduce a lof of unneeded complexity. They have been a lot of iterations on this part, I don't want to change the again. It's mostly internal, and it works well as is. Please don't over engineer.
There was a problem hiding this comment.
+using optional decoration makes it very easy to configure with the Symfony DI component since it supports service decoration and priorities. Using a different pattern such as chain (as it was in API Platform 1) will require to add a lot of specific code to have the same flexibility.
| return $propertyMetadata; | ||
| } | ||
|
|
||
| if ('post' !== strtolower($method)) { |
There was a problem hiding this comment.
With #1752, 'POST' !== $method is enough. You just have to rebase.
| final class ConstructorPropertyMetadataFactory implements PropertyMetadataFactoryInterface | ||
| { | ||
| /** | ||
| * @var PropertyMetadataFactoryInterface |
There was a problem hiding this comment.
Useless (documented by the typehint), to remove.
There was a problem hiding this comment.
Not all editors / etc uses the constructor typehint for properties, so this looks adequate ?
There was a problem hiding this comment.
We do that everywhere in the project, and in Symfony as well (we follow most of Symfony's CS). It's less maintenance and PHPStorm supports this so... 😀
There was a problem hiding this comment.
yeah, that's why it was more a question, but then I opened a ticket on phpactor. :D
| private $decorated; | ||
|
|
||
| /** | ||
| * @var OperationMethodResolverInterface |
There was a problem hiding this comment.
Useless (documented by the typehint), to remove.
| } | ||
| } | ||
|
|
||
| class DummyObjectWithConstructor |
There was a problem hiding this comment.
It's a rule, because it's what is recommended by the PSRs. Anyway, we try to enforce not reusing dummy classes, because of first and - especially - because it's a pain to maintain such tests.
Both can be done together: a test = a dummy class in a separate file. It's what we should enforce.
| } | ||
|
|
||
| foreach ($constructor->getParameters() as $constructorParameter) { | ||
| if ($constructorParameter->getName() === $property) { |
There was a problem hiding this comment.
I think we could just do $constructorParameter->name? It seems to be the same.
c0bbcaf to
d6b1556
Compare
|
Review done. Rebased. Behat tests added. 😄 |
dunglas
left a comment
There was a problem hiding this comment.
Almost good, great job! (but Travis is red currently).
| } | ||
|
|
||
| foreach ($constructor->getParameters() as $constructorParameter) { | ||
| if ($constructorParameter->name === $property) { |
There was a problem hiding this comment.
We must also check that the current value of writable is null (to avoid overwriting a manual setting tru @ApiProperty).
There was a problem hiding this comment.
Hum. I think modifying the order of decoration would be better because the property accessor will set this value to "false".
Actually, this issue is also present in the ExtractorPropertyMetadataFactory: if the property accessor decided to set the property to "not accessible" (aka false), no manual configuration can reverse the value because it fills it only if the current value is null.
What do you think of that?
TL;DR: If I do it, this class is useless.
There was a problem hiding this comment.
The user's manual configuration must always be able to override anything. And yes, I think this decorator should run after the PropertyInfo decorator.
There was a problem hiding this comment.
Ok, so this answer means that there's an issue inside the ExtractorPropertyMetadataFactory.
It also means as I understand that I can indeed override the writable value (so I don't modify this line).
Do you want me to fix everything in this PR ?
d6b1556 to
53f9ba3
Compare
|
I fixed my first commit and added a new commit for the new things pointed out. (waiting for feedback) |
| $propertyMetadata = $propertyMetadata->withInitializable($initializable); | ||
| } | ||
| } else { | ||
| // BC layer for Symfony < 4.1 |
| private $initializable; | ||
|
|
||
| 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) | ||
| 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, $initializable = null) |
992c1f5 to
aac5d55
Compare
|
@soyuka I prefer to not add those tests in this MR if you don't mind. |
aac5d55 to
d542e8c
Compare
| } | ||
| } else { | ||
| // BC layer for Symfony < 4.2 | ||
| // To be removed in EOF of Symfony 3.4 |
There was a problem hiding this comment.
I don't understand this comment. If it's a BC layer for Symfony < 4.2, shouldn't it only be removed when we drop support for Symfony < 4.2?
There was a problem hiding this comment.
Yes we can just drop the last line
|
added tests in #1991 |
|
The tests should be in this PR. |
|
to add tests here @Nek- can rebase from my branch |
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.
d542e8c to
da500b2
Compare
|
Okok guys, I was more in favor of merging PRs separately. But well, if you disagree then please review tests in #1991 first because they are not ok to me (and I assume also not for you). |
|
Thank you very much for this @Nek-! It's very appreciated. |
|
great, can't wait to try this. @komik966 can you work on the tests now? |
|
In June I've limited time, so work may be little slow. But I think we are very close to fully working relations. Partially it was implemented in my PR (without handling circular references). |
|
This is great work, but I think it's unfortunate that we've merged this without tests. |
|
@teohhanhui it is actually tested. |
|
@bendavies this is a test: https://github.com/api-platform/core/pull/1749/files#diff-b63a56b1101fe1091a9cf488e2b0b383R1 The branch of komik966 is nice because it adds many other tests, but it was not mergeable as it to me (and not only to me as I understand). What's the problem? I don't understand. (If you ask if you can use the feature, yes you can) |
|
@komik966's tests are covering relations from what I can see and there might be an issue there. Also, because of the red coverage, I think that this could be more tested then it is already :). |
… is initializable (dunglas) This PR was squashed before being merged into the 4.2-dev branch (closes #26997). Discussion ---------- [PropertyInfo] Add an extractor to guess if a property is initializable | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tests pass? | yes <!-- please add some, will be required by reviewers --> | Fixed tickets | n/a <!-- #-prefixed issue number(s), if any --> | License | MIT | Doc PR | todo When dealing with value objects, being able to detect if a property can be initialized using the constructor is a very valuable information. It's mandatory to add a proper value object support in API Platform and in the Serializer component. See api-platform/core#1749 and api-platform/core#1843 for the related discussions, extended use cases and proof of concepts. This PR adds a new interface to guess if a property can be initialized through the constructor, and an implementation using the reflection (in `ReflectionExtractor`). Commits ------- 9d2ab9e [PropertyInfo] Add an extractor to guess if a property is initializable

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.