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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNow 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
.
// ... 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 | |
{ | |
path: '/api/me', name: 'app_user_api_me') | (|
'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.
After using rector to move from annotations to PHP8 attributes check your "doctrine.yaml" file and change: from:
type: annotation
totype: attribute
indoctrine.orm.entity_managers.{xxx}.mappings key
.I battled for a while after running rector and I was able to find the solution in Stackoverflow.