X Autoload: Slicing it up
This is part of a series about
X Autoload
Subject: Separation of concerns, composition over inheritance.
Symfony's class loader
The existing autoload libraries found in symfony framework and reused in Drupal 8 core, basically consist of one object doing the entire job:
- UniversalClassLoader implements PEAR and PSR-0. It does find the file for a given class, it checks the file_exists, and it may also load that file (require_once).
- ApcUniversalClassLoader inherits from UniversalClassLoader, and adds a cache layer to the lookups.
While this does work perfectly well, there are a few things not possible with this architecture:
- If you want to replace the UniversalClassLoader with something else, you also need to copy the cache layer into a new class - that's the price of inheritance.
- Class finding and class loading are not separated from each other, making it hard to recombine different implementations of each.
- There is no easy way to mock out the file_exists() calls with a virtual filesystem. This would be very desirable for unit tests.
- There is no way to record missed file_exists() calls and compare them with an "expected" list, which again would be very desirable for unit testing.
Besides that, there are other things we might be not happy about:
- For uncached direct lookups (such as, if apc is not enabled), the symfony autoloader loops over all registered namespaces. If that is one namespace registered per Drupal module, this can be quite a long loop, easily 100 iterations per class lookup.
- In a test I did recently, I measured 14 of these iterations to take the same time as one call to file_exists(), and I estimated 20 to 40 milliseconds extra to be spent this way. The result might be totally different on another machine, but at least there is a measurable effect.
- This flaw can be mitigated by various caching strategies. But, why spend thought on caching, if we can so easily get rid of that loop?
- There is no mechanism to register arbitrary class finder logic for specific namespaces or prefixes.
Slicing it up.
Instead of one inheritance chain, we want separate components that can be wired up to play together.
- xautoload_ClassFinder_NamespaceOrPrefix for direct lookups. This does replace symfony's UniversalClassLoader, but
- without the part that does actually load (require_once) the class. This does not actually bring us a technical benefit, we just like to have our classes as small as possible..
- implementing PSR-0-NG and PEAR-NG with PSR-0 and PEAR on top of it, instead of just PSR-0 and PEAR.
- with support for class finder plugins with arbitrary logic, registered to specific prefixes or namespaces.
- As an implementation detail, we delegate the namespace and prefix lookup to a helper object.
- xautoload_ClassFinder_ApcCache for cached lookups. This does replace symfony's ApcUniversalClassLoader, but
- instead of inheriting from a specific class finder, it can be wired up with any class finder on the planet that implements xautoload_ClassFinder_Interface.
- As a direct consequence, there are no namespace or prefix registration methods on this class - this needs to be done on the injected class finder instead.
- Again, this is a class finder, not a class loader, so it does not actually do any require_once.
- xautoload_ClassLoader for the actual class loading. This class can be instantiated with any implementation of xautoload_ClassFinder.