Tom Butler's programming blog

PHP: Annotations are an Abomination

Annotations in php

There are increasingly more (mostly symfony based) PHP projects out there that are making use of annotations for configuration (including 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 symphony'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.

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.

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. You can't echo, var_dump() or debug_print_backtrace() in an annotation making it very difficult to debug.

When using Annotations, suddenly configuration is spread across multiple files. To use Symphony routing as an example, there's no way to get an overview of all the routes used by the application. When creating a new route, it's difficult to quickly see if it's already in use. It's impossible to see how the overall application is configured, which, in turn, means making any large scale structural changes means finding and altering every file rather than a single, central configuration. For instance, what if I wanted to restructure all the urls so that everything that was in /users/.../.../ becomes /customers/.../... if the configuration of those is spread amongst a dozen files that becomes a dozen files to locate then edit.

This hints at a separation of concerns issue..

3) 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.

3a) 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.

3b) 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.

4) Separation of concerns/Encapsulation

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 syntax of the routing annotations is changed, every single class using them needs to change as well. 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. A controller written for Symfony2.0 might not work in 2.1 because the syntax for annotation configuration has changed. This is ridiculous on every front. The configuration format should be able to change without breaking anything and will almost inevitably in future, support new features. Using annotation based configuration, the framework developers essentially shoot themselves in the foot because they can't change the syntax without breaking everyone's controllers! The application developers would then need to go through each of their controllers and adjust the syntax accordingly. 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.

Further to that, they break polymorphism entirely. Let's say I have a controller which uses annotations for its route. 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. For developers, this is frustrating. A developer should be able to see a method which type hints a particular interface or class name and be able to pass an object which is an instance of that class or implements the interface. With annotations, the interpreter will allow the class to be passed, but, once the client code starts reading the annotations, it will essentially see the substituted object as having an incomplete API and break because the annotations don't exist.

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!

In short, annotations break the idea of separation of concerns 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.

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 removes this by placing this responsibility back in the client code-- 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 forefit the benefit of the Dependency Injection Container, a method will now always be called with a specific parameter (specified by @InjectParms) 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!

5) Tight coupling

The knock on effect of ignoring separation of concerns is tight coupling.

Even though it shouldn't, Symfony very tightly couples controllers to the framework anyway. Because of this, 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. A better example of this is Symfony's @Inject. It's highly likely that any IoC Container using annotations will use this label. Do you feel like going through every class in the system and changing the format of @Inject because you're using a new branch of the IoC container or another, newer, IoC Container? These annotations do indeed couple your code to an implementation and the rest of the framework stack. It's only because (thankfully) annotations do not have widespread use that they don't cause portability issues.

6) Littered code and portability

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 different 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.

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 and changes the @Inject rules and wonders 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 implementation or force people to use your preferred dependency injection container.. The same goes for your own code. Why lock yourself into an implementation? In 5 years time you may have use for this class, if it's using @Inject and you're no longer using the same Dependency Injection Container you'll wish you hadn't.

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.

7) 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 impossible 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, you have a problem.

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 unneeded inheritance. And if you can achieve this reusability with a workaround, how do you make it clear what's happened to another developer looking at the code?

How should annotations work with polymorphism? If you extend a class should its 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? 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.

8) They break Version Control

For the sake of argument let's say you copy a "products" module from an existing project to another one. It provides functionality for searching for products, listing the results and displaying information about a single product. It's not unreasonable to suggests that this is a common feature for multiple sites. On the 2nd project, you don't want the url 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 because they're on different branches, 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

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 seven points I made above is enough on its own to warrant avoiding using annotations for storing application metadata. The fact there are eight 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.

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.