8000 Make resource constructor parameters writables by Nek- · Pull Request #1749 · api-platform/core · GitHub
[go: up one dir, main page]

Skip to content

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

Conversation

Nek-
Copy link
Contributor
@Nek- Nek- commented Mar 5, 2018

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.

Q A
Bug fix? somehow
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #1747
License MIT
Doc PR N/A

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from 6f74b32 to addec68 Compare March 5, 2018 16:26
$constructorParameters = $ref->getConstructor()->getParameters();
foreach ($constructorParameters as $constructorParameter) {
if ($constructorParameter->getName() === $property) {
return $propertyMetadata->withWritable(true);
Copy link
Member

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)?

Copy link
Member

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.

Copy link
Contributor Author
@Nek- Nek- Mar 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👼 symfony/symfony#25493 👼

Constructors are already supported in the serializer for a while.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool then :).

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch 2 times, most recently from 4e512a2 to 1ea3cca Compare March 6, 2018 10:11
}

// 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']) {
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member
@meyerbaptiste meyerbaptiste Mar 6, 2018

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"}
}

Copy link
Contributor Author

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?

Copy link
Member

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')

Copy link
Contributor Author
@Nek- Nek- Mar 7, 2018

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">
Copy link
Member

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?

Copy link
Contributor Author
@Nek- Nek- Mar 6, 2018

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.

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch 2 times, most recently from ee9125a to eb06080 Compare March 7, 2018 13:07
@Nek-
Copy link
Contributor Author
Nek- commented Mar 7, 2018

In what cases is this possible?

$method = $this->resourceMetadataFactory
    ->create($resourceClass)
    ->getCollectionOperationAttribute($options['collection_operation_name'], 'method')
;
// $method === null

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch 2 times, most recently from e9cfcd8 to 05c4a15 Compare March 7, 2018 13:41
@meyerbaptiste
Copy link
Member
meyerbaptiste commented Mar 7, 2018

There is a better way to do that with the OperationMethodResolver class:

$this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $options['collection_operation_name']);

Sorry, I missed this interface...

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch 2 times, most recently from 3960779 to 3c55e3d Compare March 7, 2018 13:55
@Nek-
Copy link
Contributor Author
Nek- commented Mar 7, 2018

Indeed. Fixed!

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from 3c55e3d to c0bbcaf Compare March 7, 2018 17:14
@dunglas
Copy link
Member
dunglas commented Mar 8, 2018

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) {
Copy link
Member

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?

Copy link
Contributor Author

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)

Copy link
Member

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));

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author
@Nek- Nek- Mar 8, 2018

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.

Copy link
Member

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 :).

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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 :) .

Copy link
Member
@dunglas dunglas Mar 14, 2018

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.

Copy link
Contributor

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 ?

Copy link
Contributor

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)
Copy link
Contributor

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 ?

Copy link
Member

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

Copy link
Contributor

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.

Copy link
Member

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).

Copy link
Contributor

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...

Copy link
Contributor

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

Copy link
Member
@dunglas dunglas Mar 12, 2018

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.

Copy link
Member
@dunglas dunglas Mar 12, 2018

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)) {
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor

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 ?

Copy link
Member
@dunglas dunglas Mar 14, 2018

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... 😀

Copy link
Contributor

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
Copy link
Member

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
Copy link
Member
@dunglas dunglas Mar 14, 2018

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) {
Copy link
Contributor

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.

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from c0bbcaf to d6b1556 Compare March 19, 2018 14:46
@Nek-
Copy link
Contributor Author
Nek- commented Mar 19, 2018

Review done. Rebased. Behat tests added. 😄

Copy link
Member
@dunglas dunglas left a 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) {
Copy link
Member

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).

Copy link
Contributor Author
@Nek- Nek- Mar 21, 2018

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.

Copy link
Contributor

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.

Copy link
Contributor Author
@Nek- Nek- Mar 22, 2018

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 ?

@Nek-
Copy link
Contributor Author
Nek- commented Mar 26, 2018

I see that the problem I noticed does not generate eruption of enthusiasm... So here is a schema of what's happen (as far as I understand). The problem is more obvious if you look at it. (you may also see some other issues in it)

apiplatformpropertyfactory

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from d6b1556 to 53f9ba3 Compare March 27, 2018 14:25
@Nek-
Copy link
Contributor Author
Nek- commented Mar 27, 2018

I fixed my first commit and added a new commit for the new things pointed out. (waiting for feedback)

@bendavies
Copy link
Contributor

this can't be merged without tests for relations. let's get them in somehow! @komik966 can PR to @Nek-'s branch?

$propertyMetadata = $propertyMetadata->withInitializable($initializable);
}
} else {
// BC layer for Symfony < 4.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symfony 4.2 actually.

Copy link
Contributor Author

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool $initializable = null

@soyuka
Copy link
Member
soyuka commented May 31, 2018

@Nek- @komik966 if you want me to take care of adding the tests written in #1843 in this patch let me know

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from 992c1f5 to aac5d55 Compare May 31, 2018 09:44
@Nek-
Copy link
Contributor Author
Nek- commented May 31, 2018

@soyuka I prefer to not add those tests in this MR if you don't mind.

@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from aac5d55 to d542e8c Compare May 31, 2018 09:46
}
} else {
// BC layer for Symfony < 4.2
// To be removed in EOF of Symfony 3.4
Copy link
Contributor

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?

Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes my bad!

@komik966
Copy link
Contributor

added tests in #1991
without modifications in ApiPlatform\Core\Serializer\AbstractItemNormalizer relations will not work

@teohhanhui
Copy link
Contributor

The tests should be in this PR.

@komik966
Copy link
Contributor
komik966 commented Jun 1, 2018

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.
@Nek- Nek- force-pushed the feature/make-constructor-parameters-writable branch from d542e8c to da500b2 Compare June 1, 2018 07:30
@Nek-
Copy link
Contributor Author
Nek- commented Jun 1, 2018

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).

@dunglas dunglas merged commit c81543a into api-platform:master Jun 8, 2018
@dunglas
Copy link
Member
dunglas commented Jun 8, 2018

Thank you very much for this @Nek-! It's very appreciated.

@bendavies
Copy link
Contributor

great, can't wait to try this.

@komik966 can you work on the tests now?

@komik966
Copy link
Contributor
komik966 commented Jun 8, 2018

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).

@teohhanhui
Copy link
Contributor

This is great work, but I think it's unfortunate that we've merged this without tests.

@Nek- Nek- deleted the feature/make-constructor-parameters-writable branch June 8, 2018 14:15
@Nek-
Copy link
Contributor Author
Nek- commented Jun 8, 2018

@teohhanhui it is actually tested.

@bendavies
Copy link
Contributor

Can we just clarify what needs to be done here so we don't leave work to be done, as i'm a little confused.

From reading above @Nek-, you didn't want @komik966's tests in your branch, but now you say it is actually tested?

Thanks!

@Nek-
Copy link
Contributor Author
Nek- commented Jun 11, 2018

@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)

@bendavies
Copy link
Contributor
bendavies commented Jun 11, 2018

thanks @Nek-.
i was trying to work out if there are tests in @komik966's branch that covered cases not in your tests.

@soyuka
Copy link
Member
soyuka commented Jun 11, 2018

@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 :).

fabpot added a commit to symfony/symfony that referenced this pull request Sep 4, 2018
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants
0