Last week, I read a post by Beau Simensen about resource location in PHP. It was interesting, because I had already pondered over this topic in the context of Symfony2. Thinking about how to make Beau’s proposal work for Symfony2 brought me to a result that hit me like a stone. Both he and Amy Stephen made me realize how much this could change not just Symfony2, but the PHP ecosystem as a whole. After PSR-0 and Composer, this is the next important step to ensure interoperability between PHP libraries. And it could almost completely replace the new and hotly debated autoloading PSR. Let me take a few minutes of your time to explain why.
Resource Location
In almost every PHP application, you need to locate some kind of resource. A resource, in this context, could be a template file, a configuration file, an image or any other kind of tangible asset in your project. Zend Framework 2, for example, ships translation files for its validation messages. The configuration for using these files looks like this:
$translator = new Zend\I18n\Translator\Translator();
$translator->addTranslationFile(
'phpArray'
__DIR__.'/../vendor/zendframework/zendframework/resources/languages/en.php',
'default',
'en_US'
);
Zend\Validator\AbstractValidator::setDefaultTranslator($translator);
|
You provide an absolute path to the file and the translator will know where to find it. This works well and is generic. Unfortunately, it is also verbose and couples the configuration to the specific directory structure of the zendframework/zendframework package.
Resource Identifiers
Many libraries and frameworks reduce verbosity by inventing some kind of identifier for resources.  Let’s take an example from the CakePHP documentation:
class UsersController extends AppController {
public function view_active() {
$this->layout = 'Contacts.contact';
}
}
|
The string “Contacts.contact†is the identifier of a layout template that is going to be used as layout for the view_active action. This template must be located in one of the following paths:
- /app/Plugin/Contacts/View/Layout/contact.ctp
- /app/View/Plugin/Contacts/Layout/contact.ctp
So it can be shipped with the Contacts plugin, but also overridden in the application. Essentially, CakePHP creates a mapping from resource identifiers (such as “Contacts.contactâ€) to paths in the file system. What happens if you want to use a *.ctp file that is located somewhere else, for example in a package you just installed via Composer? That’s not possible.
Let’s look at another example from Symfony2. In Symfony2′s configuration system, you can import configuration files into other configuration files. Let’s look at a typical routing configuration:
_wdt:
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
prefix: /_wdt
|
Again, a custom kind of resource identifier is used to refer to the wdt.xml file in WebProfilerBundle. What if you want to import a routing configuration file from some place that is not in a bundle? Not possible (except if you use the absolute path, but that’s not portable so I’ll dismiss this solution).
This list goes on, but the pattern repeats.
Autoloading
Let me take a short excursion to talk about autoloading. As you probably know, autoloading refers to the dynamic loading of class files when classes are used for the first time. PSR-0 did a great deal to unify autoloading among PHP projects, and a new PSR (“PSR-Xâ€) is being discussed to improve PSR-0.
The core of PSR-0 is the mapping of class names to paths on the file system. For example:
require __DIR__.'/vendor/composer/ClassLoader.php';
Â
$classLoader = new Composer\Autoload\ClassLoader();
$classLoader->add('Acme\\Demo', '/path/to/acme/demo');
$classLoader->register();
Â
// loads /path/to/acme/demo/Controller/ContactController.php
$controller = new Acme\Demo\Controller\ContactController();
|
Do you see the pattern? Mapping identifiers of classes to PHP files and namespaces to directories is essentially the same as all the other mappings described before.
Let’s extend this example: What if I want to load some file from a location that is known to the autoloader? For example, I know that config.ini is located in the same directory as the class Acme\Demo\Application. The only way to do this right now is by using reflection:
$reflClass = new \ReflectionClass('Acme\\Demo\\Application');
Â
$file = dirname($reflClass->getFileName()).'/config.ini';
|
But reflection is slow.
Uniform Resource Location
What if we could solve all of the above problems and use cases with one simple pattern? Our basic requirements are:
- Map resource identifiers to one or more paths on the file system.
- Use the same identifier pattern across different PHP libraries to make them interoperable.
The good thing: A specification for Uniform Resource Identifiers (URI) already exists (RFC 3986). Why not reuse it?
Let’s see how we could leverage URIs to locate PHP classes and files or directories relative to PHP classes:
$locator = new ResourceLocator();
$locator->addPath('classpath', '/Acme/Demo/', '/path/to/acme/demo');
Â
echo $locator->findResource('classpath:///Acme/Demo/Parser.php');
// => /path/to/acme/demo/Parser.php
Â
echo $locator->findResource('classpath:///Acme/Demo/resources');
// => /path/to/acme/demo/resources
Â
echo $locator->findResource('classpath:///Acme/Demo/resources/config.ini');
// => /path/to/acme/demo/resources/config.ini
|
This is basically the same thing that autoloaders are doing today, but a little more generic. The presented URIs have two parts:
- a scheme: “classpathâ€
- a path: “/Acme/Demo/Parser.phpâ€
The “authority†(“host:portâ€) part is empty, which is why the double slash (“//â€) is immediately followed by the initial slash of the path. Other URI parts like the query (“?queryâ€) are not required as well, but could be added for custom (non-interoperable) implementations.
An autoloader can be based on such a resource locator b turning backslashes into forward slashes:
spl_autoload_register(function ($class) use ($locator) {
include $locator->findResource('classpath:///'.strtr($class, '\\', '/').'.php');
});
|
Many of PHP’s standard functions can be used to work with URIs:
echo dirname('classpath:///Acme/Demo/Parser.php');
// => classpath:///Acme/Demo
Â
echo dirname('classpath:///Acme/Demo/Parser.php');
// => Parser.php
|
As you see, URIs are a very strong concept that decouples resource location completely from the mechanisms of individual framewo
Truncated by the Symfony feed aggregator, read more.