Symfony bundles: providing services and exposing resources
When you look at the source code of the Symfony framework, it becomes clear that bundles play two distinct and very different roles: in the first place a bundle is a service container extension: it offers ways to add, modify or remove service definitions and parameters, optionally by means of bundle configuration. This role is represented by the following methods of BundleInterface
:
namespace Symfony\Component\HttpKernel\Bundle;
interface BundleInterface extends ContainerAwareInterface
{
/** Boots the Bundle. */
public function boot();
/** Shutdowns the Bundle. */
public function shutdown();
/** Builds the bundle. */
public function build(ContainerBuilder $container);
/** Returns the container extension that should be implicitly loaded. */
public function getContainerExtension();
...
}
The second role of a bundle is that of a resource provider. When a bundle is registered in the application kernel, it automatically starts to expose all kinds of resources to the application. Think of routing files, controllers, entities, templates, translation files, etc.
The "resource-providing" role of bundles is represented by the following methods of BundleInterface
:
interface BundleInterface extends ContainerAwareInterface
{
...
/** Returns the bundle name that this bundle overrides. */
public function getParent();
/** Returns the bundle name (the class short name). */
public function getName();
/** Gets the Bundle namespace. */
public function getNamespace();
/** Gets the Bundle directory path. */
public function getPath();
}
As far as I know, only getName()
serves both purposes, since it is also used to calculate the configuration key that is used for the bundle's configuration (e.g. the configuration for the DoctrineBundle
is to be found under the doctrine
key in config.yml
).
There are several framework classes that use the bundle name and its root directory (which is returned by the bundle's getPath()
method) to locate resources in a bundle. For instance the ControllerResolver
from the FrameworkBundle
allows you to use the Bundle:Controller:action
notation to point to methods of controller classes in the Controller
directory of your bundle. And the TemplateNameParser
from the FrameworkBundle
resolves shorthand notation of templates (e.g. Bundle:Controller:action.html.twig
) to their actual locations.
It's actually quite a clever idea to use the location of the bundle class as the root directory for the resources which a bundle exposes. By doing so, it doesn't matter anymore whether a bundle is part of a package that is installed in the vendor
directory using Composer, or if it's part of your project's source code in the src
directory; the actual location of resources is always derived based on the location of the bundle class itself.
Towards a better situation for resources
Let me first say, I think the BundleInterface
ought to be separated in two different "role interfaces", based on the different roles they play. I was thinking of ProvidesServices
and a ExposesResources
interface. That would clearly communicate the two different things that a bundle can do for you.
Puli: uniform resource location
Much more important than splitting the BundleInterface
is to have a better way of exposing resources located inside a library (or bundle, it wouldn't make a difference actually). This is something Bernhard Schüssek has been working on. He has created a nice library called Puli. It basically provides a way to locate and discover resources from all parts of the application, be it your own project or a package that you pulled in using Composer.
The core class of Puli is the ResourceRepository
. It works like a registry of resource locations. For every resource or collection of resources that you want to expose, you call the add()
method and provide as a first argument a prefix, and as a second argument an absolute path to a directory:
use Webmozart\Puli\Repository\ResourceRepository;
$repo = new ResourceRepository();
$repo->add('/matthias/package-name/templates', '/path/to/Resources/views');
Now if you ask the repository to get the absolute path of a particular resource, you can do it by using the prefix you just added to the repository:
$repo->get('/matthias/package-name/templates/index.html.twig')->getRealPath();
This will return the absolute path of index.html.twig
, i.e. /path/to/Resources/views/index.html.twig
.
Packages exposing their own resources
Though this should seem quite basic to you (it's almost as simple as a string replacement), things will soon get much more interesting when you start using the Puli plugin for Composer. Once you have installed it, you can register any type of resource in the composer.json
file of the project it contains. This can be the root project you're working on, or any Composer package that is installed in that project (i.e. any package that you will find in your vendor/
directory).
To expose a specific set of resources, like some Twig template files, you can just list the location of the resources in te composer.json
of the package containing them. You can use a location that is relative to the root directory of the package, i.e. where the composer.json
file is:
{
...
"extra": {
"resources": {
"/matthias/package-name/templates": "Resources/views"
}
}
}
Puli automatically adds the absolute location of the package (e.g. /home/matthias/projects/my-application/vendor/matthias/package-name
) in front of the relative location that you provide under the resources
key (e.g. Resources/views
). It thereby makes the package file Resources/views/index.html.twig
available anywhere in the application as /matthias/package-name/template.html.twig
. Puli will take care of transforming the relative path to the absolute path when necessary.
Integration with other libraries
Of course, most PHP libraries don't know how to work with Puli's ResourceRepository
, but Bernhard has already created several Puli extensions, which form a bridge between some popular PHP projects and resources exposed through Puli. For instance there is a Twig extension, allowing you to render Puli-exposed templates using Twig. There's also a Symfony Config extension which allows you to load, for instance, routing configuration files using Puli paths.
Bernhard has also been working on a Puli Symfony bundle, which will make it really easy to have Puli-supported resource exposure in your Symfony project. A small disclaimer though: it didn't work out-of-the-box for me but in defense of the author, there is currently no stable release (yet).
A solution that will always work: stream wrappers
Even though the Symfony eco-system seems to be the first to benefit from Puli and resource-exposing packages, nothing prevents you from using Puli in other types of applications that use another framework or no framework at all. Puli itself is framework-independent and comes with tools to create PHP stream wrappers, which means that any PHP application that uses PHP's built-in file functions like file_get_contents()
or fopen()
will be able to work with resources exposed by Puli. This is what it looks like:
use Webmozart\Puli\Locator\UriLocator;
use Webmozart\Puli\StreamWrapper\ResourceStreamWrapper;
$locator = new UriLocator();
$locator->register('resource', $repository);
ResourceStreamWrapper::register($locator);
file_get_contents('resource:///matthias/package-name/templates/index.html.twig');
Conclusion
I think with Puli it will become very easy to share resources, like templates, translation files, etc. between applications. They don't need to be in a bundle (or any framework-specific package for that matter) in order to be discovered and used in an application. Resources can be in any package and will even be automatically exposed through their composer.json
file.
If you want to know more about Bernhard's ideas behind Puli, you can read about it on his website, even though at the time of writing the site is down.
Thanks for this post Matthias! :) I've been working on Puli again this week to get the first beta out. I hope there'll be news by next week.
I think Puli has a lot of potential. I would like to see it used in Symfony core.
Nice post Matthias. Scott here.:) This sounds like exactly the point where we'd need to look to actually confine resources between "customers" with different packages for our platform. Am I correct in my thinking?
Thanks Scott. Well in most scenarios all the resources from all the packages would register in a static way (using a Composer-generated file). So if you want different resources for different users you would have to add some dynamic aspect to it, like manually using the ResourceRepository to add some resources to it based on packages that should be loaded for a particular user. You'd have to separate the process in other locations too, because in the end, Twig files, translation files, etc. are all compiled to static/generated files in the application's cache, which means that they might "leak" to other users too.
Thanks for answering. :)
Yeah, this is going to be another tricky part about the system. Definitely something dynamic will have to happen to keep customer "apps" (packages/ bundles) and/ or their customizations/ extensions separated. I am still not completely sure at what level this will need to happen. If we let apps be "shared", as is usually the case in a SaaS system, the customer might lose some (too much?) flexibility. If we let apps be duplicated, although they are pretty much the same for each customer, then we could be building up unnecessary overhead, which equates to unnecessary costs (in money and/ or performance). This decision will be an important one.
In general though, I have been detailing an overall business scheme for apps and extensions, which we'll need for "Skooppa Code" (the place to get apps and extensions for Skooppa Sites). Getting this scheme to work with our PaaS system and any SaaS apps on top of that will be the next big challenge, once we make PHP "safe" for our PaaS development environment.;)
Scott
typo -> Syfony
Thanks, just changed it!