Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

New PUT Behavior

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Find your terminal and manually clear the cache directory:

rm -rf var/cache/*

I'm doing this so that, when we run all or our tests

symfony php bin/phpunit

we see a deprecation warning, which is fascinating. It says:

Since API Platform 3.1: in API Platform 4, PUT will always replace the data. set extraProperties["standard_put"] to true on every operation to avoid breaking PUT's behavior. Use PATCH for the old behavior.

Okay... what does that mean? Right now, it means nothing has changed: our PUT operation behaves like it always has. But, in API Platform 4, the behavior of PUT will change dramatically. And, at some point between now and then, we need to opt into that new behavior so that it doesn't suddenly break when we upgrade to version 4 in the future.

What's Changing in PUT

So what's changing exactly? Head over to the API docs and refresh. Use the GET collection endpoint... and hit "Execute", so we can get a valid ID.

Great: we have a treasure with ID 1.

Right now, if we send a PUT request with this ID, we can send just one field to update just that one thing. For example, we can send description to change only that.

Oh, but before we Execute this, we do need to be logged in. In my other tab, I'll fill in the login form. Perfect. Now execute the PUT operation.

Yup: we pass only the description field, and it updates only the description field: all the other fields remain the same.

Whelp, it turns out that this is not how PUT is supposed to work according to the HTTP Spec. PUT is supposed to be a "replace". What I mean is, if we send only one field, the PUT operation is supposed to take that new resource - which is just the one field - and replace the existing resource. That's a complicated way of saying that, when using PUT, you need to send every field, even the fields that aren't changing. Otherwise, they'll be set to null.

If that sounds kind of crazy, I kind of agree, but there are valid technical reasons for why this is the case. The point is that: this is how PUT is supposed to work and in API Platform 4, this is how PUT will work.

Honestly, it makes PUT less useful. So you'll notice that I'll pretty much exclusively use PATCH going forward.

Moving to the new PUT Behavior

So whether we like it or not, at some point between now and API platform 4, we need to tell API Platform that it is okay for it to change the behavior of PUT to the "new" way. Let's do that now by adding some extra config to every ApiResource attribute in our app.


To solve this globally for all your resources at once, you can add this as a default in the API Platform configuration:

# config/packages/api_platform.yaml
            standard_put: true

Open src/Entity/DragonTreasure.php... and add a new option called extraProperties set to an array with standard_put set to true:

... lines 1 - 27
... lines 29 - 64
extraProperties: [
'standard_put' => true,
... lines 69 - 89
class DragonTreasure
... lines 92 - 249

That's it! Copy that... because we're going to need that down here on this ApiResource... even though it doesn't have a PUT operation:

... lines 1 - 27
... lines 29 - 64
extraProperties: [
'standard_put' => true,
... lines 70 - 81
extraProperties: [
'standard_put' => true,
... lines 86 - 89
class DragonTreasure
... lines 92 - 249

Then, over in User, add that to both of the ApiResource spots as well:

... lines 1 - 25
... lines 27 - 44
extraProperties: [
'standard_put' => true,
... lines 50 - 59
extraProperties: [
'standard_put' => true,
... lines 64 - 66
class User implements UserInterface, PasswordAuthenticatedUserInterface
... lines 69 - 276

Now when we run our tests, the deprecation is gone! We're not using the PUT operation in any tests, so everything still passes.

Seeing the New Behavior

To see the new behavior, try out the PUT endpoint again: still sending just one field. This time... check it out! A 422 validation error! All the fields that we did not include were set to null... and that caused the validation failure.

So... this makes PUT a bit less useful... and we'll lean a lot more on PATCH. If you don't want to have a PUT operation at all anymore, that makes a lot of sense. One unique thing about the new PUT behavior is that you could use it to create new objects... which could be useful in some edge-cases... or an absolute nightmare from a security standpoint as we now need to worry about objects being edited or created via the same PUT operation. For that reason, as we go along, you'll see me remove the PUT operation in some cases.

Next: let's get more complex with security by making sure that a DragonTreasure can only be edited by its owner.

Leave a comment!

Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0