tflori/dependency-injector
Composer 安装命令:
composer require tflori/dependency-injector
包简介
Easy and lightweight dependency injector implementing PSR-11
README 文档
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
FactoryInterfaceorSharableFactoryInterface.
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 theSingletonFactoryalways 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::share('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 aDatabaseConnectiononly 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.
tflori/dependency-injector 适用场景与选型建议
tflori/dependency-injector 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 6.08k 次下载、GitHub Stars 达 4, 最近一次更新时间为 2016 年 08 月 19 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「dependency injection」 「dependency」 「di」 「dependencies」 「dependency injector」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 tflori/dependency-injector 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 tflori/dependency-injector 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 tflori/dependency-injector 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Dependencies resolver for Symfony Bundles
Dependency graph visualization for composer.json
g4 dependencies injection container based on Pimple
Provides Function To Check if a plugin is active/inactive & function to compare versions.
A fast and intuitive dependency injection container.
Enforce architecture by defining modules with allowed dependencies. Detects forbidden, uncovered, missing and unused module dependencies in PHP projects.
统计信息
- 总下载量: 6.08k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 5
- 点击次数: 26
- 依赖项目数: 2
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2016-08-19