The PrefixPathLoader¶
Overview¶
Zend Framework’s 1.X series introduced a plugin methodology surrounding associations of vendor/component prefixes
and filesystem paths in the Zend_Loader_PluginLoader
class. Zend Framework 2 provides equivalent functionality
with the PrefixPathLoader
class, and expands it to take advantage of PHP 5.3 namespaces.
The concept is relatively simple: a given vendor prefix or namespace is mapped to one or more paths, and multiple prefix/path maps may be provided. To resolve a plugin name, the prefixes are searched as a stack (i.e., last in, first out, or LIFO), and each path associated with the prefix is also searched as a stack. As soon as a file is found matching the plugin name, the class will be returned.
Since searching through the filesystem can lead to performance degradation, the PrefixPathLoader
provides
several optimizations. First, it will attempt to autoload a plugin before scanning the filesystem. This allows you
to benefit from your autoloader and/or an opcode cache. Second, it aggregates the class name and class file
associated with each discovered plugin. You can then retrieve this information and cache it for later seeding a
ClassMapAutoloader and PluginClassLoader.
PrefixPathLoader
implements the ShortNameLocator
and PrefixPathMapper
interfaces.
Note
Case Sensitivity
Unlike the PluginClassLoader, plugins resolved via the
PrefixPathLoader
are considered case sensitive. This is due to the fact that the lookup is done on the
filesystem, and thus a file exactly matching the plugin name must exist.
Note
Preference is for Namespaces
Unlike the Zend Framework 1 variant, the PrefixPathLoader
assumes that “prefixes” are PHP 5.3 namespaces by
default. You can override this behavior, however, per prefix/path you map. Please see the documentation and
examples below for details.
Quick Start¶
The PrefixPathLoader
invariably requires some configuration – it needs to know what namespaces and/or vendor
prefixes it should try, as well as the paths associated with each. You can inform the class of these at
instantiation, or later by calling either the addPrefixPath()
or addPrefixPaths()
methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | use Zend\Loader\PrefixPathLoader;
// Configure at instantiation:
$loader = new PrefixPathLoader(array(
array('prefix' => 'Foo', 'path' => '../library/Foo'),
array('prefix' => 'Bar', 'path' => '../vendor/Bar'),
));
// Or configure manually using methods:
$loader = new PrefixPathLoader();
$loader->addPrefixPath('Foo', '../library/Foo');
$loader->addPrefixPaths(array(
array('prefix' => 'Foo', 'path' => '../library/Foo'),
array('prefix' => 'Bar', 'path' => '../vendor/Bar'),
));
|
Once configured, you may then attempt to lookup a plugin.
1 2 3 4 | if (false === ($class = $loader->load('bar'))) {
throw new Exception("Plugin class matching 'bar' not found!");
}
$plugin = new $class();
|
Configuration Options¶
PrefixPathLoader Options
- $options
- The constructor accepts either an array or a
Traversable
object of prefix paths. For the format allowed, please see the addPrefixPaths() method documentation.
Available Methods¶
- __construct
Instantiate and initialize loader
__construct($options = null)
__construct() Instantiates and initializes a
PrefixPathLoader
instance. If the$prefixPaths
protected member is defined, it re-initializes it to anZend\Stdlib\ArrayStack
instance, and passes the original value to the addPrefixPaths() method. It then checks to see if$staticPaths
has been populated, and, if so, passes that on to theaddPrefixPaths()
method to merge the values. Finally, if$options
is non-null, it passes that toaddPrefixPaths()
.
- addStaticPaths
Add paths statically
addStaticPaths($paths)
addStaticPaths() Expects an array or
Traversable
object compatible with theaddPrefixPaths()
method. This method is static, and populates the protected$staticPaths
member, which is used during instantiation to either override default paths or add additional prefix/path pairs to search.
- setOptions
Configure object state
setOptions($options)
setOptions() Proxies to addPrefixPaths().
- addPrefixPath
Map a namespace/vendor prefix to the given filesystem path
addPrefixPath($prefix, $path, $namespaced = true)
addPrefixPath() Use this method to map a single filesystem path to a given namespace or vendor prefix. By default, the
$prefix
will be considered a PHP 5.3 namespace; you may specify that it is a vendor prefix by passing a booleanfalse
value to the$namespaced
argument.If the
$prefix
has been previously mapped, this method adds another$path
to a stack – meaning the new path will be searched first when attempting to resolve a plugin name to this$prefix
.
- addPrefixPaths
Add many prefix/path pairs at once
addPrefixPaths($prefixPaths)
addPrefixPaths() This method expects an array or
Traversable
object. Each item in the array or object must be one of the following:- An array, with the keys “prefix” and “path”, and optionally “namespaced”; the keys correspond to the arguments to addPrefixPath(). The “prefix” and “path” keys should point to string values, while the “namespaced” key should be a boolean.
- An object, with the attributes “prefix” and “path”, and optionally “namespaced”; the attributes correspond to the arguments to addPrefixPath(). The “prefix” and “path” attributes should point to string values, while the “namespaced” attribute should be a boolean.
The method will loop over arguments, and pass values to addPrefixPath() to process.
- getPaths
Retrieve all paths associated with a prefix, or all paths
getPaths($prefix = null)
getPaths() Use this method to obtain the prefix/paths map. If no
$prefix
is provided, the return value is anZend\Stdlib\ArrayStack
, where the keys are namespaces or vendor prefixes, and the values areZend\Stdlib\SplStack
instances containing all paths associated with the given namespace or prefix.If the
$prefix
argument is provided, two outcomes are possible. If the prefix is not found, a booleanfalse
value is returned. If the prefix is found, aZend\Stdlib\SplStack
instance containing all paths associated with that prefix is returned.
- clearPaths
Clear all maps, or all paths for a given prefix
clearPaths($prefix = null)
clearPaths() If no
$prefix
is provided, all prefix/path pairs are removed. If a$prefix
is provided and found within the map, only that prefix is removed. Finally, if a$prefix
is provided, but not found, a booleanfalse
is returned.
removePrefixPath
removePrefixPath($prefix, $path)
removePrefixPath() Removes a single path from a given prefix.
- isLoaded
Has the given plugin been loaded?
isLoaded($name)
isLoaded() Use this method to determine if the given plugin has been resolved to a class and file. Unlike
PluginClassLoader
, this method can return a booleanfalse
even if the loader is capable of loading the plugin; it simply indicates whether or not the current instance has yet resolved the plugin via theload()
method.
- getClassName
Retrieve the class name to which a plugin resolves
getClassName($name)
getClassName() Given a plugin name, this method will attempt to return the associated class name. The method completes successfully if, and only if, the plugin has been successfully loaded via
load()
. Otherwise, it will return a booleanfalse
.
- load
Attempt to resolve a plugin to a class
load($name)
load() Given a plugin name, the
load()
method will loop through the internalArrayStack
. The plugin name is first normalized usingucwords()
, and then appended to the current vendor prefix or namespace. If the resulting class name resolves via autoloading, the class name is immediately returned. Otherwise, it then loops through the associatedSplStack
of paths for the prefix, looking for a file matching the plugin name (i.e., for pluginFoo
, file nameFoo.php
) in the given path. If a match is found, the class name is returned.If no match is found, a boolean false is returned.
- getPluginMap
Get a list of plugin/class name pairs
getPluginMap()
getPluginMap() Returns an array of resolved plugin name/class name pairs. This value may be used to seed a
PluginClassLoader
instance.
- getClassMap
Get a list of class name/file name pairs
getClassMap()
getClassMap() Returns an array of resolved class name/file name pairs. This value may be used to seed a
ClassMapAutoloader
instance.
Examples¶
Using multiple paths for the same prefix
Sometimes you may have code containing the same namespace or vendor prefix in two different locations. Potentially, the same class may be defined in different locations, but with slightly different functionality. (We do not recommend this, but sometimes it happens.)
The PrefixPathLoader
easily allows for these situations; simply register the path you want to take precedence
last.
Consider the following directory structures:
1 2 3 4 5 6 7 8 9 | project
|-- library
| |-- Foo
| | |-- Bar.php
| | `-- Baz.php
|-- vendor
| |-- Foo
| | |-- Bar.php
| | `-- Foobar.php
|
For purposes of this example, we’ll assume that the common namespace is “Foo”, and that the “Bar” plugin from the vendor branch is preferred. To make this possible, simply register the “vendor” directory last.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | use Zend\Loader\PrefixPathLoader;
$loader = new PrefixPathLoader();
// Multiple calls to addPrefixPath():
$loader->addPrefixPath('Foo', PROJECT_ROOT . '/library/Foo')
->addPrefixPath('Foo', PROJECT_ROOT . '/vendor/Foo');
// Or use a single call to addPrefixPaths():
$loader->addPrefixPaths(array(
array('prefix' => 'Foo', 'path' => PROJECT_ROOT . '/library/Foo'),
array('prefix' => 'Foo', 'path' => PROJECT_ROOT . '/vendor/Foo'),
));
// And then resolve plugins:
$bar = $loader->load('bar'); // Foo\Bar from vendor/Foo/Bar.php
$baz = $loader->load('baz'); // Foo\Baz from library/Foo/Baz.php
$foobar = $loader->load('foobar'); // Foo\Foobar from vendor/Foo/Baz.php
|
Prototyping with PrefixPathLoader
PrefixPathLoader
is quite useful for prototyping applications. With minimal configuration, you can access a
full directory of plugins, without needing to update maps as new plugins are added. However, this comes with a
price: performance. Since plugins are resolved typically using by searching the filesystem, you are introducing I/O
calls every time you request a new plugin.
With this in mind, PrefixPathLoader
provides two methods for assisting in migrating to more performant
solutions. The first is getClassMap()
. This method returns an array of class name/file name pairs suitable for
use with ClassMapAutoloader. Injecting your autoloader with that map will
ensure that on subsequent calls, load()
should be able to find the appropriate class via autoloading –
assuming that the match is on the first prefix checked.
The second solution is the getPluginMap()
method, which creates a plugin name/class name map suitable for
injecting into a PluginClassLoader instance. Combine this with class
map-based autoloading, and you can actually eliminate I/O calls altogether when using an opcode cache.
Usage of these methods is quite simple.
1 2 3 | // After a number of load() operations, or at the end of the request:
$classMap = $loader->getClassMap();
$pluginMap = $loader->getPluginMap();
|
From here, you will need to do a little work. First, you need to serialize this information somehow for later use.
For that, there are two options: Zend\Serializer
or Zend\Cache
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Using Zend\Serializer:
use Zend\Serializer\Serializer;
$adapter = Serializer::factory('PhpCode');
$content = "<?php\nreturn " . $adapter->serialize($classMap) . ";";
file_put_contents(APPLICATION_PATH . '/.classmap.php', $content);
// Using Zend\Cache:
use Zend\Cache\Cache;
$cache = Cache::factory(
'Core', 'File',
array('lifetime' => null, 'automatic_serialization' => true),
array('cache_dir' => APPLICATION_PATH . '/../cache/classmaps')
);
$cache->save($pluginMap, 'pluginmap');
|
Note: the examples alternate between the class map and plugin map; however, either technique applies to either map.
Once the data is cached, you can retrieve it late to populate. In the example of the class map above, you would
simply pass the filename to the ClassMapAutoloader
instance:
1 2 | $autoloader = new Zend\Loader\ClassMapAutoloader();
$autoloader->registerAutoloadMap(APPLICATION_PATH . '/.classmap.php');
|
If using Zend\Cache
, you would retrieve the cached data, and pass it to the appropriate component; in this
case, we pass the value to a PluginClassLoader
instance.
1 2 3 | $map = $cache->load('pluginmap');
$loader = new Zend\Loader\PluginClassLoader($map);
|
With some creative and well disciplined architecture, you can likely automate these processes to ensure that
development can benefit from the dynamic nature of the PrefixPathLoader
, and production can benefit from the
performance optimizations of the ClassMapAutoloader
and PluginClassLoader
.