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!

12
Login or Register to join the conversation
Loenix Avatar

I did not find any tutorial that is really clear about it, to use rector:
Install it using tutorial
Create a reactor.php file at the root of your project

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\Symfony\Set\SensiolabsSetList;
use Rector\Symfony\Set\SymfonySetList;

return function (RectorConfig $rectorConfig): void {
	$rectorConfig->paths([
		__DIR__ . '/src',
	]);
	$rectorConfig->sets([
		DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
		SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES,
		SensiolabsSetList::FRAMEWORK_EXTRA_61
	]);
};

Then run vendor/bin/rector
Remove rector

Reply

Hey Loenix,

Do you mean the instructions in this video about how to use Rector are not clear for you? Because it's basically as you said, you need to install Rector in your system in your preferred way (or follow the way we use in this course), then create a configuration for it, and run the Rector to actually do the changes according to the configuration you created. Yeah, you can remove Rector and its config completely after it, or keep the config for the future when you will upgrade your Symfony version, PHPUnit version, or PHP version later - you would just need to tweak the config file... and probably upgrade Rector to the latest, it would be a good idea :)

Cheers!

Reply
Loenix Avatar

Yes, It was I meant, I have read this tutorial and the doctrine documentation about it, there was some differences and I was unable to understand what the rector.php file is and where to put it. So I mixed your documentation and try something and it worked, so I though a comment was useful for futur readers.

Reply

Hey Loenix,

Thanks for sharing some tips with others. That rector.php file is a config file that will be read by Rector tool telling it what exactly it should do, i.e. it specifies some rules for Rector. And usually, you put such config files in the project root dir, like the PHPUnit config file, etc.

I hope that help!

Cheers!

Reply
Cameron Avatar

A brief comment on why php introduced attributes (and where to find more info) would be useful or what could be done with them. Then the training would be a bit more holistic instead of just the mechanics of upgrading. It's mine and likely a few other people's first time seeing this php change and we don't understand it

Reply
weaverryan Avatar weaverryan | SFCASTS | Cameron | posted 8 months ago | HIGHLIGHTED

Hey Cameron!

Very fair suggestion! It's sometimes hard to back up as an instructor and remember all the little details. I can at least give some explanation here. In short, annotations were created around 10 years ago by Doctrine, mostly so that we could add the @ORM\Column annotations inside entities. Other languages have similar syntaxes. Over the years, these got more and more popular, with things like @Route, assertions for validation constraints, etc.

But, annotations were still not part of PHP's language. They're just comments, and Doctrine maintained a library that could read and parse those comments. It worked great, but it's still a bit odd for us to "invent" this new way of adding configuration into the comments system and not have it be part of PHP. And so, someone finally proposed putting into PHP itself. It has a slightly different syntax by necessity: we needed to introduce some new syntax that nobody could possibly be using already in their app (so that the introduction of PHP attributes wouldn't suddenly start making your code do something). Of course, there was a lot of debate on the syntax, and this was finally chosen as a balance between attractive/logical/simple.

I hope that helps - thanks for commenting about this :).

Cheers!

1 Reply
Fabrice Avatar

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