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);
+ }
}