Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Annotations to Attributes

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 $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Now that we're on PHP 8, let's convert our PHP annotations to the more hip and happening PHP 8 attributes. Refactoring annotations to attributes is basically just... busy work. You can do it by hand: attributes and annotations work exactly the same and use the same classes. Even the syntax is only a little different: you use colons to separate arguments... because you're actually leveraging PHP named arguments. Neato.

Configuring Rector to Upgrade Annotations

So, converting is simple... but oof, I am not excited to do all of that manually. Fortunately, Rector comes back to the rescue!! Search for "rector annotations to attributes" to find a blog post that tells you the exact import configuration we need in rector.php. Copy these three things. Oh, and starting in Rector 0.12, there's a new, simpler RectorConfig object that you'll see on this page. If you have that version, feel free to use that code.

Oh, and before we paste this in, find your terminal, add everything... and then commit. Perfect!

Back over in rector.php, replace the one line with these four lines... except we don't need the NetteSetList... and we need to add a few use statements. I'll retype the "t" in DoctrineSetList, hit "tab", and do the same for SensiolabsSetList.

35 lines rector.php
... lines 1 - 6
use Rector\Doctrine\Set\DoctrineSetList;
... lines 8 - 9
use Rector\Symfony\Set\SensiolabsSetList;
... lines 11 - 14
return static function (ContainerConfigurator $containerConfigurator): void {
... lines 16 - 24
$containerConfigurator->import(DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES);
$containerConfigurator->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES);
$containerConfigurator->import(SensiolabsSetList::FRAMEWORK_EXTRA_61);
... lines 28 - 33
};

Now, you know the drill. Run

vendor/bin/rector process src

and see what happens. Whoa... this is awesome! Look! It beautifully refactored this annotation to an attribute and... it did this all over the place! We have routes up here. And all of our entity annotations, like the Answer entity have also been converted. That was a ton of work... all automatic!

... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends BaseController
{
#[Route(path: '/api/me', name: 'app_user_api_me')]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
public function apiMe(): \Symfony\Component\HttpFoundation\Response
{
... lines 14 - 16
}
}

... lines 1 - 11
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
... lines 20 - 203
}

Fixing PHP CS

Though it did, as Rector sometimes does, mess up some of our coding standards. For example, all the way at the bottom, it did refactor this Route annotation to an attribute... but then it added a little extra space before the Response return type. That's no problem. After you run Rector, it's always a good idea to run PHP CS Fixer. Do it:

tools/php-cs-fixer/vendor/bin/php-cs-fixer fix

Love it. A bunch of fixes to bring our code back in line. Run

git diff

to see how things look now. The Route annotation changed into an attribute... and PHP CS Fixer put the Response return type back the way it was before. Rector even refactored IsGranted from SensioFrameworkExtraBundle into an attribute.

But if you keep scrolling down until you find an entity... here we go... uh oh! It killed the line breaks between our properties! It's not super obvious on the diff, but if you open any entity... yikes! This looks... cramped. I like the line breaks between my entity properties.

... lines 1 - 9
class Answer
{
use TimestampableEntity;
public const STATUS_NEEDS_APPROVAL = 'needs_approval';
public const STATUS_SPAM = 'spam';
public const STATUS_APPROVED = 'approved';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'text')]
private $content;
... lines 22 - 48
public function getUsername(): ?string
... lines 50 - 113
}

We could fix this by hand... but I'm wondering if we can teach PHP CS Fixer to do this for us.

Open php-cs-fixer.php. The rule that controls these line breaks is called class_attributes_separation with an "s" - I'll fix that in a minute. Set this to an array that describes all of the different parts of our class and how each should behave. For example, we can say ['method' => 'one'] to say that we want one empty line between each method. We can also say ['property' => 'one'] to have one line break between our properties. There's also another called trait_import. Set that to one too. That gives us an empty line between our trait imports, which is something that we have on top of Answer.

... lines 1 - 7
return $config->setRules([
... lines 9 - 10
'class_attributes_separation' => [
'elements' => ['method' => 'one', 'property' => 'one', 'trait_import' => 'one']
]
])
... line 15
;

Now try php-cs-fixer again:

tools/php-cs-fixer/vendor/bin/php-cs-fixer fix

Whoops!

The rules contain unknown fixers: "class_attribute_separation"

I meant to say class_attributes_separation with an "s". What a great error though. Let's try that again and... cool! It changed five files, and if you check those... they're back!

... lines 1 - 9
class Answer
{
use TimestampableEntity;
public const STATUS_NEEDS_APPROVAL = 'needs_approval';
public const STATUS_SPAM = 'spam';
public const STATUS_APPROVED = 'approved';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'text')]
private $content;
... lines 25 - 120
}

With just a few commands we've converted our entire site from annotations to attributes. Woo!

Next, let's add property types to our entities. That's going to allow us to have less entity config thanks to a new feature in Doctrine.

Leave a comment!

6
Login or Register to join the conversation
Fabrice Avatar
Fabrice Avatar Fabrice | posted 1 month ago

Hey! Attributes are great! But I have a question. I heard that you can use functions inside attributes uses for repetitive things.

I am thinking of a specific use case on API Platform for example (but this applies almost everywhere in reality).

Imagine, a Post entity. We want to create a custom action with API Platform that targets "/api/posts/count" to return the number of posts.

Now imagine that we would like to write the OpenAPI documentation for this action from our Post entity.

It could look like this:

#[ApiResource(
    collectionOperations:
        'get',
        'post',
        'count' => [
            'method' => 'GET',
            'path' => '/posts/count',
            'controller' => PostCountController::class,
            'openapi_context' => [
                'summary' => 'Get posts count',
                'parameters' => [
                    [
                        'in' => 'query',
                        'name' => 'online',
                        'schema' => [
                            'type' => 'integer',
                            'maximum' => 1,
                            'minimum' => 0
                        ],
                        'description' => 'Filter posts by online'
                    ],
                ],
                'response' => [
                    '200' => [
                        'description' => 'OK',
                        'content' => [
                            'application/json' => [
                                'schema' => [
                                    'type' => 'integer',
                                    'example' => 3
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
)]

Which is huge (I voluntarily took this kind of case but you can imagine that it applies to other things).

Therefore, the API Platform configuration of the Post entity is less readable and it would be necessary to move for example the entire value of the "count" key somewhere, in a private function, or even in an "api_doc" class which would contain functions with the configuration of each of our custom actions, and thus be able to simply call them from the entity.

Let's say we create a function getCountActionConfig() which would return all the content currently present in the 'count' key, allowing us to do:

#[ApiResource(
    collectionOperations:
        'get',
        'post',
        'count' => $this->getCountActionConfig()
)]

Only, it doesn't seem possible, since the call to the method will be underlined in red by PhpStorm with the following error: Constant expression contains invalid operations.

Moving the contents of the function directly into a constant would work I guess, but the goal would still be to do it through a function. And we could even go further by having the possibility of automatically generating the configuration by passing the name of the method, the description...

But a priori it does not seem possible. Do you have a solution for this?

Reply

Hi,

Have you tried static function?

#[ApiResource(
    collectionOperations:
        'get',
        'post',
        'count' => YourClassName::getCountActionConfig()
)]

Cheers!

Reply
Fabrice Avatar

Hello, sorry for delay, yes I tried and no, same problem, I'll have the same error Constant expression contains invalid operations

Reply

woh yeah, looks like the only way is to define a constant with the configuration you need and then it will work

I hope so )

Cheers!

Reply
Fabrice Avatar

Yes, this is so bad. I have to create a separate file that will contain a multitude of constants representing the configurations of my API Platform custom actions. It works, but...

It would be interesting if in a future PHP update we could use functions.

Thanks for your answers anyway!

Reply

Agree that's not very useful now, maybe something will change in future, so lets wait =)

Cheers and happy coding!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.6", // v3.6.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.5
        "doctrine/annotations": "^1.13", // 1.13.2
        "doctrine/dbal": "^3.3", // 3.3.5
        "doctrine/doctrine-bundle": "^2.0", // 2.6.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.0", // 2.11.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
        "knplabs/knp-time-bundle": "^1.18", // v1.18.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.6
        "sentry/sentry-symfony": "^4.0", // 4.2.8
        "stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.7
        "symfony/console": "6.0.*", // v6.0.7
        "symfony/dotenv": "6.0.*", // v6.0.5
        "symfony/flex": "^2.1", // v2.1.7
        "symfony/form": "6.0.*", // v6.0.7
        "symfony/framework-bundle": "6.0.*", // v6.0.7
        "symfony/mailer": "6.0.*", // v6.0.5
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/property-access": "6.0.*", // v6.0.7
        "symfony/property-info": "6.0.*", // v6.0.7
        "symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
        "symfony/routing": "6.0.*", // v6.0.5
        "symfony/runtime": "6.0.*", // v6.0.7
        "symfony/security-bundle": "6.0.*", // v6.0.5
        "symfony/serializer": "6.0.*", // v6.0.7
        "symfony/stopwatch": "6.0.*", // v6.0.5
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-chartjs": "^2.0", // v2.1.0
        "symfony/validator": "6.0.*", // v6.0.7
        "symfony/webpack-encore-bundle": "^1.7", // v1.14.0
        "symfony/yaml": "6.0.*", // v6.0.3
        "symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/string-extra": "^3.3", // v3.3.5
        "twig/twig": "^2.12|^3.0" // v3.3.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
        "phpunit/phpunit": "^9.5", // 9.5.20
        "rector/rector": "^0.12.17", // 0.12.20
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/maker-bundle": "^1.15", // v1.38.0
        "symfony/var-dumper": "6.0.*", // v6.0.6
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.6
        "zenstruck/foundry": "^1.16" // v1.18.0
    }
}