Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: spec-compliant PUT method#4996

Merged
soyuka merged 9 commits into
api-platform:mainfrom
dunglas:feat/standard-put
Jan 11, 2023
Merged

feat: spec-compliant PUT method#4996
soyuka merged 9 commits into
api-platform:mainfrom
dunglas:feat/standard-put

Conversation

@dunglas
Copy link
Copy Markdown
Member

@dunglas dunglas commented Sep 21, 2022

Q A
Branch? main
Tickets n/a
License MIT
Doc PR todo

Our current implementation of the PUT HTTP method is not standard-compliant: according to the spec, a PUT must create or replace the resource at the requested URL.

However, currently, we don't allow the creation of new resources using PUT, and we update the existing resources instead of replacing them: untouched properties are kept instead of being reset to their default values. Basically, our PUT behaves as PATCH.

The main reason behind that non-conform behavior is a Doctrine ORM limitation.

This patch adds support for spec-compliant PUT and provides a workaround for Doctrine ORM.

Making PUT spec-compliant is opt-in to preserve backward compatibility, but using the non-conform version will be deprecated (use PATCH instead), and removed in API Platform 4.

Allowing resource creation is not enabled by default because it can cause security issues.

TODO:

  • Use dedicated attribute properties instead of relying on extra_properties
  • Deprecate not setting standard_put to true
  • Update the recipe to disable PUT by default (using PATCH is probably a better solution for most common use cases, so we should make PUT opt-in)

@dunglas dunglas requested a review from soyuka September 21, 2022 17:57
Comment thread tests/Fixtures/TestBundle/Entity/StandardPut.php Outdated
// This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported:
// https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555
if (isset($context['read_data'], $context['resource_class']) && $operation instanceof HttpOperation && 'PATCH' !== $operation->getMethod()) {
$propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($context['resource_class'])->getIterator());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we can avoid all this, you can retrieve identifiers by looping over foreach $operation->getUriVariables() as $uriVariable then foreach $uriVariable->getIdentifiers() as $property this way we don't use metadata factories here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Awesome.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My refactoring doesn't handle the case where URI variables don't match the Doctrine properties, but I think that it's good enough for now, mainly because it's in sync with what is currently done here: https://github.com/api-platform/core/blob/main/src/Doctrine/Orm/State/ItemProvider.php#L51

At some point, we'll probably want to introduce a utility to map URI variables with internal Doctrine identifiers.

Comment thread src/Symfony/EventListener/WriteListener.php Outdated
@dunglas dunglas force-pushed the feat/standard-put branch 2 times, most recently from de18ece to ce98746 Compare September 21, 2022 21:01
Comment thread features/main/standard_put.feature Outdated

public function __construct(private readonly ProviderInterface $provider, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, UriVariablesConverterInterface $uriVariablesConverter = null)
{
public function __construct(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought we didn't want to have multiple lines in the constructor?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Now that we have constructor promotion and the ability to add a comma after the last parameter, I think we should use multiple lines for clarity and to reduce merge conflicts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good to know, it would be great to add it in the CS fixer.

if (
null === $data &&
(
HttpOperation::METHOD_PUT !== $operation->getMethod() ||
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why using a constant here and not elsewhere?

null !== $data &&
(
'PATCH' === $method ||
('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? false))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

use HttpOperation constants for methods

* @author Kévin Dunglas <[email protected]>
*/
#[ApiResource(extraProperties: [
'standard_put' => true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I find the name confusing

// PUT: reset the existing object managed by Doctrine and merge data sent by the user in it
// This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported:
// https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555
if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod()) {
if ($operation instanceof HttpOperation && HttpOperation::METHOD_PUT === $operation->getMethod()) {

@soyuka soyuka mentioned this pull request Nov 5, 2022
@soyuka soyuka changed the title feat: spec-compliant PUT method feat: spec-compliant PUT method + #4344 Nov 21, 2022
@soyuka soyuka changed the title feat: spec-compliant PUT method + #4344 feat: spec-compliant PUT method Nov 21, 2022
@soyuka soyuka added this to the 4.0 milestone Nov 21, 2022
@soyuka soyuka force-pushed the feat/standard-put branch 4 times, most recently from 6705312 to 83b200b Compare November 28, 2022 21:54
Comment thread src/Metadata/Put.php
$processor = null,
array $extraProperties = []
array $extraProperties = [],
private ?bool $allowCreate = null,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

IMHO the order of these parameters shouldn't be part of the BC promise and users must have to use named properties.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

note that this allows to use ...func_get_args() to call parent::__construct without having to be concerned about allowCreate.

@soyuka soyuka force-pushed the feat/standard-put branch from d26e501 to 2256c61 Compare January 11, 2023 09:59
@soyuka soyuka merged commit bde59ba into api-platform:main Jan 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants