tflori / dependency-injector
Easy and lightweight dependency injector implementing PSR-11
Installs: 4 528
Dependents: 2
Suggesters: 0
Security: 0
Stars: 4
Watchers: 2
Forks: 1
Open Issues: 1
Requires
- php: ^7.0 || ^8.0
- psr/container: ^1.0
Requires (Dev)
This package is auto-updated.
Last update: 2024-11-03 22:28:52 UTC
README
A simple and lightweight dependency injector. Compatible to php standard recommendation for dependency injection containers (PSR-11).
What Is A Dependency
Something that your application needs to work correct. For example an instance of Calculator
or Config
or an object
that implements CacheInterface
.
Basic Usage
One of the biggest problems in software development is the tight coupling of dependencies. Imagine a class that creates
an instance from DatabaseConnection
with new DatabaseConnection()
. This is called a tight coupling - it is so tight
that you would have to overwrite your autoloader (what is not always possible) to mock the database connection.
There are two solutions to test this class without the need to have a database connection during the tests.
Passing The Dependency
When creating an object of that class you can provide the DatabaseConnection
to the constructor. For example
new MyService(new DatabaseConnection)
. In the tests you can then pass a mock
new MyService(m::mock(DatabaseConnection::class))
.
That would mean that everywhere in your application where you create an object of this class you have to know where your
DatabaseConnection
object is stored or create a new one. It's even more worse: when the interface for the class
changes (for example an additional dependency get added) you will have to change this everywhere in your code.
Here comes the dependency injection into play. Here with the most strait forward and understandable way (a callback):
<?php $container->share('databaseConnection', function () { return new DatabaseConnection(); }); $container->add('myService', function () use ($container) { return new MyService($container->get('databaseConnection')); });
Make The Container Available
The container could also be available from within the class *1. This library provides a class with only
static methods DI
to make the dependencies available from everywhere. It is using the same interface but with static
calls. The above example could look like this:
<?php DI::share('databaseConnection', function () { return new DatabaseConnection(); }); DI::add('myService', function () { return new MyService(); // we can access DI::get('databaseConnection') within this class now });
Tests
Now when we want to test the class we can just replace the dependency for database connection:
<?php DI::share('databaseConnection', function () { return m::mock(DatabaseConnection::class); }); DI::get('databaseConnection')->shouldReceive('query'); // ...
This works in both versions *2 and can safely be used for testing.
We are using here the static methods from DI in the rest of the document.
Advanced Usage
We can not only store callbacks that are executed when a new instance is required. There are some other practical ways that makes it easier for you to define how dependencies should be resolved.
Make An Object
With version 2.1 comes the new method DI::make(string $class, ...$args)
which allows you to directly get an instance
of $class
with $args
as constructor arguments without defining a dependency for it.
<?php $feed = DI::make(SomeFeed::class, $_GET['id']); // equals to new SomeFeed($_GET['id']);
Even if the above examples are equal the method has a big advantage: you can provide a mock for the class.
<?php DI::instance(SomeFeed::class, m::mock(SomeFeed::class)); $feedMock = DI::make(SomeFeed::class, $_GET['id']);
Define Instances
Instances can be defined to be returned when a dependency is requested. Keep in mind that you will have to instantiate a class before using it what might have an impact in performance. Anyway this gives you an opportunity to also define several values for example a very simple configuration:
<?php DI::instance('config', (object)[ 'database' => (object)[ 'dsn' => 'mysql://whatever', 'user' => 'john', 'password' => 'does_secret', ] ]);
Do not misuse this as a global storage. You will get naming conflicts and we will not provide solutions for it.
Define Aliases
Aliases allow you to have several names for a dependency. First define the dependency and than alias it:
<?php DI::share(Config::class, Config::class); DI::alias(Config::class, 'config'); DI::alias(Config::class, 'cfg');
Define Dependencies
Dependencies that are built when they are requested can be added using Container::add(string $name, $getter)
. The
getter can be a callable (such as closures - what we did above), a class name of a factory, an instance of a factory or
any other class name.
Factory here means a class that implements
FactoryInterface
orSharableFactoryInterface
.
Container:add()
will return the factory that got added and which factory get added is defined as:
- An instance of a factory: the given factory
- A class name of a factory: a new object of the given class
- A class name of a class using singleton pattern: a
SingletonFactory
- Any other class name: a
ClassFactory
- A callable: a
CallableFactory
Dependencies using factories implementing SharableFactoryInterface
can be shared by calling $factory->share()
or
using the shortcut Container::share(string $name, $getter)
.
Class Factory
The ClassFactory
creates only an instance without any arguments by default. It also allows to pass different arguments
to the constructor and that is the usual way suggested from PSR-11:
<?php // pass some statics DI::share('session', Session::class) ->addArguments('app-name', 3600, true); new Session(DI::has('app-name') ? DI::get('app-name') : 'app-name', 3600, true); // pass dependencies DI::share('database', Connection::class) ->addArguments('config'); new Connection(DI::has('config') ? DI::get('config') : 'config'); // pass a string that is defined as dependency DI::add('view', View::class) ->addArguments(new StringArgument('default-layout')); new View('default-layout');
It is also possible to call methods on the new instance:
<?php DI::share('cache', Redis::class) ->addMethodCall('connect', 'localhost', 4321, 1); // you can also bypass resolving the dependency DI::add('view', View::class) ->addMethodCall('setView', new StringArgument('default-view'));
Non shared classes allow to pass additional arguments to the constructor:
<?php DI::add('view', View::class); $view = DI::get('view', 'login'); new View('login');
Pattern Factory
A PatternFactory
is a factory that can be used for different names. Keep in mind that when ever a dependency is
requested and no other factory is defined for this dependency all pattern factories are requested to match against the
requested name.
Namespace Factory
An example for a PatternFactory
is the NamespaceFactory
. The namespace factory can be used to load all classes of a
previously defined namespace with this factory. Similar to the class factory it supports passing arguments and method
calls after the request.
This example shows a factory definition for Controller classes:
<?php use App\Http\Controller; use DependencyInjector\Factory\NamespaceFactory; use DependencyInjector\DI; $request = (object)$_SERVER; DI::add(Controller::class, (new NamespaceFactory(DI::getContainer(), Controller::class)) ->addArguments(DI::getContainer())); DI::get(Controller\UserController::class, $request); // equals to new Controller\UserController(DI::getContainer(), $request);
A shared namespace factory stores the instance per class:
<?php DI::share('ViewHelper', (new NamespaceFactory(DI::getContainer(), App\View\Helper::class)) ->addArguments(DI::getContainer())); DI::get(App\View\Helper\Url::class);
Singleton Factory
The SingletonFactory
is a special factory that just wraps the call to ::getInstance()
. The advantage here is that
you don't have to create the instance if you don't need to or create a mock object for tests. Without this factory you
can either pass an instance of the class or stick with the call to ::getInstance()
in your code.
This factory also allows pass arguments to the ::getInstance()
method for classes that store different instances for
specific arguments.
<?php DI::add('calculator', Calculator::class); DI::get('calculator', 'rad'); Calculator::getInstance('rad'); DI::get('calculator', 'deg'); Calculator::getInstance('deg');
Callable Factory
This factory is just calling the passed callback. The callback only have to be callable what is checked with
is_callable($getter)
- so you can also pass an array with class or instance and method name.
<?php DI::share('database', function() { $config = DI::get('config'); return new PDO($config->database->dsn, $config->database->username, $config->database->password); });
Because the callback could also be a static method from a class with
[Calculator::class, 'getInstance']
. It is also possible to use this for Singleton classes. The difference is that this could be shared but theSingletonFactory
always calls::getInstance()
what is the preferred method from our point of view.
Own factories
When you write own factories you will have to implement FactoryInterface
or SharableFactoryInterface
. The
AbstractFactory
implements SharableFactoryInterface
and can be extended to your needs in a very simple way:
<?php class DatabaseFactory extends \DependencyInjector\Factory\AbstractFactory { protected $shared = true; // false is default - so simple omit it for non shared factories or use share to define protected function build() { $dbConfig = $this->container->get('config')->database; return new PDO($dbConfig->dsn, $dbConfig->user, $dbConfig->password); } }
Factories can be defined for dependencies using Container::add()
or Container::share()
as described above. But you
can also register the namespace where your factories are defined and the container will try to find the factory for
the requested dependency. When you request a dependency and it is not already defined it will check each registered
namespace for a class named $namespace . '\\' . ucfirst($dependency) . $suffix
.
Examples
Here are some small examples how you could use this library.
The Configuration
<?php class Config { private static $_instance; public $database = [ 'host' => 'localhost', 'user' => 'john', 'password' => 'does.secret', 'database' => 'john_doe' ]; public $redis = ['host' => 'localhost']; private function __construct() { // maybe some logic to change the config or initialize variables } public static function getInstance() { if (!self::$_instance) { self::$_instance = new Config(); } return self::$_instance; } } DI::add('config', Config::class); // adds a SingletonFactory function someStaticFunction() { // before if (empty(Config::getInstance()->database['host'])) { throw new Exception('No database host configured'); } // now if (empty(DI::get('config')->database['host'])) { throw new Exception('No database host configured'); } }
The Database Connection
<?php DI::set('database', function() { $dbConfig = DI::get('config')->database; $mysql = new mysqli($dbConfig['host'], $dbConfig['user'], $dbConfig['password'], $dbConfig['database']); if (!empty($mysql->connect_error)) { throw new Exception('could not connect to database (' . $mysql->connect_error . ')'); } return $mysql; }); function someStaticFunction() { // before it maybe looked like this $mysql = MyApp::getDatabaseConnection(); // now $mysql = DI::get('database'); $mysql->query('SELECT * FROM table'); }
The problem before: you can not mock the static function MyApp::getDatabaseConnection()
. You also can not mock the
static function DI::get('database')
. But you can set the dependency to return a mock object:
<?php class ApplicationTest extends TestCase { public function testSomeStaticFunction() { // prepare the mock $mock = $this->getMock(mysqli::class); $mock->expects($this->once())->method('query') ->with('SELECT * FROM table'); // overwrite the dependency DI::instance('database', $mock); someStaticFunction(); } }
Tips
Extend The DI Class
When you are using the DI
class it makes sense to extend this class and add annotations for the __callStatic()
getter
so that your IDE knows what comes back from your DI
:
<?php /** * @method static Config config() * @method static mysqli database() */ class DI extends \DependencyInjector\DI {}
Extend The Container Class
Similar functionality exists for Container
. The magic method __isset()
aliases Container::has()
, __get()
aliases
Container::get($name)
and __call()
aliases Container::get($name, ...$args)
. So you can annotate your container
like this:
<?php /** * @property Config config * @method Config config() */ class Container extends \DependencyInjector\Container {}
Comments
-
*1 Some people say this is hiding the dependencies and is an anti pattern called
Service Locator
. Don't trust them. It's still clear what are the dependencies (you just have to search for them) and it could be easier to write. But the most crucial difference is that otherwise the instance gets created without a requirement. Assume you may need aDatabaseConnection
only if the cache does not already store the result - such things can have a huge impact when we are talking about large amounts of users. -
*2 In the meta document for PSR-11 they mention that it is harder to test when you pass the container to your objects. But - as we can see - it's not.