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
... 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')]
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\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\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


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\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!

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", //
        "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