pwm / sfw-container
一个简单的容器,确保无环依赖图
3.0.0
2018-10-12 21:12 UTC
Requires
- php: >=7.1.0
Requires (Dev)
- infection/infection: ^0.8.2
- phpstan/phpstan: ^0.7.0
- phpunit/phpunit: ^6.1
- squizlabs/php_codesniffer: ^3.0
README
一个具有循环检测、可缓存实例解析和动态加载的最小化DI容器。
目录
为什么
市面上有各种各样的DI容器。我的设计目标是
- 简约
- 在依赖图中进行循环检测
- 默认实例缓存
- 动态加载
要求
PHP 7.1+
安装
composer require pwm/sfw-container
使用
基本用法
// Have some classes, some of them depend on others class A { public function __construct(int $x) {} } class B { public function __construct(string $s) {} } class C { public function __construct(A $a, B $b) {} } // Create a container $c = new Container(); // Add resolver functions to the container that will instantiate your classes $c->add(A::class, function (): A { return new A(1); }); $c->add(B::class, function (): B { return new B('x'); }); // Resolving from within resolvers is easy as the Container is passed to the functions as the 1st parameter $c->add(C::class, function (Container $c): C { return new C( $c->resolve(A::class), $c->resolve(B::class) ); }); // Resolve your classes assert($c->resolve(A::class) instanceof A); assert($c->resolve(B::class) instanceof B); assert($c->resolve(C::class) instanceof C);
循环检测
// X depends on Y and Y depends on X ... class X { public function __construct(Y $y) {} } class Y { public function __construct(X $x) {} } $c = new Container(); $c->add(X::class, function (Container $c): X { return new X($c->resolve(Y::class)); }); $c->add(Y::class, function (Container $c): Y { return new Y($c->resolve(X::class)); }); try { $c->resolve(X::class); } catch (CycleDetected $e) { assert('X -> Y -> X' === $e->getMessage()); }
工厂与缓存实例
// Simple class that saves a timestamp class TS { private $timestamp; public function __construct(int $timestamp) { $this->timestamp = $timestamp; } public function getTimestamp(): int { return $this->timestamp; } } $c = new Container(); // Add our TS class both cached and as a factory (using different keys) $c->add('cachedTS', function (): TS { return new TS(time()); }); $c->factory('factoryTS', function (): TS { return new TS(time()); }); // Get an instance for both $cTS = $c->resolve('cachedTS'); // instantiate and cache $fTS = $c->resolve('factoryTS'); // just instantiate // Wait a sec ... sleep(1); // For the cached ones timestamps will match assert($cTS->getTimestamp() === $c->resolve('cachedTS')->getTimestamp()); // Factory instantiates again, hence timestamps will differ assert($fTS->getTimestamp() !== $c->resolve('factoryTS')->getTimestamp());
动态加载
interface Reader { public function read(): string; } class XmlReader implements Reader { public function read(): string { return 'Reading Xml...'; } } class JsonReader implements Reader { public function read(): string { return 'Reading Json...'; } } $c = new Container(); // Reader's resolver has to be added via factory otherwise // it will be bound to whatever it first resolves to $c->factory(Reader::class, function (Container $c, string $strategy): Reader { switch ($strategy) { case 'xml': return new XmlReader(); case 'json': return new JsonReader(); default: throw new RuntimeException(sprintf('No reader found for %s', $strategy)); } }); assert('Reading Xml...' === $c->resolve(Reader::class, 'xml')-> read()); assert('Reading Json...' === $c->resolve(Reader::class, 'json')-> read()); try { $c->resolve(Reader::class, 'csv'); } catch (RuntimeException $e) { assert('No reader found for csv' === $e->getMessage()); }
工作原理
容器只是一个映射,键是我们定义的类的ID,值是称为解析器的函数。解析一个类简单地意味着执行它的解析器,这将返回该类的实例。使用包含命名空间的全类名作为ID是良好的实践,但我们可以使用任何我们喜欢的字符串。
所有解析器都将容器本身作为它们的第一个参数,这使得在解析器内部进行解析变得容易,从而使解析过程递归。这对于类可能具有其他类作为依赖项非常有用。
虽然我们可以构建我们喜欢的任何依赖图,但解析过程将在遍历过程中遇到循环时停止。这确保我们只处理无环图(即DAGs),并使我们免于调用栈溢出。
容器默认缓存解析实例,这意味着后续的解析将返回相同的实例。这是好的,通常也是我们想要的。然而,如果解析器是通过factory()
方法添加的,则每次解析都将返回该类的新实例。
我们还可以向解析器传递额外的参数。结合factory()
,这使得实现动态加载成为可能,即以不同的方式实例化接口。一个例子是所谓的策略模式,即使用运行时信息,例如用户提供的CLI参数,来实例化某些接口的适当实现。
测试
$ vendor/bin/phpunit
$ composer phpcs
$ composer phpstan
$ composer infection