PHP: Annotations are an Abomination
Update 21/08/2018
Added comparison to getters
Update 16/11/2017
Clarified note regarding Symfony.
Update 17/06/2017
I disliked the wording in parts of this article so I've restructured parts of it.
Annotations in php
Annotations are in the same realm as global variables and singletons. They offer the developer a minor convenience at the expense of flexibility, encapsulation and various other traits we strive for as programmers.
There are increasingly more (mostly Symfony based) PHP projects out there that are making use of annotations for configuration (such as dependency injection and routing). However, how anyone can think that teaching people this is acceptable or a good practice, is frankly, ridiculous. Those people developing such high profile frameworks should know better.
Take Symfony's routing class example (http://symfony.com/doc/2.0/bundles/SensioFrameworkExtraBundle/annotations/routing.html)
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class PostController extends Controller
{
/**
* @Route("/")
*/
public function indexAction()
{
// ...
}
}
I'm sure it works perfectly well. But so do global variables and singletons. It doesn't make them right and it certainly doesn't make them the best tool for the job.
There are many reasons using annotations is a bad idea. I'm going to use this routing class as an example, but all these issues are with annotations in general, not just Symfony's routing class.
Yes, I know in Symfony this approach is optional but the problems I highlight here are issues with annotations in principle, not only with this specific example. If you use annotations, you fall into the problems I present here. And for what? Just so you can store metadata in the same file as your code.
I will also stress that this isn't just because annotations are not a proper language feature in PHP, the same arguments apply whether PHP supports annotations officially or not.
Preface: Annotations are the OOP equivalent of HTML's style attribute.
/**
*@Route("/users/list")
*/
public function listAction() {
}
is the equivalent of
<div style="width: 120px;">
Just as the style attribute couples display information with the markup, annotations couple application configuration with application logic.
By using a CSS stylesheet instead of the style attribute, it's easy to substitute the CSS and apply different CSS styles to the pages. The CSS code can be organised in its own file and the same markup can even be used where no CSS is required.
Annotations do the same thing: They move application configuration, which is otherwise unrelated to the logic in the class (A class has no concept of a "route", only methods and properties) which is then used to configure another entirely unrelated class (in this case the router) somewhere else in the project. The class contains data/configuration that is used solely by another, entirely unrelated, class and a different tier of the architecture.
With either external CSS or external configuration, it's possible to easily replace the CSS/configuration. The entire look of the website or configuration of the router can be changed without needing to amend the HTML markup or the class itself.
While reading through the rest of this article, keep that in mind. More importantly though, I want to make it clear that:
The single benefit of annotations is the minor convenience of editing application logic and application configuration in the same file
What can be achieved with annotations can also be achieved using a data structure hardcoded elsewhere in the application or loaded from a JSON/XML/Whatever configuration file.
The minor convenience often becomes an inconvenience as I'll outline shortly. As you're reading through the negatives introduced by annotations, keep weighing them up against this singular entry of minor convenience the pros column.
1. Changing comments should not break things
Every novice programmer is taught that comments are never executed. Making them integral for required functionality is ludicrous. If the comments get removed/altered then it becomes very difficult to work out why the application suddenly doesn't work.
Breaking this very well understood and fundamental language feature is absurd.
That said, annotations for IDE hinting are perfectly acceptable because the script still executes successfully whether they're there or not.
Yes, this is a minor thing, I'm starting small.
2. Increased difficulty in development and debugging
A developer unfamiliar with the annotation meta-language will find it difficult to program in. There's no IDE hints, there's no IDE syntax checking, the syntax is unfamiliar to them. What's worse, because configuration is spread across multiple files, they can't even easily copy/paste examples from what's already there. Doing any debugging becomes impossible because you can't echo, var_dump() or debug_print_backtrace() in an annotation.
3. Action At A Distance
The biggest problem global variables cause is that code in one part of the application changes some global state which has a knock on effect elsewhere in the application.
Annotations do the same thing: Adding a controller with routes causes a state change in the router at a completely different layer of the application. The router is suddenly not in charge of its own state and the controller doesn't even have a dependency on the router.
4. Inconvenience
Despite the only benefit of annotations being convenience of editing configuration and logic in the same place, they are sometimes more inconvenient than not.
If you own a site, get a bug report that says There is a bug on this page with a URI, you will want to find the corresponding controller. If you are using annotations for your routes as in the example above, you have to look through the annotations in potentially hundreds of methods across dozens of controllers.
If you want to update your routes so that /users/*
becomes /customers/*
you have to go searching across all your controllers.
Adding a new route also becomes a problem. How can you tell if it's already in use?
Because of these problems, Symfony allows you to run the command debug:router
which lets you find a corresponding controller to a route. This screams "I caused a problem by spreading my configuration around multiple files so I had to create a tool to solve it. It's definitely not that doing so was a poor choice to begin with".
Instead, using a centralised routing file, you can easily lookup the controller/action that is mapped to a given route in a single file.
As Symfony does, you can create tools to overcome these issues, but the fact these tools are necessary hints at a separation of concerns issue...
5. Not SOLID
In OOP we strive for SOLID code. The wikipedia page has a good explanation of SOLID principles for anyone unaware of this term.
Annotations break two of these.
5a. Single Responsibility principle
The S in solid.
the single responsibility principle states that every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
Annotations break this because when Annotations are used, the class provides both the behaviours of an object and the configuration for an entirely different part of the system (e.g. providing configuration for a router or a dependency injection container).
Robert C. Martin, who coined the term Single Responsibility Principle described it as
A class should have one, and only one, reason to change
If you are using annotations, your code has at least two reasons to change: A change to the configuration or a change to the logic. This causes a specific problem when it comes to version control as I'll show later on.
5b. Liskov substitution principle
The L in SOLID
It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).
Annotations unquestionably break this because if a subclass does not provide identical annotations, then the subclass cannot be used in place of the parent class without altering the behaviour of the code. When using annotations, interfaces become irrelevant. We can't provide an interface and a implementation separately. If one implementation provides annotations and another doesn't we'll get a completely different result. If annotations are required for the code to run, they have become part of the interface.
6. Annotations are glorified getters
Take a look at this example from Symfony (https://symfony.com/doc/current/routing.html):
class BlogController extends AbstractController
{
/**
* Matches /blog exactly
*
* @Route("/blog", name="blog_list")
*/
public function list()
{
// ...
}
/**
* Matches /blog/*
*
* @Route("/blog/{slug}", name="blog_show")
*/
public function show($slug)
{
// $slug will equal the dynamic part of the URL
// e.g. at /blog/yay-routing, then $slug='yay-routing'
// ...
}
}
This same configuration could be expressed as a simple getter:
class BlogController extends AbstractController
{
public static function getRoutes() {
return [
'list' => ['/blog', 'blog_list'],
'show' => ['/blog/{slug}', 'blog_show']
];
}
public function list()
{
// ...
}
public function show($slug)
{
// $slug will equal the dynamic part of the URL
// e.g. at /blog/yay-routing, then $slug='yay-routing'
// ...
}
}
Whatever is reading the annotation could be changed to call BlogController::getRoutes()
to retrieve the configuration.
Most people will dislike this code because it has hardcoded static values. I hate to break it to you but Annotations are hardcoded static values.
This approach is marginally less convenient than annotations but if you are arguing in favour of annotations from a theoretical perspective, you also need to argue that this is a good idea as both pieces of code can produce the same result and both introduce the same set of problems.
7. Separation of concerns
The biggest issue is one of OOP theory. In OOP we strive for Separation of Concerns whereby one part of the system can have its implementation entirely changed without having knock-on effects anywhere else. To use the Symfony routing example, the routing table structure, internal terms/names and the entire routing mechanism should be able to be changed without affecting any existing controller code.
Annotations make this impossible. Using them means that if the implementation of the router is changed, every single class using them needs to change as well.
For example, if the router was changed to replace the term {slug}
with {uri}
every single controller would need to be amended.
"But my configuration isn't going to change, so I may as well use annotations"
Some would argue "my dependencies aren't going to change so I may as well use singletons everywhere". It's the same argument and it makes the same unfounded assumption: You know exactly what's going to happen in the future. Although to non-programmers it sometimes looks like programmers have super powers, I haven't yet met a clairvoyant developer. You have no idea how things are going to change in the future and writing code assuming that nothing will change is asking for pain down the line.
With separated metadata, the metadata format can be altered entirely with ease and without needing amends to the code itself. E.g. replacing an XML routing table with a JSON routing table would be relatively painless. However, replacing annotation based routing with JSON routing becomes an ordeal.
This creates horrible backwards compatibility issues for both app developers and framework authors
Symfony cannot easily change the implementation of the router without considering backwards compatibility. A controller written for Symfony 2.0 might not work in 3.0 because the syntax for annotation configuration has changed.
It is not far fetched to strive for a world where the router can be replaced without needing to go off and amend all the controllers.
Using annotation based configuration, the framework developers essentially shoot themselves in the foot because they can't change the annotation syntax without breaking everyone's controllers!
With an XML, JSON or PHP based routing table, it's a change in one file to bring all outdated modules into a newer version of the framework. Not only that, automated tools can easily be written to convert an XML configuration to JSON. Re-writing annotations is a lot more work (and causes a rather obscene commit containing every controller in the project!)
The most laughable use of annotations for configuration is @Inject
which allows the annotation to configure the Dependency Injection Container.
The Dependency Injection Container is there for the purpose of separation of concerns; client code should not be concerned with
where its dependencies come from or the state of them. Using @Inject
makes the class using the annotation aware of the existence and implementation of the container. It knows the container exists and it knows that the container is looking for a specific annotation.
This is exactly the opposite of what was trying to be achieved by choosing to use a Dependency Injection Container in the first place! By
using @InjectParams
in Symfony you forfeit the benefit of the Dependency Injection Container, a method will now always be called with a specific parameter (specified by @InjectParams
) and cannot easily be injected with a subclass or other implementation which uses the same interface.
This removes any flexibility and is almost as bad as ServiceLocator::Locate('Somedependency')
yet adds two layers of complexity!
Yes, the @InjectParams
rules can be overridden. So to can the style attribute with !important
in a CSS file. By overriding the annotation at a higher level you have essentially implemented the equivalent of !important
for your configuration. Most of the arguments against !important also apply to your fancy "ignore the annotations and use this configuration instead" hack.
8. Broken Encapsulation
Wikipedia defines encapsulation as:
A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data
If you are using annotations, the data is stored in a class and the operated on by whatever reads the annotations at a completely different layer of the application.
Whoever writes the controller code knows about the router. By providing @Inject
, the class author knows that the class is going to be running in an environment in which something is looking for that annotation in that particular format. The annotation provides configuration for a part of the application that the author of the class should not even be aware exists.
In short, annotations break encapsulation by mixing metadata with the code it's related to. Why should a controller care what its route is? Why would a controller need to understand the concept of a "route" in the first place? It plainly breaks encapsulation by exposing external implementation to irrelevant classes. Objects should not have control over how they may be used externally and only be concerned with their own responsibility.
I call this kind of encapsulation break "looking up"
9. Breaks polymorphism
Take the BlogController
class example.
In OOP, I should be able to substitute this class with a child class or any other class which has the same interface.
Annotations make this impossible because the annotations have become part of the class API and must be supplied alongside the class.
In OOP we strive for polymorphism. I can interchange instances of different classes provided they have the same API. However, annotations break this. If I substitute a controller for a class which has an identical interface, it simply will not work unless it also supplies the annotations.
Someone should be able to implement an interface and use it in place of the original class. However, if the original class has annotations, simply implementing another class with the same interface is not enough: The new class is incomplete because it doesn't supply the additional configuration.
Ironically, in this scenario, the initial purpose of annotations which was to allow for better documented code has been entirely undermined. Because annotations are now part of the class API, type hinting and other self-documenting inbuilt language features are useless! I can pass a class that implements an interface but because whatever is using the class is looking for annotations, the API is incomplete without them.
For this purpose, getters in the interface would be superior, whoever implements the interface would then know they need to provide the configuration. Using annotations there is no way to enforce this contract.
10. Tight coupling
The knock on effect of ignoring separation of concerns and broken encapsulation is tight coupling.
As Symfony tightly couples controllers to the framework anyway the above example doesn't cause any additional problems related to coupling which is probably why Symfony users are happy to overlook it; the controllers need the framework stack in order to work at all. But in the real world, where we use loose coupling, this could well present very large and very real problems.
You may argue "But the annotations don't couple the component to the framework. I can move the code elsewhere and it will work in isolation". This is half true.
It will indeed work in isolation. But what if every framework used annotations for configuration? Now your annotations cause an issue if you try to use a Symfony class in Zend or Zend class in Symfony.
The annotations won't be understood correctly and will likely break things. As it stands, it's only through obscurity that they don't cause issues with portability.
If annotations became commonplace, we'd get to a point where they required CSS style vendor prefixes.
@--Symfony-Inject("security.context", required = false)
@--PHPDI-Inject({"security.context"})
private $adminCheck;
Joyous.
If annotations were avoided in the first places you could just configure the DIC externally and never worry about supporting different implementations in your classes.
By using annotations you are writing a class that will only work in the environment in which it was designed. You forfeit code reuse. And for what? So you can put the configuration in the same file as the logic?
11. Say goodbye to portability/code reuse
Let's say you replace Symfony's router with a different one. Not an unreasonable request-- Symfony build their components with modularity in mind, don't they? However, because of the existing annotations littered throughout your code, you can't use a new router that uses annotations because of name clashes. Perhaps the convenient annotation called @Route wasn't such a great idea after all?
You can't even easily port existing routes to the new router because you don't have a complete list of them in a centralised location. You have to go through every single controller action one by one updating the @Route syntax to the new router implementation. Ouch.
Worse though, is portability of classes between projects. A lot of websites have similar requirements, if you build a shopping cart system on one site it will no doubt be useful on another.
Imagine you copy some classes between projects. The first project used @Inject
to inject dependencies, the second one doesn't.
Because it's time consuming you leave the @Inject
annotations
in the classes. They're harmless comments, right?
Of course, anyone else looking at your code sees the @Inject
annotation and assumes that's how it works,
change the @Inject
rules and wonder why it doesn't alter the behaviour.
Not a huge problem in itself, but think about it: would you publish a class you intended other developers to use in their own projects with annotation based configuration?
No, you wouldn't. You wouldn't because you wouldn't want to lock people into a specific environment. You wouldn't expect people to only run the code in an environment that had your preferred router or dependency injection container (or whatever is reading the annotation). Why would you want to do that very thing to your future self?
You're just passing these problems on to your future self when you find a use for the class in the framework du jour that no doubt uses a completely different router, DIC, etc. Why would you want to lock yourself into an implementation and reduce the reusability of your own code?
Even ignoring futureproofing, imagine you have two Symfony projects both using its annotation based router and you want to move some code between the two projects. This makes you ask a lot of questions: Are the routes I've used in my controller already in use on that project? Is the new project using a different annotation-based router that might clash? To answer these, you'll need to do a lot of digging around in the 2nd project. If you'd used a centralised routing table, these issues are easily resolved and none of the copied classes need ever be modified or even examined.
12. Flexibility
Probably the most obvious problem people will hit is that using annotations in this way immediately limits flexibility.
The self-configuring class is stuck with that configuration forever.
It's difficult to initiate two instances of the class with different configurations.
Using the @Inject
example, unless you avoid using the injection container or have some horrible workaround, the DIC
must create an instance of your class with the configuration designated in @Inject
. As soon
as you want to wire the class differently, it becomes needlessly difficult.
Essentially, the class has become static. It's impossible to reuse it with another configuration. If I want to use the same controller for a different route, using annotations makes this very difficult without some ugly hacks such as controller action chaining or unnecessary inheritance. And if you can achieve this reusability with this hacky workaround, how do you make it clear what's happened to another developer looking at the code?
Aside: The underlying problem is that most frameworks tightly couple the controllers to the views and the models which means there is no way to reuse them anyway. But that's another topic for another day.
How should annotations work with polymorphism? If you extend a class should it inherit its parents annotations be extended as well? What if, in the router example, you want the route to dynamically point you to a relevant subclass? The power of polymorphism is completely lost in this case because routes have become hardcoded! Hardcoding is always a bad idea!
In effect, annotations force your classes to be static. How can you have two instances of the same class with different configurations? The same controller with different class variables and different routes? This is a core concept in OOP but annotations break this because they configure objects at a class/static level (Configuration will apply to every object that is an instance of the class) rather than an object level where each object can have its own configuration.
13. They break Version Control
For the sake of argument let's say you copy a "products" module from an existing project to another site. It provides functionality
for searching for products, listing the results and displaying information about a single product. It's not unreasonable to suggest that
this is common feature for multiple sites.
On the second project, you don't want the URI to be /products/
because you want something a lot more specific.
Your client's site sells cars so you want the url to be /cars/
instead of /products/
. If you'd used annotations you'd have to go through
and alter each route. A minor inconvenience and only marginally more difficult than altering routes in a separate, centralised routing table.
However, introduce Version Control into this scenario and you've created a problem for yourself. Your second site is now using a new branch of the code and only because the routes have changed! You can't fix a bug in the products module and push it to each of your client sites that are using it. Each site is on its own branch of the code solely because you have hardcoded routes into the controllers using annotations. Whoops!
From a business perspective this is terrible, had you kept to a single branch you could very easily sell new features that were developed to each of your clients and deploy them at the press of a button; by pushing the new version to their site. But by using annotations and creating a separate branch you make this far more difficult for yourself.
This is a perfect example of why mixing application metadata into the application code is a very bad idea. It prevents you from sharing the application code between projects which are (almost inevitably!) configured differently.
Conclusion
Let's keep in mind that the sole benefit to using annotations for configuration is the minor convenience of storing application configuration and application logic in the same file. That's it! That is the only reason to use annotations over an alternative approach. Is this minimal convenience ever worth it? I'd argue definitely not.
Using annotations for configuration is horrible. It goes against common sense, basic programming principles, prevents debugging and breaks two fundamental OOP principles: Encapsulation and Polymorphism as well as making version control and sharing code between projects far more challenging.
Annotations for this purpose go against everything we strive for as programmers. It both annoys and worries me that very high profile projects are making use of them in this manner.
They are a very useful tool for documenting code. However, they should not be abused to configure an application. Each of the thirteen points I made above is enough on its own to warrant avoiding using annotations for storing application metadata. The fact there are thirteen real world problems (likely more, I probably missed some obvious ones!) introduced by their use makes them totally unfit for any purpose.
This is one of the few cases where I'd even go as far as saying: They should never be used. With a lot of bad practices, there are certain scenarios where they can add some benefit, usually because they're a shortcut and make development time significantly faster (even if they introduce problems as the project grows) or avoid a lot of complexity so make sense for small projects. However, annotations add complexity and don't offer enough of a benefit at all to warrant any use. The single benefit is that it allows programmers to edit two sets of data (application logic and application configuration) within a single file. Compared with the problems they cause this is not enough to ever encourage their use.
Annotations - For | Annotations - Against |
---|---|
Edit application configuration and application logic in the same file | Counterintuitive - changing comments affects application flow |
Cannot be debugged easily (cannot var_dump an annotation) | |
Introduces action at a distance | |
Breaks the Single Responsibility Principle | |
Breaks the Liskov Substitution Principle | |
Breaks Encapsulation | |
Breaks Separation of Cocerns | |
Makes Polymorphism significantly more difficult | |
Introduces coupling between unrelated components (e.g. every class and a dependency injection container or controllers and a router) | |
Makes code less portable | |
Makes it more difficult to instantiate the same class with multiple configurations. | |
Makes version control more difficult as different configurations require different branches |
Like global variables and singletons, they offer some minor convenience (much more minor convenience than singletons or globals I might add) at the expense of clean, maintainable, flexible and portable code. Is it worth sacrificing all that for a very minor convenience?
In my opinion, this is one of those fad-of-the-month trends, like singletons, which have a sudden surge in popularity because people are playing with new language features but after a few years the downsides will have become painfully apparent to people who've used them in the real world and we'll see an influx of "is evil" and "are bad practice" articles related to them.
Update 2018: Called it
When I first published this article five and half years ago you'll notice I finished by saying: we'll see an influx of "is evil" and "are bad practice" articles related to them..
- https://dzone.com/articles/are-annotations-bad
- https://www.yegor256.com/2016/04/12/java-annotations-are-evil.html
- https://dzone.com/articles/java-annotations-are-a-big-mistake
- https://javadevguy.wordpress.com/2016/01/13/evil-annotations/
- https://blog.softwaremill.com/the-case-against-annotations-4b2fb170ed67
Ok perhaps "influx" was a bit overblown but most of these are Java related where developers have had time to fall into the various traps that annotations create.
Some of these are highlighting problems with individual annotations. Because they have encountered one or more of the issues above when using that particular annotation. These problems exist with annotations as a programming practice and this list will only grow.