From 437825be70c0bd72b582244c243cc985c11f46d6 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 17 Nov 2016 12:32:10 +0200 Subject: [PATCH] Implemented Container::setDependencies(), Container::setDefinitions Closes #11758 Closes #13029 --- docs/guide/concept-configurations.md | 22 ++++ docs/guide/concept-di-container.md | 153 +++++++++++++++++++++-- framework/CHANGELOG.md | 1 + framework/base/Application.php | 17 +++ framework/di/Container.php | 80 ++++++++++++ tests/framework/base/ApplicationTest.php | 32 +++++ tests/framework/di/ContainerTest.php | 57 +++++++++ 7 files changed, 353 insertions(+), 9 deletions(-) create mode 100644 tests/framework/base/ApplicationTest.php diff --git a/docs/guide/concept-configurations.md b/docs/guide/concept-configurations.md index a5f7905061..0afefb2e04 100644 --- a/docs/guide/concept-configurations.md +++ b/docs/guide/concept-configurations.md @@ -135,6 +135,28 @@ an [entry script](structure-entry-scripts.md), where the class name is already g More details about configuring the `components` property of an application can be found in the [Applications](structure-applications.md) section and the [Service Locator](concept-service-locator.md) section. +Since version 2.0.11, the application configuration supports [Dependency Injection Container](concept-di-container.md) +configuration using `container` property. For example: + +```php +$config = [ + 'id' => 'basic', + 'basePath' => dirname(__DIR__), + 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), + 'container' => [ + 'definitions' => [ + 'yii\widgets\LinkPager' => ['maxButtonCount' => 5] + ], + 'singletons' => [ + // Dependency Injection Container singletons configuration + ] + ] +]; +``` + +To know more about the possible values of `definitions` and `singletons` configuration arrays and real-life examples, +please read [Advanced Practical Usage](concept-di-container.md#advanced-practical-usage) subsection of the +[Dependency Injection Container](concept-di-container.md) article. ### Widget Configurations diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 976730b2af..2b561e5756 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -224,11 +224,13 @@ and the container will automatically resolve dependencies by instantiating them them into the newly created objects. The dependency resolution is recursive, meaning that if a dependency has other dependencies, those dependencies will also be resolved automatically. -You can use [[yii\di\Container::get()]] to create new objects. The method takes a dependency name, -which can be a class name, an interface name or an alias name. The dependency name may or may -not be registered via `set()` or `setSingleton()`. You may optionally provide a list of class -constructor parameters and a [configuration](concept-configurations.md) to configure the newly created object. -For example, +You can use [[yii\di\Container::get()|get()]] to either create or get object instance. +The method takes a dependency name, which can be a class name, an interface name or an alias name. +The dependency name may be registered via [[yii\di\Container::set()|set()]] +or [[yii\di\Container::setSingleton()|setSingleton()]]. You may optionally provide a list of class +constructor parameters and a [configuration](concept-configurations.md) to configure the newly created object. + +For example: ```php // "db" is a previously registered alias name @@ -312,10 +314,10 @@ Yii creates a DI container when you include the `Yii.php` file in the [entry scr of your application. The DI container is accessible via [[Yii::$container]]. When you call [[Yii::createObject()]], the method will actually call the container's [[yii\di\Container::get()|get()]] method to create a new object. As aforementioned, the DI container will automatically resolve the dependencies (if any) and inject them -into the newly created object. Because Yii uses [[Yii::createObject()]] in most of its core code to create +into obtained object. Because Yii uses [[Yii::createObject()]] in most of its core code to create new objects, this means you can customize the objects globally by dealing with [[Yii::$container]]. -For example, you can customize globally the default number of pagination buttons of [[yii\widgets\LinkPager]]: +For example, let's customize globally the default number of pagination buttons of [[yii\widgets\LinkPager]]. ```php \Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]); @@ -368,6 +370,138 @@ cannot be instantiated. This is because you need to tell the DI container how to Now if you access the controller again, an instance of `app\components\BookingService` will be created and injected as the 3rd parameter to the controller's constructor. +Advanced Practical Usage +--------------- + +Say we work on API application and have: + - `app\components\Request` class that extends `yii\web\Request` and provides additional functionality + - `app\components\Response` class that extends `yii\web\Response` and should have `format` property + set to `json` on creation + - `app\storage\FileStorage` and `app\storage\DocumentsReader` classes the implement some logic on + working with documents that are located in some file storage: + ```php + class FileStorage + { + public function __contruct($root) { + // whatever + } + } + + class DocumentsReader + { + public function __contruct(FileStorage $fs) { + // whatever + } + } + ``` + +It is possible to configure multiple definitions at once, passing configuration array to +[[yii\di\Container::setDefinitions()|setDefinitions()]] or [[yii\di\Container::setSingletons()|setSingletons()]] method. +Iterating over the configuration array, the methods will call [[yii\di\Container::set()|set()]] +or [[yii\di\Container::setSingleton()|setSingleton()]] respectively for each item. + +The configuration array format is: + + - `key`: class name, interface name or alias name. The key will be passed to the + [[yii\di\Container::set()|set()]] method as a first argument `$class`. + - `value`: the definition associated with `$class`. Possible values are described in [[yii\di\Container::set()|set()]] + documentation for the `$definition` parameter. Will be passed to the [[set()]] method as + the second argument `$definition`. + +For example, let's configure our container to follow the aforementioned requirements: + +```php +$container->setDefinitions([ + 'yii\web\Request' => 'app\components\Request', + 'yii\web\Response' => [ + 'class' => 'app\components\Response', + 'format' => 'json' + ], + 'app\storage\DocumentsReader' => function () { + $fs = new app\storage\FileStorage('/var/tempfiles'); + return new app\storage\DocumentsReader($fs); + } +]); + +$reader = $container->get('app\storage\DocumentsReader); +// Will create DocumentReader object with its dependencies as described in the config +``` + +> Tip: Container may be configured in declarative style using application configuration since version 2.0.11. +Check out the [Application Configurations](concept-service-locator.md#application-configurations) subsection of +the [Configurations](concept-configurations.md) guide article. + +Everything works, but in case we need to create create `DocumentWriter` class, +we shall copy-paste the line that creates `FileStorage` object, that is not the smartest way, obviously. + +As described in the [Resolving Dependencies](#resolving-dependencies) subsection, [[yii\di\Container::set()|set()]] +and [[yii\di\Container::setSingleton()|setSingleton()]] can optionally take dependency's constructor parameters as +a third argument. To set the constructor parameters, you may use the following configuration array format: + + - `key`: class name, interface name or alias name. The key will be passed to the + [[yii\di\Container::set()|set()]] method as a first argument `$class`. + - `value`: array of two elements. The first element will be passed the [[yii\di\Container::set()|set()]] method as the + second argument `$definition`, the second one — as `$params`. + +Let's modify our example: + +```php +$container->setDefinitions([ + 'tempFileStorage' => [ // we've created an alias for convenience + ['class' => 'app\storage\FileStorage'], + ['/var/tempfiles'] // could be extracted from some config files + ], + 'app\storage\DocumentsReader' => [ + ['class' => 'app\storage\DocumentsReader'], + [Instance::of('tempFileStorage')] + ], + 'app\storage\DocumentsWriter' => [ + ['class' => 'app\storage\DocumentsWriter'], + [Instance::of('tempFileStorage')] + ] +]); + +$reader = $container->get('app\storage\DocumentsReader); +// Will behave exactly the same as in the previous example. +``` + +You might notice `Instance::of('tempFileStorage')` notation. It means, that the [[yii\di\Container|Container]] +will implicitly provide dependency, registered with `tempFileStorage` name and pass it as the first argument +of `app\storage\DocumentsWriter` constructor. + +> Note: [[yii\di\Container::setDefinitions()|setDefinitions()]] and [[yii\di\Container::setSingletons()|setSingletons()]] + methods are available since version 2.0.11. + +Another step on configuration optimization is to register some dependencies as singletons. +A dependency registered via [[yii\di\Container::set()|set()]] will be instantiated each time it is needed. +Some classes do not change the state during runtime, therefore they may be registered as singletons +in order to increase the application performance. + +A good example could be `app\storage\FileStorage` class, that executes some operations on file system with a simple +API (e.g. `$fs->read()`, `$fs->write()`). These operations do not change the internal class state, so we can +create its instance once and use it multiple times. + +```php +$container->setSingletons([ + 'tempFileStorage' => [ + ['class' => 'app\storage\FileStorage'], + ['/var/tempfiles'] + ], +]); + +$container->setDefinitions([ + 'app\storage\DocumentsReader' => [ + ['class' => 'app\storage\DocumentsReader'], + [Instance::of('tempFileStorage')] + ], + 'app\storage\DocumentsWriter' => [ + ['class' => 'app\storage\DocumentsWriter'], + [Instance::of('tempFileStorage')] + ] +]); + +$reader = $container->get('app\storage\DocumentsReader); +``` When to Register Dependencies ----------------------------- @@ -375,8 +509,9 @@ When to Register Dependencies Because dependencies are needed when new objects are being created, their registration should be done as early as possible. The following are the recommended practices: -* If you are the developer of an application, you can register dependencies in your - application's [entry script](structure-entry-scripts.md) or in a script that is included by the entry script. +* If you are the developer of an application, you can register your dependencies using application configuration. + Please, read the [Application Configurations](concept-service-locator.md#application-configurations) subsection of + the [Configurations](concept-configurations.md) guide article. * If you are the developer of a redistributable [extension](structure-extensions.md), you can register dependencies in the bootstrapping class of the extension. diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 3c263771a5..64bdccf15e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -57,6 +57,7 @@ Yii Framework 2 Change Log - Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) - Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) +- Enh #11758: Implemented Dependency Injection Container configuration using Application configuration array (silverfire) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) diff --git a/framework/base/Application.php b/framework/base/Application.php index f31bfd1d27..03688807d8 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -246,6 +246,12 @@ abstract class Application extends Module $this->setTimeZone('UTC'); } + if (isset($config['container'])) { + $this->setContainer($config['container']); + + unset($config['container']); + } + // merge core components with custom components foreach ($this->coreComponents() as $id => $component) { if (!isset($config['components'][$id])) { @@ -652,4 +658,15 @@ abstract class Application extends Module exit($status); } } + + /** + * Configures [[Yii::$container]] with the $config + * + * @param array $config values given in terms of name-value pairs + * @since 2.0.11 + */ + public function setContainer($config) + { + Yii::configure(Yii::$container, $config); + } } diff --git a/framework/di/Container.php b/framework/di/Container.php index 3649d6fe9c..5446126d43 100644 --- a/framework/di/Container.php +++ b/framework/di/Container.php @@ -566,4 +566,84 @@ class Container extends Component } return $args; } + + /** + * Registers class definitions within this container. + * + * @param array $definitions array of definitions. There are two allowed formats of array. + * The first format: + * - key: class name, interface name or alias name. The key will be passed to the [[set()]] method + * as a first argument `$class`. + * - value: the definition associated with `$class`. Possible values are described in + * [[set()]] documentation for the `$definition` parameter. Will be passed to the [[set()]] method + * as the second argument `$definition`. + * + * Example: + * ```php + * $container->setDefinitions([ + * 'yii\web\Request' => 'app\components\Request', + * 'yii\web\Response' => [ + * 'class' => 'app\components\Response', + * 'format' => 'json' + * ], + * 'foo\Bar' => function () { + * $qux = new Qux; + * $foo = new Foo($qux); + * return new Bar($foo); + * } + * ]); + * ``` + * + * The second format: + * - key: class name, interface name or alias name. The key will be passed to the [[set()]] method + * as a first argument `$class`. + * - value: array of two elements. The first element will be passed the [[set()]] method as the + * second argument `$definition`, the second one — as `$params`. + * + * Example: + * ```php + * $container->setDefinitions([ + * 'foo\Bar' => [ + * ['class' => 'app\Bar'], + * [Instance::of('baz')] + * ] + * ]); + * ``` + * + * @see set() to know more about possible values of definitions + * @since 2.0.11 + */ + public function setDefinitions(array $definitions) + { + foreach ($definitions as $class => $definition) { + if (count($definition) === 2 && array_values($definition) === $definition) { + $this->set($class, $definition[0], $definition[1]); + continue; + } + + $this->set($class, $definition); + } + } + + /** + * Registers class definitions as singletons within this container by calling [[setSingleton()]] + * + * @param array $singletons array of singleton definitions. See [[setDefinitions()]] + * for allowed formats of array. + * + * @see setDefinitions() for allowed formats of $singletons parameter + * @see setSingleton() to know more about possible values of definitions + * @since 2.0.11 + */ + public function setSingletons(array $singletons) + { + foreach ($singletons as $class => $definition) { + if (count($definition) === 2 && array_values($definition) === $definition) { + $this->setSingleton($class, $definition[0], $definition[1]); + continue; + } + + $this->setSingleton($class, $definition); + } + } } diff --git a/tests/framework/base/ApplicationTest.php b/tests/framework/base/ApplicationTest.php new file mode 100644 index 0000000000..2be0858d93 --- /dev/null +++ b/tests/framework/base/ApplicationTest.php @@ -0,0 +1,32 @@ +mockApplication([ + 'container' => [ + 'definitions' => [ + Dispatcher::className() => DispatcherMock::className() + ] + ], + 'bootstrap' => ['log'] + ]); + + $this->assertInstanceOf(DispatcherMock::className(), Yii::$app->log); + } +} + +class DispatcherMock extends Dispatcher +{ + +} diff --git a/tests/framework/di/ContainerTest.php b/tests/framework/di/ContainerTest.php index c6ac6db1ad..fe632a8b78 100644 --- a/tests/framework/di/ContainerTest.php +++ b/tests/framework/di/ContainerTest.php @@ -10,6 +10,9 @@ namespace yiiunit\framework\di; use Yii; use yii\di\Container; use yii\di\Instance; +use yiiunit\data\ar\Cat; +use yiiunit\data\ar\Order; +use yiiunit\data\ar\Type; use yiiunit\framework\di\stubs\Bar; use yiiunit\framework\di\stubs\Foo; use yiiunit\framework\di\stubs\Qux; @@ -223,4 +226,58 @@ class ContainerTest extends TestCase }; $this->assertNull($container->invoke($closure)); } + + public function testSetDependencies() + { + $container = new Container(); + $container->setDefinitions([ + 'model.order' => Order::className(), + Cat::className() => Type::className(), + 'test\TraversableInterface' => [ + ['class' => 'yiiunit\data\base\TraversableObject'], + [['item1', 'item2']] + ], + 'qux.using.closure' => function () { + return new Qux(); + } + ]); + $container->setDefinitions([]); + + $this->assertInstanceOf(Order::className(), $container->get('model.order')); + $this->assertInstanceOf(Type::className(), $container->get(Cat::className())); + + $traversable = $container->get('test\TraversableInterface'); + $this->assertInstanceOf('yiiunit\data\base\TraversableObject', $traversable); + $this->assertEquals('item1', $traversable->current()); + + $this->assertInstanceOf('yiiunit\framework\di\stubs\Qux', $container->get('qux.using.closure')); + } + + public function testContainerSingletons() + { + $container = new Container(); + $container->setSingletons([ + 'model.order' => Order::className(), + 'test\TraversableInterface' => [ + ['class' => 'yiiunit\data\base\TraversableObject'], + [['item1', 'item2']] + ], + 'qux.using.closure' => function () { + return new Qux(); + } + ]); + $container->setSingletons([]); + + $order = $container->get('model.order'); + $sameOrder = $container->get('model.order'); + $this->assertSame($order, $sameOrder); + + $traversable = $container->get('test\TraversableInterface'); + $sameTraversable = $container->get('test\TraversableInterface'); + $this->assertSame($traversable, $sameTraversable); + + $foo = $container->get('qux.using.closure'); + $sameFoo = $container->get('qux.using.closure'); + $this->assertSame($foo, $sameFoo); + } }