Adding & Populating the Custom Field

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

Let'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
}

Setting Custom Data in the Data Provider

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 isMe Based on the Current User

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.

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
    }
}