A new form of "Dependency Injection" in PHP
Not to be confused with Dependency Injection Container or Ioc Container
like Symfony DI Container, Pimple or Laravel Service Container,
DependencyPocket is not a DI Container
in that way, It's rather a new form of Dependency Injection
.
Essentially, there're two ways of dependency injection : Constructor Injection
and Setter Injection
,
I'd like to think of this technique as the third type, the Pocket Injection
.
composer require mmghv/dependency-pocket "~0.2"
We use this pocket inside our classes to hold the dependencies, we first create a new pocket :
use mmghv\DependencyPocket;
// ...
$this->pocket = new DependencyPocket();
Then we add the dependencies, We first define them (defining the Name
and the Type
of each dependency) :
$this->pocket->define([
'dep1', // allow any type
'dep2' => 'string', // primitive type
'dep3' => 'array', // primitive type
'dep4' => 'App\Model\Article' // class or interface
]);
Then we set our dependencies (set dependencies values) :
$this->pocket->set([
'dep1' => true,
'dep2' => 'some value',
'dep3' => [1, 2],
'dep4' => $myArticle
]);
Then when we want to get a dependency we simply do :
$dep = $this->pocket->get('myDep');
// or
$dep = $this->pocket->myDep;
Or we can use Property Overloading
to easily access our dependencies from our class (or subclasses) :
public function __get($name)
{
if ($this->pocket->has($name)) {
return $this->pocket->get($name);
} else {
throw new \Exception("Undefined property: [$name]");
}
}
Basically Its useful when your class has many dependencies which not all of them are required so you don't want your constructor to have all these dependencies but still need a way to easily change them in the subclasses and tests.
Imagine you have a class with 5 dependencies but only 2 of them are essential and the other 3 can be set to some default values, Then you extend this class and the subclass can resolve one of the two essential dependencies to a default value and needs to replace one of the optional dependencies of the parent class, You need to be able to do that and want your final class to have only one dependency in the constructor but still be able to change any default ones from any future subclasses as well as the ability to mock any of these dependencies in the tests.
Using DependencyPocket you can achieve that like the following without using a setter for each dependency and also mocking dependencies for tests is easier than using Setter Injection
method :
// Manager.php
namespace App\Managers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use mmghv\DependencyPocket;
class Manager
{
protected $pocket;
/**
* Define class dependencies.
*/
protected function defineDependencies()
{
if ($this->pocket) {
return true;
}
$this->pocket = new DependencyPocket();
$this->pocket->define([
'app' => 'Laravel\Lumen\Application',
'model' => 'Illuminate\Database\Eloquent\Model',
'validator' => 'Illuminate\Contracts\Validation\Factory',
'request' => 'Illuminate\Http\Request',
'redirectUrl' => 'string',
]);
}
/**
* Create new manager.
*
* @param Application $app
* @param Model $model
* @param array $dependencyPocket
*/
public function __construct(Application $app, Model $model, array $dependencyPocket = [])
{
$this->defineDependencies();
$this->pocket->set($dependencyPocket += [
'app' => $app,
'model' => $model,
'validator' => $app->make('validator'), // default value
'request' => $app->make('request'), // default value
'redirectUrl' => $app->make('session')->previousUrl(), // default value
]);
}
}
// ArticleManager.php
namespace App\Managers;
use Illuminate\Contracts\Foundation\Application;
use App\Models\Article;
class ArticleManager extends Manager
{
/**
* Define any additional class dependencies, Declare this function
* only when you need to define new dependencies.
*/
protected function defineDependencies()
{
if (parent::defineDependencies()) {
return true;
}
$this->pocket->define([
// define any new dependencies for this class
]);
}
/**
* Create new article-manager.
*
* @param Application $app
* @param array $dependencyPocket
*/
public function __construct(Application $app, array $dependencyPocket = [])
{
// always call this first
$this->defineDependencies();
// default value for $model dependency
$model = new Article();
// call parent construct and pass dependencies
parent::__construct($app, $model, $dependencyPocket += [
'validator' => new CustomValidator(), // replace default dependency value
// add any new dependencies for this calss, needs to be defined first in 'defineDependencies()'
]);
}
}
Then when we instantiate the ArticleManager
class we only need to pass one dependency like this :
$manager = new ArticleManager($app);
But also we have the ability to easily replace any default dependencies when we want like this :
$manager = new ArticleManager($app, [
'model' => $anotherModel,
'validator' => $anotherValidator
]);
This is a relatively new technique so any contributions (suggestions, enhancements to the technique used) are welcome. PSR-2 standards are used and tests should cover any changes in case of PRs.
Copyright © 2016, Mohamed Gharib. Released under the MIT license.