-
-
Notifications
You must be signed in to change notification settings - Fork 912
Make resource constructor parameters writables #1749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make resource constructor parameters writables #1749
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constructors are already supported in the serializer for a while.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool then :).
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we have custom collection operations that don't have the post
name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
He meant this:
collectionOperations={
"foo
10000
" = {"method" = "POST"}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Thanks for the tip, I was not aware of that :) .
@@ -69,6 +69,10 @@ | |||
<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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use 30 as a priority as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when does this throw a RuntimeException?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also asked :) : #1749 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here:
throw new RuntimeException(sprintf('Either a "route_name" or a "method" operation attribute must exist for the operation "%s" of the resource "%s".', $operationName, $resourceClass)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't best practice to put these in another file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it actually a rule or just something people did without taking care?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I guess it's a rule :) .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't an anonymous class be used ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would the decorated object be null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so that it can be used without decoration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or a chain of responsability would also do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useless (documented by the typehint), to remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not all editors / etc uses the constructor typehint for properties, so this looks adequate ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useless (documented by the typehint), to remove.
} | ||
} | ||
|
||
class DummyObjectWithConstructor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could just do $constructorParameter->name
? It seems to be the same.
c0bbcaf
to
d6b1556
Compare
Review done. Rebased. Behat tests added. 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Symfony 4.2 actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arf, yes.
|
||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool $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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we can just drop the last line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes my bad!
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.