Reducing redundancy in Doctrine Annotations loading

Submitted by Frederic Marand on

Doctrine Annotations is a central part of the Drupal 8 Plugin API. One of its conceptual annoyances is the need for a separate autoloading process for annotations, which in some scenarios leads to duplicated path declarations. Let us see how to remedy to it.

The Annotations loading process

Doctrine Annotations are classes, and the whole Doctrine annotations process, like PHP autoloading in general (the single autoload function in plain PHP, the SPL autoload stack otherwise), relies upon a single global, the AnnotationRegistry class, to which annotation classes can be registered.

This registry supports three registration mechanismes, as described in its documentation:

  • direct file registration, with the AnnotationRegistry::registerFile($path) method, which is just a wrapper for require_once $path. This is useful to load annotation files containing multiple annotation classes, like Doctrine ORM's own DoctrineAnnotations.php, without going through multiple autoloads
  • namespace(s) registration with the AnnotationRegistry::registerNamespace($ns, $path)/AnnotationRegistry::registerNamespaces($namespaces) methods, which register autoload namespaces to the annotation autoloader, implemented in AnnotationRegistry::loadAnnotationClass.
  • custom loaders with the <code>AnnotationRegistry::registerLoader($callable), to handle specific scenarios

Implementation in PSR-0/PSR-4 projects with Composer

In most cases, registration will be handled by the namespace-based registration. However, since the annotations autoloader is not tied to a specific autoloading scheme, it needs to receive a path parameter for each namespace it handles. In a typical PSR-0/PSR-4 project with Composer, it can mean something like this:

::registerAutoloadNamespace("Symfony\Component\Validator\Constraints", "$basePath/vendor/symfony/validator");
AnnotationRegistry::registerAutoloadNamespace("MyProject\Annotations", "$basePath/src");

As evidenced by that fragment, this means having to specify in code the namespace-to-path mapping already specified in the composer.json file, under the PSR-0/PSR-4 specification, which is a source of extra work and possible errors.

Autoloading error handling

Why is it necessary ? To quote the Doctrine doc:

How are these annotations loaded? From looking at the code you could guess that the ORM Mapping, Assert Validation and the fully qualified annotation can just be loaded using the defined PHP autoloaders. This is not the case however: For error handling reasons every check for class existence inside the AnnotationReader sets the second parameter $autoload of class_exists($name, $autoload) to false. To work flawlessly the AnnotationReader requires silent autoloaders which many autoloaders are not. Silent autoloading is NOT part of the PSR-0 specification for autoloading.

Indeed, the requirement for silent autoloaders is lacking in PSR-0 and had been hotly and repeatedly debated in the PSR-4 discussions (see the PSR-4: The registered autoloader MUST NOT throw exceptions thread for an example), so caution is indeed needed in the absolutely general case.

However, in most cases, the autoloader is known to be silent: Composer's findFile() method is silent, which means this extra caution is not needed in a project where Composer is used for autoloading.

Removing duplication via registerLoader()

So if we want to avoid re-declaring our namespace-to-path mapping and rely on the existing declarations, what can we do ? Create a custom loader and use it with AnnotationRegistry::registerLoader($callable). Since we will want to pass it parameters, and loaders only receive the name of the class to load, we will implement it as a class with the __invoke() magic method.

All this pseudo-loader will have to do will be to answer true for classes in the namespace it handles, to claim it actually found the class, and let the default autoloader work its magic later on, when the Annotations DocParser will try to access the annotation classes.

That way we can parse the example given on the Annotations documentation page without repeating the namespace to path mapping:

That's it: annotation parsing, without duplicated mappings.

Philipp (not verified)

Sun, 2015-08-30 16:50

My friend, you are magician. I was going crazy over the fact that Doctrine didn't like this class name. This is such an elegant solution. Thank you very much!