If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBack 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.
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.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.5.10
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.1.2
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
"doctrine/orm": "^2.4.5", // 2.8.2
"nelmio/cors-bundle": "^2.1", // 2.1.0
"nesbot/carbon": "^2.17", // 2.39.1
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", // 5.2.2
"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.18.7
"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.8.0
"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.23.0
"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.8.0
}
}
Hey, I'm following your steps and PUT works fine but when I try to create a POST request to DTO object that has id as identifier object is persisted successfully in DB but API response is Unable to generate an IRI for myExampleDTO.
Tried adding groups for normalization and denormalization and exclude the ID but did not helped.
Can you please give some advise or clue?
Thanks!