Custom Resource PUT Operation

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

Back to the API documentation! Let's pretend that we also need to be able to update a DailyStats. Maybe, if you're an admin, sometimes you need to double-check the data and update it. Specifically, let's make it possible to change the totalVisitors field.

Ok, cool! Updating is done via the put operation. Over in DailyStats, under itemOperations, add put:

... lines 1 - 9
/**
* @ApiResource(
... lines 12 - 14
* itemOperations={
... line 16
* "put",
* },
... line 19
* )
*/
class DailyStats
{
... lines 24 - 55
}

Then, because this is the first time we will be denormalizing, copy normalizationContext, paste, rename it to denormalizationContext and set its groups to daily-stats:write:

... lines 1 - 9
/**
* @ApiResource(
... line 12
* denormalizationContext={"groups"={"daily-stats:write"}},
... lines 14 - 19
* )
*/
class DailyStats
{
... lines 24 - 55
}

Take that daily-stats:write group and put it above totalVisitors:

... lines 1 - 21
class DailyStats
{
... lines 24 - 28
/**
* @Groups({"daily-stats:read", "daily-stats:write"})
*/
public $totalVisitors;
... lines 33 - 55
}

Let's give it a try! No, it won't work yet but... it will kind of seem like it's working. Refresh the docs. There's our PUT operation. Execute the collection operation so we can get a valid ID. Perfect. I'll copy this 2020-09-03. Down in the put operation... actually, let me scroll back up so we can see that the totalVisitors is currently 1,500.

Down on the put operation, hit "Try it out", paste the date string as the id and set totalVisitors to 500. Hit "Execute" and... it works! Wait, it worked?

No, it didn't really work. The deserialization process did update the DailyStats object, which is why we see the correct number in the response. But it's not actually saving. If you re-tried the get collection operation... yep! It's still 1,500.

Creating the Data Persister

What we're missing is a data persister for DailyStats.

Cool! We know how to make those! In the src/DataPersister/ directory, create a new PHP class and call it DailyStatsPersister, or, DailyStatsDataPersister if you want to be more consistent than I'm being. Make this implement DataPersisterInterface:

... lines 1 - 2
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
class DailyStatsPersister implements DataPersisterInterface
{
... lines 9 - 19
}

Then go to the "Code"->"Generate" menu - or Command + N on a Mac - and select "Implement Methods" - to add the three methods that we need:

... lines 1 - 6
class DailyStatsPersister implements DataPersisterInterface
{
public function supports($data): bool
{
}
public function persist($data)
{
}
public function remove($data)
{
}
}

As usual supports() is pretty easy: if a DailyStats is being saved, we want to handle it. So, return $data instanceof DailyStats:

... lines 1 - 5
use App\Entity\DailyStats;
class DailyStatsPersister implements DataPersisterInterface
{
public function supports($data): bool
{
return $data instanceof DailyStats;
}
... lines 14 - 22
}

Next, we don't actually need remove() because we haven't added the delete operation. Oh, but first, I mis-typed $data:

... lines 1 - 7
class DailyStatsPersister implements DataPersisterInterface
{
public function supports($data): bool
{
return $data instanceof DailyStats;
}
... lines 14 - 22
}

Anyways, down in remove(), let's throw a new Exception:

not supported

... lines 1 - 7
class DailyStatsPersister implements DataPersisterInterface
{
... lines 10 - 18
public function remove($data)
{
throw new \Exception('not supported!');
}
}

For persist(), to truly make this work, we should open the fake_stats.json file and change its contents. But... doing that would be pretty boring and simple. So instead, let's fake it and log a message inside persist() with the new totalVisitors value.

To do that, add public function __construct() and autowire LoggerInterface $logger:

... lines 1 - 6
use Psr\Log\LoggerInterface;
class DailyStatsPersister implements DataPersisterInterface
{
... lines 11 - 12
public function __construct(LoggerInterface $logger)
{
... line 15
}
... lines 17 - 34
}

Hit Alt+Enter and go to initialize properties to create that property and set it:

... lines 1 - 8
class DailyStatsPersister implements DataPersisterInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 17 - 34
}

Down in persist, we know that the $data argument will be a DailyStats object. Add a bit of PHPDoc above this: I don't need the @return, but I do want to say that $data will be a DailyStats object:

... lines 1 - 8
class DailyStatsPersister implements DataPersisterInterface
{
... lines 11 - 22
/**
* @param DailyStats $data
*/
public function persist($data)
{
... line 28
}
... lines 30 - 34
}

Inside the method, say: $this->logger->info(), sprintf():

Update the visitors to "%d"

And pass $data->totalVisitors for the wildcard:

... lines 1 - 8
class DailyStatsPersister implements DataPersisterInterface
{
... lines 11 - 25
public function persist($data)
{
$this->logger->info(sprintf('Update the visitors to "%d"', $data->totalVisitors));
}
... lines 30 - 34
}

Let's see if it works! Move back over. I still have my documentation open, so let's just hit "Execute" again. When it finishes... okay! No error and it still says totalVisitors 500.

To prove that our data persister was actually called, go down to the web debug toolbar, hover over the AJAX icon, and open the last request that was made. I'll open in a new tab.

This takes me to the profiler for that request. Go down to logs and... perfect!

Update the visitors to "500"

So adding the put operation was... pretty simple! And we could also use this to support the POST operation if we wanted to allow new items to be created.

Next: for CheeseListing, we added a bunch of built-in filters to allow users to search and filter the results:

... lines 1 - 18
/**
... lines 20 - 45
* @ApiFilter(BooleanFilter::class, properties={"isPublished"})
* @ApiFilter(SearchFilter::class, properties={
... lines 48 - 51
* })
* @ApiFilter(RangeFilter::class, properties={"price"})
* @ApiFilter(PropertyFilter::class)
... lines 55 - 57
*/
class CheeseListing
{
... lines 61 - 217
}

But what if the built-in filters aren't enough? What if we need to add some custom filtering logic? Let's do that next by first creating a custom filter for a Doctrine entity and later creating a custom filter for our DailyStats resource.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.5.7
        "doctrine/annotations": "^1.0", // 1.10.4
        "doctrine/doctrine-bundle": "^2.0", // 2.1.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.4.5", // v2.7.3
        "nelmio/cors-bundle": "^2.1", // 2.1.0
        "nesbot/carbon": "^2.17", // 2.39.1
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.4
        "ramsey/uuid-doctrine": "^1.6", // 1.6.0
        "symfony/asset": "5.1.*", // v5.1.5
        "symfony/console": "5.1.*", // v5.1.5
        "symfony/debug-bundle": "5.1.*", // v5.1.5
        "symfony/dotenv": "5.1.*", // v5.1.5
        "symfony/expression-language": "5.1.*", // v5.1.5
        "symfony/flex": "^1.1", // v1.9.6
        "symfony/framework-bundle": "5.1.*", // v5.1.5
        "symfony/http-client": "5.1.*", // v5.1.5
        "symfony/monolog-bundle": "^3.4", // v3.5.0
        "symfony/security-bundle": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/validator": "5.1.*", // v5.1.5
        "symfony/webpack-encore-bundle": "^1.6", // v1.7.3
        "symfony/yaml": "5.1.*" // v5.1.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.2
        "symfony/browser-kit": "5.1.*", // v5.1.5
        "symfony/css-selector": "5.1.*", // v5.1.5
        "symfony/maker-bundle": "^1.11", // v1.21.1
        "symfony/phpunit-bridge": "5.1.*", // v5.1.5
        "symfony/stopwatch": "5.1.*", // v5.1.5
        "symfony/twig-bundle": "5.1.*", // v5.1.5
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.5
        "zenstruck/foundry": "^1.1" // v1.1.2
    }
}