PHP >= 5.3.9
Puli returns the absolute file paths of the files (resources) in your PHP project through a unified naming system. Essentially, Puli lets you use a resource locator to access files like this:
echo $locator->get('/webmozart/puli/css/style.css')->getPath();
// => /path/to/resources/assets/css/style.css
This document teaches you how to use Puli in practice.
You can install Puli with Composer:
{
"require": {
"webmozart/puli": "~1.0@dev"
}
}
Run composer install
or composer update
and you're ready to start.
Tool | Description | Version |
---|---|---|
Composer | The Puli plugin for Composer builds resource locators from composer.json definitions. | 1.0.0-alpha1 |
Twig | The Puli extension for Twig lets you access templates via Puli paths. | 1.0.0-dev |
Puli manages files in a repository, where you map them to a path:
use Webmozart\Puli\Repository\ResourceRepository;
$repo = new ResourceRepository();
$repo->add('/webmozart/puli', '/path/to/resources/assets/*');
$repo->add('/webmozart/puli/trans', '/path/to/resources/trans');
The method add()
works very much like copying on your local file system. The
only difference is that no file is ever really moved on your disk.
You can locate the added files using the get()
method:
echo $repo->get('/webmozart/puli/css/style.css')->getPath();
// => /path/to/resources/assets/css/style.css
echo $repo->get('/webmozart/puli/trans/en.xlf')->getPath();
// => /path/to/resources/trans/en.xlf
The get()
method accepts either the path to the resource, a glob pattern or an
array containing multiple paths or patterns. If you pass a pattern or an array,
the method will always return an array as well.
foreach ($repo->get('/webmozart/puli/*') as $resource) {
echo $resource->getRepositoryPath();
}
// => /webmozart/puli/css
// => /webmozart/puli/trans
You can remove resources from the repository with the remove()
method:
$repo->remove('/webmozart/puli/css');
Building and configuring a repository is expensive and should not be done on
every request. For this reason, Puli allows to dump resource locators that are
optimized for retrieving resources. Resource locators must implement the
interface ResourceLocatorInterface
, which provides a subset of the
methods available in the resource repository. Naturally, resource locators are
frozen and cannot be modified.
Currently, Puli only provides one locator implementation: PhpResourceLocatorDumper
.
This locator caches the paths to the resources in your repository in PHP files.
These files are usually stored in the cache directory of your application. Pass
the path to this cache directory when you call the dumpLocator()
method:
use Webmozart\Puli\LocatorDumper\PhpResourceLocatorDumper;
$dumper = new PhpResourceLocatorDumper();
$dumper->dumpLocator($repo, '/path/to/cache');
Then create a PhpResourceLocator
and pass the path to the directory where
you dumped the PHP files. The locator lets you then access the resources in the
same way as the repository does:
use Webmozart\Puli\Locator\PhpResourceLocator;
$locator = new PhpResourceLocator('/path/to/cache');
echo $locator->get('/webmozart/puli/css/style.css')->getPath();
// => /path/to/resources/assets/css/style.css
echo $locator->get('/webmozart/puli/trans/en.xlf')->getPath();
// => /path/to/resources/trans/en.xlf
Puli allows to use multiple resource locators at the same time through the
UriLocator
. You can register multiple ResourceLocatorInterface
instances for different URI schemes. Then you can use the UriLocator
like a regular resource locator, except you need to pass URIs instead of
simple paths. An example tells a thousand stories:
use Webmozart\Puli\Locator\PhpResourceLocator;
use Webmozart\Puli\Locator\UriLocator;
$locator = new UriLocator();
$locator->register('resource', new PhpResourceLocator('/cache/resource'));
$locator->register('namespace', new PhpResourceLocator('/cache/namespace'));
echo $locator->get('resource:///webmozart/puli/css/style.css')->getPath();
// => /path/to/resources/assets/css/style.css
echo $locator->get('namespace:///Webmozart/Puli/Puli.php')->getPath();
// => /path/to/webmozart/puli/src/Puli.php
In this example, the URI locator routes all requests for URIs with the protocol "resource://" to one resource locator and requests for URIs with the protocol "namespace://" to the other locator.
To improve performance in requests where you don't access either of the protocols, you can also register callbacks that create the resource locators:
$locator->register('resource', function () {
return new PhpResourceLocator('/cache/resource')
});
Puli supports a stream wrapper that lets you access the contents of the
repository transparently through PHP's file functions. To register the wrapper,
call the register()
method in ResourceStreamWrapper
and pass a
configured UriLocator
:
use Webmozart\Puli\Locator\UriLocator;
use Webmozart\Puli\StreamWrapper\ResourceStreamWrapper;
$locator = new UriLocator();
$locator->register('resource', $repository);
ResourceStreamWrapper::register($locator);
You can now use regular PHP functions to access the files and directories in the repository.
$contents = file_get_contents('resource:///webmozart/puli/css/style.css');
$entries = scandir('resource:///webmozart/puli');
Even better: If you register the stream wrapper, you can use Puli resources with all frameworks and libraries that use PHP's file functions under the hood.
The get()
method returns one or more ResourceInterface
instances. This
interface lets you access the name, the repository path and the real file path
of the resource:
$resource = $repo->get('/webmozart/puli/css');
echo $resource->getName();
// => css
echo $resource->getRepositoryPath();
// => /webmozart/puli/css
echo $resource->getPath();
// => /path/to/resources/assets/css
The method getName()
will always return the name of the resource in the
repository. If you want to retrieve the name of the resource on the filesystem,
use basename()
instead:
$repo->add('/webmozart/puli', '/path/to/resources/assets');
$resource = $repo->get('/webmozart/puli');
echo $resource->getName();
// => puli
echo basename($resource->getPath());
// => assets
Directory resources implement the additional interface
DirectoryResourceInterface
. This way you can easily distinguish directories
from files:
use Webmozart\Puli\Resource\DirectoryResourceInterface;
$resource = $repo->get('/webmozart/puli/css');
if ($resource instanceof DirectoryResourceInterface) {
// ...
}
Directories are traversable and countable:
$directory = $repo->get('/webmozart/puli/css');
echo count($directory);
// => 2
foreach ($directory as $resource) {
// ...
}
You can access the contents of a directory with the methods get()
,
contains()
and all()
or use its ArrayAccess
interface:
$resource = $directory->get('style.css');
$resource = $directory['style.css'];
if ($directory->contains('style.css')) {
// ...
}
if (isset($directory['style.css'])) {
// ...
}
$resources = $directory->all();
Direct modifications of DirectoryResourceInterface
instances are not
allowed. You should use the methods provided by ResourceRepositoryInterface
instead.
Puli lets you override files and directories without losing the original paths.
This is very useful if you want to remember a cascade of files in order to merge
them later on. The method getAlternativePaths()
returns all paths that were
registered for a resource, in the order of their registration:
$repo->add('/webmozart/puli/config', '/path/to/vendor/webmozart/puli/config');
$repo->add('/webmozart/puli/config', '/path/to/app/config');
$resource = $repo->get('/webmozart/puli/config/config.yml');
echo $resource->getPath();
// => /path/to/app/config/config.yml
print_r($resource->getAlternativePaths());
// Array
// (
// [0] => /path/to/vendor/webmozart/puli/config/config.yml
// [1] => /path/to/app/config/config.yml
// )
Resources managed by Puli can be tagged. This is useful for marking resources
that support specific features. For example, you can tag all XLIFF translation
files that can be consumed by a class Acme\Translator
:
$repo->tag('/webmozart/puli/translations/*.xlf', 'acme/translator/xlf');
You can remove one or all tags from a resource using the untag()
method:
// Remove the tag "acme/translator/xlf"
$repo->untag('/webmozart/puli/translations/*.xlf', 'acme/translator/xlf');
// Remove all tags
$repo->untag('/webmozart/puli/translations/*.xlf');
You can get all files that bear a specific tag with the getByTag()
method:
$resources = $repo->getByTag('acme/translator/xlf');
You can also read all tags that have been registered in the repository:
$tags = $repo->getTags();
This method will return an array of strings, namely the tags that have been registered.
Tagging can be used to implement classes that autonomously discover the
resources they need. For example, the Acme\Translator
class mentioned before
can provide a discoverResources()
method which extracts all resources marked
with the "acme/translator/xlf" tag from the repository:
namespace Acme;
use Webmozart\Puli\Locator\ResourceLocatorInterface;
class Translator
{
// ...
public function discoverResources(ResourceLocatorInterface $locator)
{
foreach ($locator->getByTag('acme/translator/xlf') as $resource) {
// register $resource->getPath()...
}
}
}
Adding the tagged files to the translator becomes as easy as passing the repository or the resource locator:
use Acme\Translator;
$translator = new Translator();
$translator->discoverResources($repo);
Puli provides an interface ResourceDiscoveringInterface
for marking such
classes. Dependency Injection Containers can rely on this interface to inject
the resource locator automatically.
namespace Acme;
use Webmozart\Puli\ResourceDiscoveringInterface;
class Translator implements ResourceDiscoveringInterface
{
// ...
}
By default, you can use Glob patterns to locate files both in the repository and on your filesystem:
// Glob the repository
foreach ($repo->get('/webmozart/puli/css/*.css') as $resource) {
// ...
}
// Glob the filesystem
$repo->add('/webmozart/puli/css', '/path/to/resources/css/*.css');
If you want to use other patterns than Glob, you can create custom
implementations of PatternInterface
. As an example, look at the code of
GlobPattern
:
class GlobPattern implements PatternInterface
{
private $pattern;
private $staticPrefix;
private $regExp;
public function __construct($pattern)
{
$this->pattern = $pattern;
$this->regExp = '~^'.str_replace('\*', '[^/]+', preg_quote($pattern, '~')).'$~';
if (false !== ($pos = strpos($pattern, '*'))) {
$this->staticPrefix = substr($pattern, 0, $pos);
} else {
$this->staticPrefix = $pattern;
}
}
public function getStaticPrefix()
{
return $this->staticPrefix;
}
public function getRegularExpression()
{
return $this->regExp;
}
public function __toString()
{
return $this->pattern;
}
}
The method getRegularExpression()
returns the pattern converted to a regular
expression. The method getStaticPrefix()
returns the prefix of the path that
never changes. This is used to reduce the number of internal preg_match()
calls.
In order to use custom patterns, pass the PatternInterface
instance wherever
a Glob pattern is accepted. For example, if you implement a class
RegExpPattern
:
foreach ($repo->get(new RegExpPattern('~^/webmozart/puli/css/.+\.css$~')) as $resource) {
// ...
}
So far, the custom pattern only works for locating files in the repository. If
you also want to use it to locate files on the filesystem, create an
implementation of PatternLocatorInterface
. As example, look at the code
of GlobPatternLocator
:
class GlobPatternLocator implements PatternLocatorInterface
{
// ...
public function locatePaths(PatternInterface $pattern)
{
return glob((string) $pattern);
}
}
The locatePaths()
method receives the pattern instance and returns the paths
in the file system which match the pattern.
To use the locator, create an implementation of PatternFactoryInterface
and
return the locator from the createPatternLocator()
method. A convenient
solution is to let the locator itself implement this interface. If we assume
again that you implemented a RegExpPatternLocator
, you can extend the
implementation like this:
class RegExpPatternLocator implements PatternLocatorInterface, PatternFactoryInterface
{
// ...
public function createPatternLocator()
{
return $this;
}
}
Pass the factory to the constructor of the repository or the resource locator:
$repo = new ResourceRepository(new RegExpPatternLocator());
// Locate files using regular expressions
$repo->add('/webmozart/puli/css', new RegExpPattern('~^/path/to/css/.+\.css$~'));
If you try to implement the above code snippets, you will notice that the
PatternFactoryInterface
requires to implement two more methods, namely
acceptsSelector()
and createPattern()
. These methods help to automatically
create PatternInterface
instances from the string selectors passed to
add()
, get()
and similar methods. You could implement the methods like this:
class RegExpPatternLocator implements PatternLocatorInterface, PatternFactoryInterface
{
// ...
public function acceptsSelector($selector)
{
return '~' === $selector[0];
}
public function createPattern($selector)
{
new RegExpPattern($selector);
}
}
With these additions, it's possible to pass custom patterns as strings.
Internally, the methods of the pattern factory will be called to construct a new
RegExpPattern
instance automatically:
$repo->add('/webmozart/puli/css', '~^/path/to/css/.+\.css$~');