, (*1)
A WordPress-specific dependency injection container that doesn't suck at factoring objects., (*2)
It is mainly inspired to Pimple, but allows easier object instances creation., (*3)
It is not a full-working plugin, it is a library to be embedded in larger projects via Composer., (*4)
As Pimple (and other DI containers), Faber manages two different kind of data: services and properties., (*5)
Properties are variables stored "as they are", that is possible to retrieve when needed., (*6)
Services are objects that are used in different part of the application, and when they are required again and again, same instance is returned.
Services are registered via factory closures: they are anonymous functions that return instances of objects., (*7)
Faber also implements factory pattern: is possible to use registered factory closures to obtain always fresh, vanilla instances of objects., (*8)
, (*9)
How it Works
Requirements, Installation and License
Create Container Instance
Before being able to do anything, an instance of container is needed. There are two ways: the static way and the dynamic one., (*10)
Static Instantiation: The instance() Method
First way to create an instance of the container is to use the static instance() method:, (*11)
``` php
$container = GM\Faber::instance( 'my_plugin' );, (*12)
The argument passed to instance method is the ID of the instance: is possible to create any number of instances passing different IDs, they will be completely isolated one from another:
``` php
$plugin_container = GM\Faber::instance( 'my_plugin' );
$theme_container = GM\Faber::instance( 'my_theme' );
Benefit of this approach is simple: once having a container is useless if it isn't accessible from any part of application, using this method is possible to access container from anywhere in the app by calling the instance() method again and again., (*13)
Note that the instance() method has an alias: i():, (*14)
``` php
$container = GM\Faber::i( 'my_app' ); // is identical to GM\Faber::instance( 'my_app' );, (*15)
##Dynamic Instantiation
Instances of container can be also created using *canonical* way:
``` php
$container = new GM\Faber();
When instantiated like so, a method to retrieve container instance should be arranged, maybe using a global variable:, (*16)
``` php
global $myapp;
$myapp = new GM\Faber();, (*17)
##Init Hook
No matter the method used to instantiate the container, when an instance is created, an action hook is fired: **`"faber_{$id}_init"`**. The `$id` part is the instance ID.
When container is instantiated using dynamic method, to pass an ID is optional, but when no ID is passed to constructor an unique ID is automatically created, it isn't predictable, but can be retrieved using **`getId()`** method.
``` php
$myapp = new GM\Faber();
$id = $myapp->getId(); // will be something like 'faber_5400d85c5a18c'
However, when one plans to use the init hook and dynamic instantiation, is probably preferable to pass an ID to constructor like so:, (*18)
``` php
$myapp = new GM\Faber( [], 'my_app' );
$id = $myapp->getId(); // will be 'my_app', (*19)
The ID is the second argument, because first is an array of properties / services to be registered during instance creation.
Init hook can be used to add properties and services to container, because it passes the just-created container instance as argument to hooking callbacks:
``` php
add_action( 'faber_' . $container->getId() . '_init', function( $container ) {
// do something with container
});
Registering and Getting Services
Having an instance of container, is possible to register services to be used everywhere in app code., (*20)
A service is an object that does something as part of a larger system., (*21)
Services are defined by closures (anonymous functions) that return an instance of an object:, (*22)
``` php
// define some services, (*23)
$container['bar'] = function ( $cont, $args ) {
return new My\App\Bar( $cont['foo'] );
};, (*24)
$container['foo'] = function ( $cont, $args ) {
return new My\App\Foo();
};, (*25)
Things to note in previous code:
- array access interface is used to add services (container object is treated as an array)
- factory closures have access to the container instance that can be used to inject dependencies in the object to be created (in the example above an instance of class `Foo` is injected into `Bar` class)
- factory closures have access to the `$args` variable: it is an array of arguments used when the instance is retrieved (better explained below)
- order used to define services doesn't matter: factories are executed if and when a service is required, not when it is defined.
To get registered services is possible to use array access:
``` php
$bar = $container['bar']; // $bar is an instance of My\App\Bar
or, as alternative, the container get() method:, (*26)
``` php
$bar = $container->get( 'bar' ); // $bar is an instance of My\App\Bar, (*27)
Services are cached, it means that when same service is required after first time, same instance is returned:
``` php
$bar_1 = $container['bar'];
$bar_2 = $container->get( 'bar' );
var_dump( $bar_1 === $bar_2 ); // TRUE
$foo = $container['foo'];
// here getFoo() returns the instance of Foo injected in Bar constructor
$bar_foo = $bar_1->getFoo();
var_dump( $foo === $bar_foo ); // TRUE
Objects with Arguments
Sometimes classes require some arguments to be instantiated, and these arguments vary from instance to instance., (*28)
An example:, (*29)
``` php
class Post {, (*30)
private $wp_post;
private $options;, (*31)
function __construct( $id, Options $options ) {
$this->wp_post = get_post( $id );
$this->options = $options;
}
}, (*32)
Actually, this is not a *service*, however it's an object that needs a variable argument (`$id`) and a service (`$options`).
Whereas the service fits perfectly workflow explained above (an instance of `Options` can be saved in the container and passed to `Post`) is not possible to do the same thing with `$id`.
This is the limit of a lot of DI containers, but luckily not of Faber.
As example having following definitions
``` php
$container['post'] = function ( $cont, $args ) {
return new Post( $args['id'], $cont['options'] );
};
$container['options'] = function ( $cont, $args ) {
return new Options;
};
is possible to build some post objects like so:, (*33)
``` php
$post_1 = $container->get( 'post', [ 'id' => 1 ] );
$post_2 = $container->get( 'post', [ 'id' => 2 ] );
$post_1_again = $container->get( 'post', [ 'id' => 1 ] );, (*34)
var_dump( $post_1 === $post_2 ); // FALSE: different args => different object
var_dump( $post_1 === $post_1_again ); // TRUE: same args => same object, (*35)
In short, `get()` method can be used to pass an array of arguments to factory closure where it is used to generate objects.
When same ID and same arguments are passed to `get()` method, the same instance is obtained; changing arguments different instances are returned.
Note: in code above the instance of `Options` passed to `Post` objects is always the same.
##How To Force Fresh Instances
Normally, services are cached: if same arguments are passed to `get()` method then the same instance is returned.
What if a *fresh* instance of `Post` having ID 1 is required even if it has been required before?
The **`make()`** is there for the scope:
``` php
$post_1 = $container->get( 'post', [ 'id' => 1 ] );
$post_1_again = $container->get( 'post', [ 'id' => 1 ] );
$post_1_fresh = $container->make( 'post', [ 'id' => 1 ] );
var_dump( $post_1 === $post_1_again ); // TRUE
var_dump( $post_1 === $post_1_fresh ); // FALSE: make() force fresh instances
Get data using "Demeter chain" IDs
Assuming some classes are defined like so, (*36)
``` php
class Foo {, (*37)
function getBar() {
return new Bar;
}
}, (*38)
class Bar {, (*39)
function getBaz() {
return new Baz;
}
}, (*40)
class Baz {, (*41)
function getResult() {
return 'Result!';
}
}, (*42)
and first class is registered in the container like so:
``` php
$container['foo'] = function() {
return new Foo;
}
To get call getResult() method of Baz class, is possible to do something like:, (*43)
``` php
$result = $container['foo']->getBar()->getBaz()->getResult(); // 'Result!', (*44)
Fine. But (since version 1.1) in Faber is also possible to do:
``` php
$result = $container['foo->getBar->getBaz->getResult'] // 'Result!'
So is possible to access methods of objects in container by using object assigment operator (->) to "glue" object IDs and methods to be called in chain., (*45)
Registering and Getting Properties
Properties are usually non-object variables that need to be globally accessible in the app. They are registered and retrieved in same way of services:, (*46)
``` php
// define properties, (*47)
$container->add( 'version', '1.0.0' );
$container['timeout'] = HOUR_IN_SECONDS;
$container['app_path'] = plugin_dir_path( FILE );
$container['app_assets_url'] = plugins_url( 'assets/' , FILE );, (*48)
// get properties, (*49)
$version = $container['version'];
$timeout = $container->get( 'timeout' );, (*50)
For properties just like for services, is possible to use `add()` or array access to register properties and `get()` or array access to retrieve them.
Additional **`prop()`** method can be used to get properties, it returns an error if used with a service ID:
``` php
$app_path = $container->prop( 'app_path' );
Saving Closures as Properties
When a closure is added to container, by default it's considered a factory closure for a service.
To store a closure "as is", treating it as a property, is possible use the protect() method:, (*51)
``` php
$container->protect( 'greeting', function() {
return date('a') === 'am' ? 'Good Morning' : 'Good Evening';
});, (*52)
#Hooks
There are only three hooks fired inside Faber class.
The first is `"faber_{$id}_init"` explained [here](#init-hook).
The other two are filters, and they are:
- **`"faber_{$id}_get_{$which}"`**
- **`"faber_{$id}_get_prop_{$which}"`**
These filters are fired every time, respectively, a service or a property are retrieved from the container.
The variable `$id` part is the container instance ID.
The variable `$which` part is the ID of service / property being retrieved.
First argument they pass to hooking callbacks is the value just retrieved from container.
Second argument is the instance of container itself.
``` php
$container = GM\Faber::i( 'test' );
$container['foo'] => 'foo';
$container['bar'] => function() {
new Bar;
};
$foo = $container['foo']; // result passes through 'faber_test_get_prop_foo' filter
$bar = $container['bar']; // result passes through 'faber_test_get_bar' filter
Bulk Registering
Instead of registering services one by one using array access or add() method, is possible to register more services (and properties) at once. That can be done in three ways:, (*53)
- passing an array of definitions to constructor (only when using dynamic instantiation method)
- passing an array of definitions to
load() method
- passing the path of a PHP file that returns an array of definitions to
loadFile() method
Definitions to Constructor
``` php
$def = [
'timeout' => HOUR_IN_SECONDS,
'version' => '1.0.0',
'foo' => function( $container, $args ) {
return new Foo;
},
'bar' => function( $container, $args ) {
return new Bar( $container['foo'] );
}
];, (*54)
$container = new GM\Faber( $def );, (*55)
##Definitions to `load()` method
``` php
$container = new GM\Faber;
$container->load( $def ); // $def is same of above
Definitions to loadFile() method
First a definitions file is needed, something like:, (*56)
``` php
<?php
// file must return an array, (*57)
return [
'timeout' => HOUR_IN_SECONDS,
'version' => '1.0.0',
'foo' => function( $container, $args ) {
return new Foo;
},
'bar' => function( $container, $args ) {
return new Bar( $container['foo'] );
}
];, (*58)
and then
``` php
$container = new GM\Faber;
$container->loadFile( 'full/path/to/definitions/file.php' );
Load On Init
An intersting usage of load() and loadFile() methods is combined with the Faber init hook:, (*59)
``` php
add_action( 'faber_' . $container->getId() . '_init', function( $container ) {
$container->loadFile( 'full/path/to/definitions.php' );
});, (*60)
It's a simple way to register services keeping code clean and readable.
#Updating, Removing, Freezing and Unfreezing
##Updating
Once a service or a property is registered, it can be updated using array access or **`update()`** method:
``` php
$container[ 'foo' ] = 'Foo';
$container[ 'bar' ] = 'Bar';
echo $container[ 'foo' ]; // Output "Foo"
echo $container[ 'bar' ]; // Output "Bar"
$container[ 'foo' ] = 'New Foo';
$container->update( 'bar', 'New Bar' );
echo $container[ 'foo' ]; // Output "New Foo"
echo $container[ 'bar' ]; // Output "New Bar"
Note that a service can be updated only with another service and a protected closure with another closure:, (*61)
``` php
$container[ 'foo_service' ] = function() {
return Foo;
};, (*62)
$container->protect( 'closure', function() {
return 'Hello World!';
});, (*63)
$container[ 'foo_service' ] = 'a string'; // will fail, (*64)
$container->update( 'closure', [ 'a', 'b' ] ); // will fail, (*65)
##Removing
Once a service or a property is registered, it can be removed using `unset()` and array access or via **`remove()`** method:
``` php
$container[ 'foo' ] = 'Foo';
$container[ 'bar' ] = 'Bar';
unset( $container[ 'foo' ] );
$container->remove( 'foo' );
Freezing and Unfreezing
Updating and removing definitions can be avoided by freezing them via, guess it, freeze() method., (*66)
When frozen, a property or a service can't be updated or removed until unfrozen via unfreeze() method:, (*67)
``` php
$container[ 'foo' ] = 'Foo!';, (*68)
$container->freeze( 'foo' );, (*69)
unset( $container[ 'foo' ] ); // will fail
echo $container[ 'foo' ]; // still output "Foo!", (*70)
$container->update( 'foo', 'New Foo!' ); // will fail
echo $container[ 'foo' ]; // still output "Foo!", (*71)
$container->unfreeze( 'foo' );, (*72)
$container[ 'foo' ] = 'New Foo!'; // will success
echo $container[ 'foo' ]; // output "New Foo!", (*73)
$container->remove( 'foo' ); // will success
echo $container[ 'foo' ]; // will output an error message, (*74)
#Fluent Interface
Methods that set *things* on container return an instance of the container itself, allowing *fluent interface*, i.e. *chained* methods.
Among methods that support this interface there are:
- `load()`
- `loadFile()`
- `add()`
- `protect()`
- `freeze()`
- `unfreeze()`
- `update()`
- `remove()`
E. g. the following is valid code:
``` php
$foo = GM\Faber::instance('myapp')
->load( $defs )
->loadFile('defs.php')
->add( 'foo', 'Foo!' )
->add( 'bar', 'Bar!' )
->protect( 'hello', function() { return 'Hello!'; } )
->freeze( 'foo' )
->freeze( 'hello' )
->unfreeze( 'foo' )
->update( 'foo', 'New Foo!' )
->remove( 'bar' )
->get('foo'); // return 'New Foo!'
Issers and Info Getters
Conditional Methods AKA Issers
For a better control of application flow and to avoid errors, is reasonable that one wants to check if a specific ID is registered in the container and if it is associated to a property or a factory, if it is frozen, and so on., (*75)
To fulfill these needs there are a set of conditional methods ("issers"). They are:, (*76)
-
$c->isProp( $id ) returns true if given ID is associated to a property (protected closures are properties too)
-
$c->isFactory( $id ) returns true if given ID is associated to a factory closure
-
$c->isProtected( $id ) returns true if given ID is associated to a protected closure
-
$c->isFrozen( $id ) returns true if given ID is associated to a frozen item (no matter if property or factory)
-
$c->isCachedObject( $key ) returns true if given key is associated to a cached object (more info)
(where $c is an instance of the container, of course), (*77)
Info Getters
Sometimes is also desirable to have informations on the current state of a container instance., (*78)
There are some getters that provide useful informations:, (*79)
-
$c->getID() returns ID of container instance
-
$c->getHash() returns an hash associated to the container instance, used in different places internally
-
$c->getFactoryIds() returns an array of all IDs associated to factory closures
-
$c->getPropIds() returns an array of all IDs associated to properties (including protected closures)
-
$c->getFrozenIds() returns an array of all IDs of frozen items (both properties and factory closures)
-
$c->getObjectsInfo() returns an array containing informations about all cached objects (more info)
-
$c->getInfo() return a plain object (stdClass) where properties are informations on the current state of the instance, a sort of summary of all previous getters. Note that Faber implements JsonSerializable interface and when json_encode is called with a container instance, what is encoded is just the object returned by this method
Cached Objects
Thanks to the fact that factory closures accept arguments, for every factory closures can exist different cached objects. An example:, (*80)
``` php
// define a service
$container['foo'] = function( $c, $args ) {
return new Foo( $args );
};, (*81)
// create some objects
$foo_1 = $container->get( 'foo', [ 'id' => 'foo_1' ] );
$foo_2 = $container->get( 'foo', [ 'id' => 'foo_2' ] );
$foo_3 = $container->get( 'foo', [ 'id' => 'foo_3' ] );, (*82)
So there are three cached objects, all related to the factory with ID "foo".
Using following code is possible to prove that objects are cached:
``` php
$foo_3_bis = $container->get( 'foo', [ 'id' => 'foo_3' ] );
var_dump( $foo_3_bis === $foo_3 ); // TRUE: objects are cached
Worth to know that every cached object is stored in the container with an identifier (usually called $key)., (*83)
This identifier (aka "key") is visible among informations retrieved via $c->getObjectInfo() or $c->getInfo() methods., (*84)
It is a long hash string, but predictable if the ID of the factory closure and the array of used arguments are known. In facts, using those informations, object key can be retrieved via getObjectKey() method:, (*85)
``` php
$foo_3_key = $container->getObjectKey( 'foo', [ 'id' => 'foo_3' ] );, (*86)
echo $foo_3_key; // "foo_00000002264fbf000000005edc5c64_3cb01b53c9fbca307016d562fa42ce", (*87)
`isCachedObject()` method can be used with object key to know if a specific object has been cached or not
``` php
$key = $container->getObjectKey( 'foo', [ 'id' => 'foo_3' ] );
if ( $container->isCachedObject( $key ) ) {
// Yes, objects is cached
} else {
// No, objects is not cached
}
Freezing and Unfreezing Cached Objects
Up in this page is explained how to freeze and unfreeze factory closures.
Faber also allows to freeze and unfreeze specific cached objects: that is possible using same methods with object key instead of factory ID., (*88)
Note: when a factory closure is deleted or updated, all the cached objects it has generated are removed, unless specific cached object was frozen., (*89)
Example:, (*90)
``` php
// define a service
$container['foo'] = function( $c, $args ) {
return new Foo( $args );
};, (*91)
// create some objects
$foo_1 = $container->get( 'foo', [ 'id' => 'foo_1' ] );
$foo_2 = $container->get( 'foo', [ 'id' => 'foo_2' ] );, (*92)
// get object keys
$key_1 = $container->getObjectKey( 'foo', [ 'id' => 'foo_1' ] );
$key_2 = $container->getObjectKey( 'foo', [ 'id' => 'foo_2' ] );, (*93)
// test: are object cached?
var_dump( $container->isCachedObject( $key_1 ) ); // TRUE
var_dump( $container->isCachedObject( $key_2 ) ); // TRUE, (*94)
// freeze first object
$container->freeze( $key_1 );, (*95)
// delete factory closure
unset( $container['foo'] );, (*96)
// test: are objects related to deleted factory deleted as well?
var_dump( $container->isCachedObject( $key_1 ) ); // TRUE: still there because frozen
var_dump( $container->isCachedObject( $key_2 ) ); // FALSE: vanished, (*97)
// get cached object
$cached_foo = $container->get( $key_1 );, (*98)
Faber has helper method **`getAndFreeze()`** that gets an object and also freeze it:
``` php
$foo = $container->getAndFreeze( 'foo', [ 'id' => 'foo_1' ] );
$key = $container->getObjectKey( 'foo', [ 'id' => 'foo_1' ] );
var_dump( $container->isFrozen( $key ) ); // TRUE
About Serialization
Current versions of PHP have a limitation: can't serialize closures or any variable that contains closures, e. g. array containing closures or objects having closures in properties: trying to serialize them a catchable fatal error is thrown., (*99)
Faber is designed to have quite a lot closures as properties, but no errors happen when its instances are serialized, because Faber implements __sleep() and __wakeUp() PHP methods to prevent that., (*100)
When an instance of Faber is serialized and then unserialized, the serialized/unserialized object contains only the ID and the hash of original objects, that being strings, bring no problems., (*101)
A little sugar: serializing and then unserializing a Faber instance during same request will create an exact clone of original object, because on wake up container state is cloned from saved instance:, (*102)
``` php
$faber = GM\Faber::i( 'test' ); // or $faber = new GM\Faber(), (*103)
$faber['foo'] = function() {
return new Foo;
};, (*104)
$sleep = serialize( $faber );
$wakeup = unserialize( $sleep );, (*105)
$foo = $wakeup['foo'];
var_dump( $foo instanceof Foo ); // TRUE, (*106)
#Error Handling
Things can go wrong. Every method of Faber can fail, for different reasons.
When it happens, Faber returns an Error object that is a subclass of [`WP_Error`](http://codex.wordpress.org/Class_Reference/WP_Error) and so can be checked using [`is_wp_error()`](http://codex.wordpress.org/Function_Reference/is_wp_error) function.
``` php
$container[ 'foo' ] = 'Foo!';
$foo = $container[ 'foo' ];
if ( ! is_wp_error( $foo ) ) {
echo $foo; // Output "Foo!";
}
$meh = $container[ 'meh' ]; // 'meh' is an unregistered ID: $meh is a WP_Error object
if ( ! is_wp_error( $meh ) ) {
echo $meh; // not executed
}
The extended WP_Error class used by Faber works well with fluent interface. When using fluent interface everyone of the chained functions can fail and return a WP_Error object, so next method in the chain is called on error object instead of on Faber object., (*107)
That's not a problem: the custom error ebject will add an error to its errors storage and then returns itself, so that at the end of the chain an error object is obtained and it has track of every method called on it: debug happiness., (*108)
Requirements
Installation
In composer.json require Faber like so:, (*109)
{
"require": {
"php": ">=5.4",
"giuseppe-mazzapica/faber": "dev-master"
}
}
License
Faber is released under MIT., (*110)