Resource Discovery with Puli

Two days ago, I announced Puli's first beta release. If you haven't heard about Puli before, I recommend you to read that blog post as well as the Overview page in Puli's documentation.

Today, I would like to show you how Puli's Discovery Component helps you to build and use powerful Composer packages with less work and more fun than ever before.

The Problem

Update : This post is partially obsolete. Please check the Puli website for up-to date information about Puli.

Many libraries support configuration code, translations, HTML themes or other content in files of a specific format. The Doctrine ORM, for example, is able to load entity mappings from special XML files:

<!-- res/config/doctrine/Acme.Blog.Post.dcm.xml -->
<doctrine-mapping ...>
    <entity name="Acme\Blog\Post">
        <field name="name" type="string" />

This mapping, stored in the file Acme.Blog.Post.dcm.xml in our fictional "acme/blog" package, contains all the information Doctrine needs to save our Acme\Blog\Post object in the database.

When setting up Doctrine, we need to pass the location of the *.dcm.xml file to Doctrine's XmlDriver. That's easy as long as we do it ourselves, but:

  • What if someone else uses our package? How will they find our file?
  • What if multiple packages provide *.dcm.xml files? How do we find all these files?
  • We need to remove the appropriate setup code after removing a package.
  • We need to adapt the setup code after installing a new package.

Multiply this effort for every other library that uses user-provided files and you end up with a lot of configuration effort. Let's see how Puli helps us to fix this.

Package Roles

For better understanding, it's useful to assign two different roles to our packages:

  • Resource consumers, like Doctrine, process files of a certain format.
  • Resource providers, like our "acme/blog" package, ship such files.

Puli connects consumers and providers through a mechanism called resource binding. Resource binding is a very simple mechanism:

  1. At first, the consumer defines a binding type.
  2. Then, one or multiple providers bind resources to these types.
  3. Finally, the consumer fetches all the resources bound to their type and does something with them.

Let's put on the hat of a Doctrine developer and see how this works in practice.

Discovering Resources

We start by defining the binding type "doctrine/xml-mapping" with Puli's Command Line Interface (CLI):

$ puli type define doctrine/xml-mapping \
    --description "An XML entity mapping loaded by Doctrine's PuliDriver"

We passed a nicely readable description that is displayed when typing puli type:

$ puli type
Enabled binding types:

    doctrine/xml-mapping An XML entity mapping loaded by Doctrine's PuliDriver

Use "puli bind <resource> <type>" to bind a resource to a type.

Great! Now we'll use Puli's ResourceDiscovery to find all the Puli resources bound to our type:

foreach ($discovery->find('doctrine/xml-mapping') as $binding) {
    foreach ($binding->getResources() as $resource) {
        // load $resource

Remember we're still wearing the Doctrine developer hat? Let's put this code into a PuliDriver class so that anybody can easily configure Doctrine to load Puli resources.

Binding Resources

Now, we'll put on the "acme/blog" developer hat. Let's bind the XML file from before to Doctrine's binding type:

$ puli bind /acme/blog/config/doctrine/*.xml doctrine/xml-mapping

The bind command accepts two parameters:

  • The path or glob for the Puli resources we want to bind.
  • The name of the binding type.

We can use puli find to check which resources match the binding:

$ puli find -b doctrine/xml-mapping
FileResource /acme/blog/config/doctrine/Acme.Blog.Post.dcm.xml

Apparently our XML file was registered successfully.

Application Setup

We'll change hats one last time. This time, we'll wear your hat. What do we have to do to use both the "doctrine/orm" package and the "acme/blog" package in our application?

The first thing obviously is to install the packages and the Puli CLI with Composer:

$ composer require doctrine/orm acme/blog puli/cli

Once this is done, we have to configure Doctrine to use the PuliDriver:

use Doctrine\ORM\Configuration;

// Puli setup
$factoryClass = PULI_FACTORY_CLASS;
$factory = new $factoryClass();
$repo = $factory->createRepository();
$discovery = $factory->createDiscovery($repo);

// Doctrine setup
$config = new Configuration();
$config->setMetadataDriverImpl(new PuliDriver($discovery));

// ...

With as little effort as this, Doctrine will now use all the resources bound to the "doctrine/xml-mapping" type in any installed Composer package.

Will it though?

Enabled and Disabled Bindings

Automatically loading stuff from all Composer packages is a bit scary, hence Puli does not enable bindings in your installed packages by default. We can see these bindings when typing puli bind:

$ puli bind
Bindings that are neither enabled nor disabled:
  (use "puli bind --enable <uuid>" to enable)

      653fc9 /acme/blog/config/doctrine/*.xml doctrine/xml-mapping

If we trust the "acme/blog" developer and actually want to use the binding, we can do so by typing:

$ puli bind --enable 653fc9

That's all, folks. :) Read more about resource discovery with Puli in the Resource Discovery guide in the documentation. And please leave me your comments below.