Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

User Class Dto

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

The fastest way to get started with API Platform is by adding these #[ApiResource] attributes above your entity classes. That's because API Platform gives you free state providers that query from the database (which includes pagination and filters) and free state processors that save things to the database.

To use DTOs or Not?

But, as we've seen with DailyQuest, that's not required. And if your API starts to look pretty different from your entities - like you have fields in your API that don't exist in your entity or are named differently - it might make sense to separate your entity and API resource classes.

Right now, our entities are API resources... and that has added some complexity. For example, we have a custom isMine field which is powered by this isOwnedByAuthenticatedUser property: a non-persisted property that we populate via a state provider. And one of the most noticeable things is our huge use of serialization groups. We have to use serialization groups, like treasure:read, so that we can include the properties we want and avoid the properties that we don't want.

This has saved us some time... but increased complexity. So let's get crazy and use a dedicated class for our API from the start. That's often referred to as a "DTO", or "Data Transfer Object". I'll use that term a lot - but for us, it just means "the dedicated class for our API" - like the DailyQuest class.

Removing the API Stuff from User

Alright, folks, commence cleanup! It's time to wipe out all the API-related grime from our pristine User entity. Remove the #[ApiResource()] attribute... both of them, filters and validation. You may still want validation constraints if you're using your entity with the form system... but since we're not, let's clear it. I'm also clearing anything related to serialization... and hunting down hopefully everything that's hiding.

Woh. This class is a lot smaller now. I think that's everything... the use statements on top look good... so... awesome!

Let's also remove the state processor for User, which hashes the plain password. We are going to re-implement many of the things we just deleted, but I want to start with a clean look at things.

Alright, go check out the API docs. We're reduced to "Quest" and "Treasure". I love it!

Creating the DTO / Dedicated ApiResource Class

We're going to start like we did with the DailyQuest. In the src/ApiResource/ directory, create a new class called UserApi... to indicate this is the user class for our API. Inside, add #[ApiResource] above it.

... lines 1 - 2
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
class UserApi

So far, this is just like any other custom API resource. It shows up in the docs... and if we try the GET collection operation, it fails with a 404. Heck, we're even missing the "ID" part in the URL of the item operations.

To fix that, in UserApi, add a public ?int $id = null property... because our users will still be identified by their database id. Oh, and I'm using a public property just to make life easier... and because this class will stay simple, so it's not a big deal.

... lines 1 - 2
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
class UserApi
public ?int $id = null;

The moment we do this... API Platform recognizes that id as the identifier, and our operations are looking good.

While we're here, let's also tweak the shortName. This is called UserApi, which is a terrible name - so change it: shortName: 'User'.

... lines 1 - 6
shortName: 'User',
class UserApi
public ?int $id = null;

Suddenly... this is starting to look like what we had before!

The big missing pieces, like with DailyQuest, are the state provider and state processor. Let's add the state provider next.... but with a twist that leverages a brand-new feature that's going to save us a ton of work.

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.1.x-dev", // 3.1.x-dev
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.10.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
        "doctrine/orm": "^2.14", // 2.16.1
        "nelmio/cors-bundle": "^2.2", // 2.3.1
        "nesbot/carbon": "^2.64", // 2.69.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.23.1
        "symfony/asset": "6.3.*", // v6.3.0
        "symfony/console": "6.3.*", // v6.3.2
        "symfony/dotenv": "6.3.*", // v6.3.0
        "symfony/expression-language": "6.3.*", // v6.3.0
        "symfony/flex": "^2", // v2.3.3
        "symfony/framework-bundle": "6.3.*", // v6.3.2
        "symfony/property-access": "6.3.*", // v6.3.2
        "symfony/property-info": "6.3.*", // v6.3.0
        "symfony/runtime": "6.3.*", // v6.3.2
        "symfony/security-bundle": "6.3.*", // v6.3.3
        "symfony/serializer": "6.3.*", // v6.3.3
        "symfony/stimulus-bundle": "^2.9", // v2.10.0
        "symfony/string": "6.3.*", // v6.3.2
        "symfony/twig-bundle": "6.3.*", // v6.3.0
        "symfony/ux-react": "^2.6", // v2.10.0
        "symfony/ux-vue": "^2.7", // v2.10.0
        "symfony/validator": "6.3.*", // v6.3.2
        "symfony/webpack-encore-bundle": "^2.0", // v2.0.1
        "symfony/yaml": "6.3.*", // v6.3.3
        "symfonycasts/micro-mapper": "^0.1.0" // v0.1.1
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.11
        "symfony/browser-kit": "6.3.*", // v6.3.2
        "symfony/css-selector": "6.3.*", // v6.3.2
        "symfony/debug-bundle": "6.3.*", // v6.3.2
        "symfony/maker-bundle": "^1.48", // v1.50.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.3.2
        "symfony/stopwatch": "6.3.*", // v6.3.0
        "symfony/web-profiler-bundle": "6.3.*", // v6.3.2
        "zenstruck/browser": "^1.2", // v1.4.0
        "zenstruck/foundry": "^1.26" // v1.35.0