From 65c2ade8ed66b61967b08b36be918e1930dbd90d Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Fri, 2 Dec 2016 10:04:59 +0200 Subject: [PATCH 1/6] Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX Fixes #13118 --- framework/CHANGELOG.md | 1 + framework/assets/yii.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9030302d8a..632335a2d2 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,6 +27,7 @@ Yii Framework 2 Change Log - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) +- Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) - Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index e2db617a38..a73c296787 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -164,7 +164,7 @@ window.yii = (function ($) { pjaxContainer, pjaxOptions = {}; - if (pjax !== undefined && $.support.pjax) { + if (pjax !== undefined && pjax !== 0 && $.support.pjax) { if ($e.data('pjax-container')) { pjaxContainer = $e.data('pjax-container'); } else { From 9807b2e1a1ac1c671c860ab448eab8bf9154b2bf Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Fri, 2 Dec 2016 10:09:08 +0200 Subject: [PATCH 2/6] Follow-up to 65c2ade. Smarter fix of #13118 --- framework/assets/yii.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index a73c296787..372ae45ae4 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -153,7 +153,7 @@ window.yii = (function ($) { method = !$e.data('method') && $form ? $form.attr('method') : $e.data('method'), action = $e.attr('href'), params = $e.data('params'), - pjax = $e.data('pjax'), + pjax = $e.data('pjax') || 0, pjaxPushState = !!$e.data('pjax-push-state'), pjaxReplaceState = !!$e.data('pjax-replace-state'), pjaxTimeout = $e.data('pjax-timeout'), @@ -164,7 +164,7 @@ window.yii = (function ($) { pjaxContainer, pjaxOptions = {}; - if (pjax !== undefined && pjax !== 0 && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { if ($e.data('pjax-container')) { pjaxContainer = $e.data('pjax-container'); } else { @@ -190,13 +190,13 @@ window.yii = (function ($) { if (method === undefined) { if (action && action != '#') { - if (pjax !== undefined && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { $.pjax.click(event, pjaxOptions); } else { window.location = action; } } else if ($e.is(':submit') && $form.length) { - if (pjax !== undefined && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) @@ -249,7 +249,7 @@ window.yii = (function ($) { oldAction = $form.attr('action'); $form.attr('action', action); } - if (pjax !== undefined && $.support.pjax) { + if (pjax !== 0 && $.support.pjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) From c17766181feca90a7642ffa4fcd7c52655fc4490 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Mon, 10 Oct 2016 15:35:07 +0300 Subject: [PATCH 3/6] Added `QueryInterface::emulateExecution()` Added `QueryInterface::emulateExecution()`, which allows preventing of the actual query execution. This allows to cancel `DataProvider` preventing search query execution in case search model is invalid: ``` php public function search($params) { $query = Item::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); $this->load($params); if (!$this->validate()) { $query->where('0=1'); $query->emulateExecution(); // No SQL execution will be done return $dataProvider; } ``` This also fix unecessary query in case of `via()` usage. See #12390. fixes #12390 fixes #6373 close #12708 --- framework/CHANGELOG.md | 6 ++- framework/UPGRADE.md | 6 +++ framework/db/ActiveRelationTrait.php | 6 +++ framework/db/Query.php | 29 ++++++++++ framework/db/QueryInterface.php | 12 +++++ framework/db/QueryTrait.php | 22 ++++++++ tests/framework/db/ActiveRecordTest.php | 66 +++++++++++++++++++++++ tests/framework/db/QueryTest.php | 70 +++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 632335a2d2..3c263771a5 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -28,7 +28,8 @@ Yii Framework 2 Change Log - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) -- Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) +- Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) +- Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) @@ -37,6 +38,7 @@ Yii Framework 2 Change Log - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) +- Enh #12390: Avoid creating queries with false where contdition (`0=1`) when fetching relational data (klimov-paul) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) @@ -53,7 +55,7 @@ Yii Framework 2 Change Log - Enh #13036: Added shortcut methods `asJson()` and `asXml()` for returning JSON and XML data in web controller actions (cebe) - Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar) - 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 #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: 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/UPGRADE.md b/framework/UPGRADE.md index d91e00d9d1..7c67fd70a2 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -50,6 +50,12 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.10 +----------------------- + +* A new method `public function emulateExecution($value = true);` has been added to the `yii\db\QueryInterace`. + This method is implemented in the `yii\db\QueryTrait`, so this only affects your code if you implement QueryInterface + in a class that does not use the trait. Upgrade from Yii 2.0.9 ---------------------- diff --git a/framework/db/ActiveRelationTrait.php b/framework/db/ActiveRelationTrait.php index b8c5241160..4d9caf3d4f 100644 --- a/framework/db/ActiveRelationTrait.php +++ b/framework/db/ActiveRelationTrait.php @@ -464,6 +464,9 @@ trait ActiveRelationTrait } } } + if (empty($values)) { + $this->emulateExecution(); + } } else { // composite keys @@ -478,6 +481,9 @@ trait ActiveRelationTrait $v[$attribute] = $model[$link]; } $values[] = $v; + if (empty($v)) { + $this->emulateExecution(); + } } } $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); diff --git a/framework/db/Query.php b/framework/db/Query.php index 6fe9bfc9f9..1bb831da0d 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -207,6 +207,9 @@ class Query extends Component implements QueryInterface */ public function all($db = null) { + if ($this->emulateExecution) { + return []; + } $rows = $this->createCommand($db)->queryAll(); return $this->populate($rows); } @@ -244,6 +247,9 @@ class Query extends Component implements QueryInterface */ public function one($db = null) { + if ($this->emulateExecution) { + return false; + } return $this->createCommand($db)->queryOne(); } @@ -257,6 +263,9 @@ class Query extends Component implements QueryInterface */ public function scalar($db = null) { + if ($this->emulateExecution) { + return null; + } return $this->createCommand($db)->queryScalar(); } @@ -268,6 +277,10 @@ class Query extends Component implements QueryInterface */ public function column($db = null) { + if ($this->emulateExecution) { + return []; + } + if ($this->indexBy === null) { return $this->createCommand($db)->queryColumn(); } @@ -300,6 +313,9 @@ class Query extends Component implements QueryInterface */ public function count($q = '*', $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("COUNT($q)", $db); } @@ -313,6 +329,9 @@ class Query extends Component implements QueryInterface */ public function sum($q, $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("SUM($q)", $db); } @@ -326,6 +345,9 @@ class Query extends Component implements QueryInterface */ public function average($q, $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("AVG($q)", $db); } @@ -363,6 +385,9 @@ class Query extends Component implements QueryInterface */ public function exists($db = null) { + if ($this->emulateExecution) { + return false; + } $command = $this->createCommand($db); $params = $command->params; $command->setSql($command->db->getQueryBuilder()->selectExists($command->getSql())); @@ -379,6 +404,10 @@ class Query extends Component implements QueryInterface */ protected function queryScalar($selectExpression, $db) { + if ($this->emulateExecution) { + return null; + } + $select = $this->select; $limit = $this->limit; $offset = $this->offset; diff --git a/framework/db/QueryInterface.php b/framework/db/QueryInterface.php index 28d16c9e1c..7503fa3664 100644 --- a/framework/db/QueryInterface.php +++ b/framework/db/QueryInterface.php @@ -252,4 +252,16 @@ interface QueryInterface * @return $this the query object itself */ public function offset($offset); + + /** + * Sets whether to emulate query execution, preventing any interaction with data storage. + * After this mode is enabled, methods, returning query results like [[one()]], [[all()]], [[exists()]] + * and so on, will return empty or false values. + * You should use this method in case your program logic indicates query should not return any results, like + * in case you set false where condition like `0=1`. + * @param boolean $value whether to prevent query execution. + * @return $this the query object itself. + * @since 2.0.11 + */ + public function emulateExecution($value = true); } diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php index 4a64903d0b..67cd2bf215 100644 --- a/framework/db/QueryTrait.php +++ b/framework/db/QueryTrait.php @@ -50,6 +50,12 @@ trait QueryTrait * row data. For more details, see [[indexBy()]]. This property is only used by [[QueryInterface::all()|all()]]. */ public $indexBy; + /** + * @var boolean whether to emulate the actual query execution, returning empty or false results. + * @see emulateExecution() + * @since 2.0.11 + */ + public $emulateExecution = false; /** @@ -388,4 +394,20 @@ trait QueryTrait $this->offset = $offset; return $this; } + + /** + * Sets whether to emulate query execution, preventing any interaction with data storage. + * After this mode is enabled, methods, returning query results like [[one()]], [[all()]], [[exists()]] + * and so on, will return empty or false values. + * You should use this method in case your program logic indicates query should not return any results, like + * in case you set false where condition like `0=1`. + * @param boolean $value whether to prevent query execution. + * @return $this the query object itself. + * @since 2.0.11 + */ + public function emulateExecution($value = true) + { + $this->emulateExecution = $value; + return $this; + } } diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 709757bf7b..8137243752 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -1294,4 +1294,70 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertEquals($newTotal, $newOrder->total); } + public function testEmulateExecution() + { + $this->assertGreaterThan(0, Customer::find()->from('customer')->count()); + + $rows = Customer::find() + ->from('customer') + ->emulateExecution() + ->all(); + $this->assertSame([], $rows); + + $row = Customer::find() + ->from('customer') + ->emulateExecution() + ->one(); + $this->assertSame(null, $row); + + $exists = Customer::find() + ->from('customer') + ->emulateExecution() + ->exists(); + $this->assertSame(false, $exists); + + $count = Customer::find() + ->from('customer') + ->emulateExecution() + ->count(); + $this->assertSame(0, $count); + + $sum = Customer::find() + ->from('customer') + ->emulateExecution() + ->sum('id'); + $this->assertSame(0, $sum); + + $sum = Customer::find() + ->from('customer') + ->emulateExecution() + ->average('id'); + $this->assertSame(0, $sum); + + $max = Customer::find() + ->from('customer') + ->emulateExecution() + ->max('id'); + $this->assertSame(null, $max); + + $min = Customer::find() + ->from('customer') + ->emulateExecution() + ->min('id'); + $this->assertSame(null, $min); + + $scalar = Customer::find() + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->scalar(); + $this->assertSame(null, $scalar); + + $column = Customer::find() + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->column(); + $this->assertSame([], $column); + } } diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index a5a68f1420..005f9a0c85 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\db; +use yii\db\Connection; use yii\db\Expression; use yii\db\Query; @@ -317,4 +318,73 @@ abstract class QueryTest extends DatabaseTestCase $count = (new Query)->from('customer')->having(['status' => 2])->count('*', $db); $this->assertEquals(1, $count); } + + public function testEmulateExecution() + { + $db = $this->getConnection(); + + $this->assertGreaterThan(0, (new Query())->from('customer')->count('*', $db)); + + $rows = (new Query()) + ->from('customer') + ->emulateExecution() + ->all($db); + $this->assertSame([], $rows); + + $row = (new Query()) + ->from('customer') + ->emulateExecution() + ->one($db); + $this->assertSame(false, $row); + + $exists = (new Query()) + ->from('customer') + ->emulateExecution() + ->exists($db); + $this->assertSame(false, $exists); + + $count = (new Query()) + ->from('customer') + ->emulateExecution() + ->count('*', $db); + $this->assertSame(0, $count); + + $sum = (new Query()) + ->from('customer') + ->emulateExecution() + ->sum('id', $db); + $this->assertSame(0, $sum); + + $sum = (new Query()) + ->from('customer') + ->emulateExecution() + ->average('id', $db); + $this->assertSame(0, $sum); + + $max = (new Query()) + ->from('customer') + ->emulateExecution() + ->max('id', $db); + $this->assertSame(null, $max); + + $min = (new Query()) + ->from('customer') + ->emulateExecution() + ->min('id', $db); + $this->assertSame(null, $min); + + $scalar = (new Query()) + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->scalar($db); + $this->assertSame(null, $scalar); + + $column = (new Query()) + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->column($db); + $this->assertSame([], $column); + } } From 437825be70c0bd72b582244c243cc985c11f46d6 Mon Sep 17 00:00:00 2001 From: SilverFire - Dmitry Naumenko Date: Thu, 17 Nov 2016 12:32:10 +0200 Subject: [PATCH 4/6] 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); + } } From fc0752f388d7dec6c82ac5ec431702a188bb8c81 Mon Sep 17 00:00:00 2001 From: Robert Korulczyk Date: Fri, 2 Dec 2016 15:56:49 +0100 Subject: [PATCH 5/6] Disable `Controversial/CamelCasePropertyName` codeclimate check (#13121) * Disable `Controversial/CamelCasePropertyName` codeclimate check * Fix indentation --- .codeclimate.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 614dd1ae2f..bbabf2810a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -13,8 +13,10 @@ engines: phpmd: enabled: true checks: - CleanCode/StaticAccess: - enabled: false + CleanCode/StaticAccess: + enabled: false + Controversial/CamelCasePropertyName: + enabled: false ratings: paths: - "**.js" From faea888652be0c71c7b02d8e4d1e94b8397650ee Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 2 Dec 2016 16:15:17 +0100 Subject: [PATCH 6/6] update codeclimate rules to match yii style follow up to #13121 --- .codeclimate.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index bbabf2810a..d8166d7a26 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,11 +12,30 @@ engines: enabled: true phpmd: enabled: true + # configure checks, see https://phpmd.org/rules/index.html for details checks: + # Static access on Yii::$app is normal in Yii CleanCode/StaticAccess: enabled: false + # Yii is a framework so if fulfills the job of encapsulating superglobals + Controversial/Superglobals: + enabled: false + # allow private properties to start with $_ Controversial/CamelCasePropertyName: - enabled: false + enabled: true + allow-underscore: true + # Short variable names are no problem in most cases, e.g. $n = count(...); + Naming/ShortVariable: + enabled: false + # Long variable names can help with better understanding so we increase the limit a bit + Naming/LongVariable: + enabled: true + maximum: 25 + # method names like up(), gc(), ... are okay. + Naming/ShortMethodName: + enabled: true + minimum: 2 + ratings: paths: - "**.js"