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 SubscribeLet's ignore all of this data provider stuff for a minute and just pretend that we want to add a nice, normal field to our API. Simple! In User
, add a new property - private $isMe
- and put it in the user:read
group:
... lines 1 - 12 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
... lines 14 - 39 | |
class User implements UserInterface | |
{ | |
... lines 42 - 95 | |
/** | |
* @Groups({"user:read"}) | |
*/ | |
private $isMe; | |
... lines 100 - 265 | |
} |
The only difference between this property and the other properties is that I'm not going to add @ORM\Column
because I don't want to store this field in the database.
Down at the bottom of this class, go to "Code"->"Generate" - or Command
+N
on a Mac - and select getters and setters for the isMe
field. Oh, but let's improve these: add the boolean type-hint on the argument and a bool
return type:
... lines 1 - 39 | |
class User implements UserInterface | |
{ | |
... lines 42 - 256 | |
public function getIsMe(): bool | |
{ | |
return $this->isMe; | |
} | |
public function setIsMe(bool $isMe) | |
{ | |
$this->isMe = $isMe; | |
} | |
} |
So... that's actually all we need to make this part of our API! Open a new tab and go to /api
to check out the documentation. On the get endpoint, click to look at the schema. There it is! An isMe
boolean field.
To be even cooler - which is always my goal - back up on the property... there it is... we can add more docs:
Returns true if this is the currently-authenticated user
... lines 1 - 39 | |
class User implements UserInterface | |
{ | |
... lines 42 - 95 | |
/** | |
* Returns true if this is the currently-authenticated user | |
* | |
* @Groups({"user:read"}) | |
*/ | |
private $isMe; | |
... lines 102 - 267 | |
} |
That's nice because API Platform will automatically use this in the docs. When we look at the schema now on the user operation... there it is!
In a minute, we're going to set this field in UserDataProvider
. But we do have a, sort of, strange situation, because - if we ever called the isMe
field outside of an API call where the data provider is called... the isMe
field won't be set!
Let's be extra cautious. Down in the getter, if $this->isMe
is null, it means it simply hasn't been set. Throw a new LogicException
:
The
isMe
field has not been initialized.
... lines 1 - 39 | |
class User implements UserInterface | |
{ | |
... lines 42 - 258 | |
public function getIsMe(): bool | |
{ | |
if ($this->isMe === null) { | |
throw new \LogicException('The isMe field has not been initialized'); | |
} | |
return $this->isMe; | |
} | |
... lines 267 - 271 | |
} |
Let's finally set this field in UserDataProvider
. My guess is that the getCollection()
method will return an array of users... but let's actually check that. Add $users =
, dd($users)
and, at the bottom, return $users
:
... lines 1 - 9 | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
... lines 12 - 18 | |
public function getCollection(string $resourceClass, string $operationName = null, array $context = []) | |
{ | |
$users = $this->collectionDataProvider->getCollection($resourceClass, $operationName, $context); | |
dd($users); | |
return $users; | |
} | |
... lines 26 - 30 | |
} |
Back at the browser, find the original tab and refresh. Oh! It's not an array of users! It's a Paginator object with a Doctrine Paginator inside! So... this obviously isn't an array, but the Paginator
object is iterable: we can loop over it like an array and then update the isMe
field on each item.
Above this line, let's add some documentation to help my editor: I'll advertise that $users
is an array of User
objects... which is actually a lie... but when we loop over it, we will get User
objects:
... lines 1 - 7 | |
use App\Entity\User; | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
... lines 12 - 18 | |
public function getCollection(string $resourceClass, string $operationName = null, array $context = []) | |
{ | |
/** @var User[] $users */ | |
$users = $this->collectionDataProvider->getCollection($resourceClass, $operationName, $context); | |
... lines 23 - 28 | |
} | |
... lines 30 - 34 | |
} |
Now, do that loop: foreach ($users as $user)
. And inside say $user->setIsMe()
- yay for auto-completion - and set this to true to start:
... lines 1 - 7 | |
use App\Entity\User; | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
... lines 12 - 18 | |
public function getCollection(string $resourceClass, string $operationName = null, array $context = []) | |
{ | |
/** @var User[] $users */ | |
$users = $this->collectionDataProvider->getCollection($resourceClass, $operationName, $context); | |
foreach ($users as $user) { | |
$user->setIsMe(true); | |
} | |
return $users; | |
} | |
... lines 30 - 34 | |
} |
Let's see if it shows up! Move over, refresh and... yes! Every record has isMe: true
.
Setting this to the correct value is probably the easiest part of the whole process. Start by adding a second argument to the constructor - Security $security
- so we can get the authenticated user. I'll hit Alt
+Enter
and go to Initialize Properties to create that property and set it:
... lines 1 - 8 | |
use Symfony\Component\Security\Core\Security; | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
... line 13 | |
private $security; | |
public function __construct(CollectionDataProviderInterface $collectionDataProvider, Security $security) | |
{ | |
... line 18 | |
$this->security = $security; | |
} | |
... lines 21 - 38 | |
} |
Now, before the loop, set $currentUser = $this->security->getUser()
and set isMe
with a simple $currentUser === $user
:
... lines 1 - 10 | |
class UserDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface | |
{ | |
... lines 13 - 21 | |
public function getCollection(string $resourceClass, string $operationName = null, array $context = []) | |
{ | |
... lines 24 - 26 | |
$currentUser = $this->security->getUser(); | |
foreach ($users as $user) { | |
$user->setIsMe($currentUser === $user); | |
} | |
... lines 31 - 32 | |
} | |
... lines 34 - 38 | |
} |
I love it! Try this one last time and... nice! The first one has isMe: true
and then isMe
is false for the others.
We did it! Oh, except that this only works for the collection endpoint. Try going to /api/users/1.jsonld
. Yep!
The isMe field has not been initialized
That makes sense: we only added the logic to the collection data provider, not the item data provider, which is still being done by the core Doctrine item provider. If we want to output the field here, we need to do a little bit more work.
Let's do that next and also find one other - kind of surprising - spot where we also need to set this field.
"Houston: no signs of life"
Start the conversation!
// 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
}
}