Commit 7939a3de by Carsten Brandt

Merge branch 'master' into array-attribute-relations

* master: Update output-data-widgets.md Fixes #4370: fileinfo extension is required for FileValidator. Fixes #4342 Fixes #3887 minor refactoring of FileValidator. Doc translate for rest-resources Better fix for Dropdown container options #4388: CORS filter fixes: Fixed test break. #4310: Removed `$data` from signature of `yii\rbac\ManagerInterface` Update .travis.yml Update ActiveRecordInterface.php Fixes #4384. Sphinx distributed indexes support provided typo fix [skip ci] Fixes #3611: Refactored query caching.
parents 5ad178a6 92e131c5
......@@ -31,7 +31,7 @@ install:
- tests/unit/data/travis/memcache-setup.sh
- tests/unit/data/travis/cubrid-setup.sh
# basic and advanced application:
- tests/unit/data/travis/setup-apps.sh
# - tests/unit/data/travis/setup-apps.sh
before_script:
- echo 'elasticsearch version ' && curl http://localhost:9200/
......@@ -40,16 +40,16 @@ before_script:
- tests/unit/data/travis/sphinx-setup.sh
- mongo yii2test --eval 'db.addUser("travis", "test");'
# basic and advanced application:
- tests/unit/data/travis/init-apps.sh
# - tests/unit/data/travis/init-apps.sh
script:
- vendor/bin/phpunit --verbose --coverage-clover=coverage.clover --exclude-group mssql,oci,wincache,xcache,zenddata
- cd apps/basic && php vendor/bin/codecept run
- cd ../advanced/backend && ../vendor/bin/codecept run
- cd ../common && ../vendor/bin/codecept run
- cd ../frontend && ../vendor/bin/codecept run
# - cd apps/basic && php vendor/bin/codecept run
# - cd ../advanced/backend && ../vendor/bin/codecept run
# - cd ../common && ../vendor/bin/codecept run
# - cd ../frontend && ../vendor/bin/codecept run
after_script:
- cd ../../..
# - cd ../../..
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover
Ресурсы
=========
RESTful API интерфейсы предназначены для доступа и управления *ресурсами*. Вы можете представлять ресурсы как
[модели](structure-models.md) в архитектуре [MVC](http://ru.wikipedia.org/wiki/Model-View-Controller).
Хотя в Yii нет никаких ограничений в том как представить ресурс, здесь вы можете представлять ресурсы
как объект наследующий свойства и методы [[yii\base\Model]] или дочерних классов (например [[yii\db\ActiveRecord]]), потому как:
* [[yii\base\Model]] реализует [[yii\base\Arrayable]] интерфейс, настраиваемый как вам удобно, для представления
данных через RESTful API интерфейс.
* [[yii\base\Model]] поддерживает [валидацию](input-validation.md), что полезно для RESTful API
реализующего ввод данных.
* [[yii\db\ActiveRecord]] реализует мощный функционал для работы с БД, будет полезным если данные ресурса хранятся в поддерживаемых БД.
В этом разделе, мы опишем как использовать методы наследуемые вашим класом ресурсов от [[yii\base\Model]] (или дочерних классов) необходимые RESTful API.
> Если класс ресурса не наследуется от [[yii\base\Model]], то будут возвращены все public поля.
## Поля <a name="fields"></a>
Когда ресурс включается в ответ RESTful API, необходимо представить(сеарилизовать) ресурс как строку.
Yii разбивает процесс сеарилизации на два шага. На первом шаге, ресурс конвертируется в массив [[yii\rest\Serializer]].
На втором шаге, массив сеарилизуется в строку в требуемом формате (JSON, XML) при помощи
[[yii\web\ResponseFormatterInterface|интерфейса для форматирования ответа]]. Это сделано для того чтобы при разработке вы могли сосредоточится на разработке класса ресурсов.
При переопределении методов [[yii\base\Model::fields()|fields()]] и/или [[yii\base\Model::extraFields()|extraFields()]],
вы можете указать какие данные будут отображаться при представлении в виде массива.
Разница между этими двумя методами в том, что первый определяет стандартный набор полей которые будут включены в представлении массивом, а второй
определяет дополнительные поля, которые могут быть включены в массив если запрос пользователя к ресурсу использует дополнительные параметры.
Например:
```
// вернёт все поля объявленные в fields()
http://localhost/users
// вернёт только поля id и email, если они объявлены в методе fields()
http://localhost/users?fields=id,email
// вернёт все поля обявленные в fields() и поле profile если оно указано в extraFields()
http://localhost/users?expand=profile
// вернёт только id, email и profile, если они объявлены в fields() и extraFields()
http://localhost/users?fields=id,email&expand=profile
```
### Переопределение `fields()` <a name="overriding-fields"></a>
По умолчанию, [[yii\base\Model::fields()]] вернёт все атрибуты модели как поля, пока что
[[yii\db\ActiveRecord::fields()]] возвращает только те атрибуты которые были объявлены в схеме БД.
Вы можете переопределить `fields()` при этом добавить, удалить, переименовать или переобъявить поля. Возвращаемое значение `fields()`
должно быть массивом. Ключи массива это имена полей, и значения массива могут быть именами свойств/атрибутов или анонимных функций, возвращающих соответсвующее значение полей.
Если имя атрибута такое же, как ключ массива вы можете не заполнять значение. Например:
```php
// явное перечисление всех атрибутов, лучше всего использовать когда вы хотите убедиться что изменение
// таблицы БД или атрибутов модели не повлияет на изменение полей в представлении для API (для поддержки обратной совместимости с API).
public function fields()
{
return [
// название поля совпадает с названием атрибута
'id',
// ключ массива "email", соответсвует значению атрибута "email_address"
'email' => 'email_address',
// ключ массива "name", это PHP callback функция возвращающая значение
'name' => function () {
return $this->first_name . ' ' . $this->last_name;
},
];
}
// Для фильтрации полей лучше всего использовать, поля наследуемые от родительского класса
// и blacklist для не безопасных полей.
public function fields()
{
$fields = parent::fields();
// удаляем не безопасные поля
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
```
> Внимание: По умолчанию все атрибуты модели будут включены в представление для API, вы должны
> убедиться что не безопасные данные, не попадут в представление. Если в модели есть не безопасные поля,
> вы должны переопределить метод `fields()` для их фильтрации. В приведённом примере
> мы удаляем из представления `auth_key`, `password_hash` и `password_reset_token`.
### Переопределение `extraFields()` <a name="overriding-extra-fields"></a>
По умолчанию, [[yii\base\Model::extraFields()]] ничего не возвращает, а [[yii\db\ActiveRecord::extraFields()]]
возвращает названия отношений объявленных в ДБ.
Формат вовзращаемызх данных `extraFields()` такой же как `fields()`. Как правило, `extraFields()`
используется для указания полей, значения которых являются объектами. Например учитывая следующее объявление полей
```php
public function fields()
{
return ['id', 'email'];
}
public function extraFields()
{
return ['profile'];
}
```
запрос `http://localhost/users?fields=id,email&expand=profile` может возвращать следующие JSON данные:
```php
[
{
"id": 100,
"email": "100@example.com",
"profile": {
"id": 100,
"age": 30,
}
},
...
]
```
## Связи <a name="links"></a>
[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS), аббревиатура для Hypermedia as the Engine of Application State,
необходим для того чтобы RESTful API, мог отобразить информацию которая позволяет клиентам просматривать возможности, поддерживаемые ресурсом. Ключ HATEOAS возвращает список ссылок с
информацией о параметрах доступных в методах API.
Ваши классы ресурсов могу поддерживать HATEOAS реализуя [[yii\web\Linkable]] интерфейс. Интерфейс
реализует один метод [[yii\web\Linkable::getLinks()|getLinks()]] который возвращает список [[yii\web\Link|links]].
Вы должны вернуть существующий URL на метод ресурса. Например:
```php
use yii\db\ActiveRecord;
use yii\web\Link;
use yii\web\Linkable;
use yii\helpers\Url;
class User extends ActiveRecord implements Linkable
{
public function getLinks()
{
return [
Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true),
];
}
}
```
При отправке ответа объект `User` будет содержать поле `_links` содержащий ссылки связанные с объектом `User`.
Например:
```
{
"id": 100,
"email": "user@example.com",
// ...
"_links" => [
"self": "https://example.com/users/100"
]
}
```
## Коллекции <a name="collections"></a>
Объекты ресурсов могут групироваться в *коллекции*. Каждая коллекция включает список объектов ресурсов одного типа.
Так как коллекции представляются в виде массива, их удобнее использовать как [проводник данных](output-data-providers.md).
Так как проводник данных поддерживает операции сортировки, разбиения на страницы это удобно использовать для RESTful API.
Например следующей метод возвращает проводник данных о почтовом ресурсе:
```php
namespace app\controllers;
use yii\rest\Controller;
use yii\data\ActiveDataProvider;
use app\models\Post;
class PostController extends Controller
{
public function actionIndex()
{
return new ActiveDataProvider([
'query' => Post::find(),
]);
}
}
```
При отправке ответа RESTful API, [[yii\rest\Serializer]] добавит текущую страницу ресурсов и сеарилизует все объекты ресурсов.
Кроме того, [[yii\rest\Serializer]] добавит HTTP заголовки содержащие информацию о нумерации страниц:
* `X-Pagination-Total-Count`: Количество ресурсов;
* `X-Pagination-Page-Count`: Количество страниц ресурсов;
* `X-Pagination-Current-Page`: Текущая страница (начинается с 1);
* `X-Pagination-Per-Page`: Количество ресурсов отображаемых на 1 странице;
* `Link`: Набор ссылок позволяющий клиенту пройти все ресурсы, страница за страницей.
Примеры вы можете найти в разделе [Быстрый старт](rest-quick-start.md#trying-it-out).
......@@ -245,20 +245,15 @@ Query caching requires a [[yii\db\Connection|DB connection]] and a valid `cache`
The basic usage of query caching is as follows, assuming `$db` is a [[yii\db\Connection]] instance:
```php
$duration = 60; // cache query results for 60 seconds.
$dependency = ...; // optional dependency
$db->beginCache($duration, $dependency);
$result = $db->cache(function ($db) {
// ...performs DB queries here...
// the result of the SQL query will be served from the cache
// if query caching is enabled and the query result is found in the cache
return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
$db->endCache();
});
```
As you can see, any SQL queries in between the `beginCache()` and `endCache()` calls will be cached.
If the result of the same query is found valid in the cache, the query will be skipped and the result
will be served from the cache instead.
Query caching can be used for [DAO](db-dao.md) as well as [ActiveRecord](db-active-record.md).
> Info: Some DBMS (e.g. [MySQL](http://dev.mysql.com/doc/refman/5.1/en/query-cache.html))
......@@ -269,13 +264,85 @@ Query caching can be used for [DAO](db-dao.md) as well as [ActiveRecord](db-acti
### Configurations <a name="query-caching-configs"></a>
Query caching has two configurable options through [[yii\db\Connection]]:
Query caching has three global configurable options through [[yii\db\Connection]]:
* [[yii\db\Connection::enableQueryCache|enableQueryCache]]: whether to turn on or off query caching.
It defaults to true. Note that to effectively turn on query caching, you also need to have a valid
cache, as specified by [[yii\db\Connection::queryCache|queryCache]].
* [[yii\db\Connection::queryCacheDuration|queryCacheDuration]]: this represents the number of seconds
that a query result can remain valid in the cache. The duration will be overwritten if you call
[[yii\db\Connection::beginCache()]] with an explicit duration parameter.
that a query result can remain valid in the cache. You can use 0 to indicate a query result should
remain in the cache forever. This property is the default value used when [[yii\db\Connection::cache()]]
is called without specifying a duration.
* [[yii\db\Connection::queryCache|queryCache]]: this represents the ID of the cache application component.
It defaults to `'cache'`. Query caching is enabled only when there is a valid cache application component.
It defaults to `'cache'`. Query caching is enabled only if there is a valid cache application component.
### Usages <a name="query-caching-usages"></a>
You can use [[yii\db\Connection::cache()]] if you have multiple SQL queries that need to take advantage of
query caching. The usage is as follows,
```php
$duration = 60; // cache query results for 60 seconds.
$dependency = ...; // optional dependency
$result = $db->cache(function ($db) {
// ... perform SQL queries here ...
return $result;
}, $duration, $dependency);
```
Any SQL queries in the anonymous function will be cached for the specified duration with the specified dependency.
If the result of a query is found valid in the cache, the query will be skipped and the result will be served
from the cache instead. If you do not specify the `$duration` parameter, the value of
[[yii\db\Connection::queryCacheDuration|queryCacheDuration]] will be used instead.
Sometimes within `cache()`, you may want to disable query caching for some particular queries. You can use
[[yii\db\Connection::noCache()]] in this case.
```php
$result = $db->cache(function ($db) {
// SQL queries that use query caching
$db->noCache(function ($db) {
// SQL queries that do not use query caching
});
// ...
return $result;
});
```
If you just want to use query caching for a single query, you can call [[yii\db\Command::cache()]] when building
the command. For example,
```php
// use query caching and set query cache duration to be 60 seconds
$customer = $db->createCommand('SELECT * FROM customer WHERE id=1')->cache(60)->queryOne();
```
You can also use [[yii\db\Command::noCache()]] to disable query caching for a single command. For example,
```php
$result = $db->cache(function ($db) {
// SQL queries that use query caching
// do not use query caching for this command
$customer = $db->createCommand('SELECT * FROM customer WHERE id=1')->noCache()->queryOne();
// ...
return $result;
});
```
### Limitations <a name="query-caching-limitations"></a>
......
......@@ -381,6 +381,7 @@ $query->andFilterWhere(['LIKE', 'author.name', $this->getAttribute('author.name'
> ```
>
> Same is true for the sorting definition:
>
> ```php
> $dataProvider->sort->attributes['author.name'] = [
> 'asc' => ['au.name' => SORT_ASC],
......
......@@ -346,10 +346,10 @@ The Cors filtering could be tuned using the `cors` property.
* `cors['Origin']`: array used to define allowed origins. Can be `['*']` (everyone) or `['http://www.myserver.net', 'http://www.myotherserver.com']`. Default to `['*']`.
* `cors['Access-Control-Request-Method']`: array of allowed verbs like `['GET', 'OPTIONS', 'HEAD']`. Default to `['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']`.
* `cors['Access-Control-Request-Headers']`: array of allowed headers. Can be `['*']` all headers or specific ones `['X-Request-With']`. Default to `['*']`.
* `cors['Access-Control-Allow-Credentials']`: define if current request can be made using credentials. Can be `true`, `false`. Default to `true`.
* `cors['Access-Control-Allow-Credentials']`: define if current request can be made using credentials. Can be `true`, `false` or `null` (not set). Default to `null`.
* `cors['Access-Control-Max-Age']`: define lifetime of pre-flight request. Default to `86400`.
For example, allowing CORS for origin : `http://www.myserver.net` with method `GET`, `HEAD` and `OPTIONS` and do not send `Access-Control-Allow-Credentials` header :
For example, allowing CORS for origin : `http://www.myserver.net` with method `GET`, `HEAD` and `OPTIONS` :
```php
use yii\filters\Cors;
......@@ -363,7 +363,6 @@ public function behaviors()
'cors' => [
'Origin' => ['http://www.myserver.net'],
'Access-Control-Request-Method' => ['GET', 'HEAD', 'OPTIONS'],
'Access-Control-Allow-Credentials' => null,
],
],
], parent::behaviors());
......@@ -385,7 +384,6 @@ public function behaviors()
'cors' => [
'Origin' => ['http://www.myserver.net'],
'Access-Control-Request-Method' => ['GET', 'HEAD', 'OPTIONS'],
'Access-Control-Allow-Credentials' => null,
],
'actions' => [
'login' => [
......
......@@ -40,7 +40,12 @@ class Dropdown extends Widget
*/
public $encodeLabels = true;
/**
* @var array the HTML attributes for the widget container tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
protected $_containerOptions = [];
/**
* Initializes the widget.
* If you override this method, make sure you call the parent implementation first.
......@@ -49,6 +54,7 @@ class Dropdown extends Widget
{
parent::init();
Html::addCssClass($this->options, 'dropdown-menu');
$this->_containerOptions = $this->options;
}
/**
......@@ -56,17 +62,16 @@ class Dropdown extends Widget
*/
public function run()
{
echo $this->renderItems($this->items, $this->options);
echo $this->renderItems($this->items);
}
/**
* Renders menu items.
* @param array $items the menu items to be rendered
* @param array $containerOptions the HTML attributes for the widget container tag
* @return string the rendering result.
* @throws InvalidConfigException if the label option is not specified in one of the items.
*/
protected function renderItems($items, $containerOptions)
protected function renderItems($items)
{
$lines = [];
foreach ($items as $i => $item) {
......@@ -88,13 +93,13 @@ class Dropdown extends Widget
$linkOptions['tabindex'] = '-1';
$content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions);
if (!empty($item['items'])) {
unset($containerOptions['id']);
$this->renderItems($item['items'], $containerOptions);
unset($this->_containerOptions['id']);
$this->renderItems($item['items']);
Html::addCssClass($options, 'dropdown-submenu');
}
$lines[] = Html::tag('li', $content, $options);
}
return Html::tag('ul', implode("\n", $lines), $this->options);
return Html::tag('ul', implode("\n", $lines), $this->_containerOptions);
}
}
......@@ -6,6 +6,7 @@ Yii Framework 2 sphinx extension Change Log
- Bug #3668: Escaping of the special characters at 'MATCH' statement added (klimov-paul)
- Bug #4018: AR relation eager loading does not work with db models (klimov-paul)
- Bug #4375: Distributed indexes support provided (klimov-paul)
- Enh #3520: Added `unlinkAll()`-method to active record to remove all records of a model relation (NmDimas, samdark, cebe)
- Enh #4048: Added `init` event to `ActiveQuery` classes (qiangxue)
- Enh #4086: changedAttributes of afterSave Event now contain old values (dizews)
......
......@@ -459,12 +459,19 @@ class Schema extends Object
}
throw $e;
}
foreach ($columns as $info) {
$column = $this->loadColumnSchema($info);
$index->columns[$column->name] = $column;
if ($column->isPrimaryKey) {
$index->primaryKey = $column->name;
if (empty($columns[0]['Agent'])) {
foreach ($columns as $info) {
$column = $this->loadColumnSchema($info);
$index->columns[$column->name] = $column;
if ($column->isPrimaryKey) {
$index->primaryKey = $column->name;
}
}
} else {
// Distributed index :
$agent = $this->getIndexSchema($columns[0]['Agent']);
$index->columns = $agent->columns;
}
return true;
......
......@@ -68,6 +68,7 @@ Yii Framework 2 Change Log
- Bug #4162: Fixed bug where schema name was not used in ’SHOW CREATE TABLE’ query in `yii\db\mysql\Schema` (stevekr)
- Bug #4241: `yii\widgets\Pjax` was incorrectly setting container id (mitalcoi)
- Bug #4276: Added check for UPLOAD_ERR_NO_FILE in `yii\web\UploadedFile` and return null if no file was uploaded (OmgDef)
- Bug #4342: mssql (dblib) driver does not support getting attributes (tof06)
- Bug: Fixed inconsistent return of `\yii\console\Application::runAction()` (samdark)
- Bug: URL encoding for the route parameter added to `\yii\web\UrlManager` (klimov-paul)
- Bug: Fixed the bug that requesting protected or private action methods would cause 500 error instead of 404 (qiangxue)
......@@ -168,6 +169,11 @@ Yii Framework 2 Change Log
- Chg #3383: Added `$type` parameter to `IdentityInterface::findIdentityByAccessToken()` (qiangxue)
- Chg #3531: \yii\grid\GridView now allows any character (except ":") in the attribute part of the shorthand syntax for columns (rawtaz)
- Chg #3544: Added `$key` as a parameter to the callable specified via `yii\grid\DataColumn::value` (mdmunir)
- Chg #3611: Query caching is refactored. (qiangxue)
- `yii\db\Connection::beginCache()` and `endCache()` are removed.
- Added `yii\db\Connection::cache()` and `noCache()`.
- Added `Command::cache()` and `noCache()`.
- `yii\db\Connection::queryCacheDuration` is now used as a default cache duration parameter.
- Chg #3640: All cookies are now httpOnly by default in order to increase overall security (samdark)
- Chg #3687: Default `sourceLanguage` and `language` are now `en-US` in order for i18n formatter to work correctly (samdark)
- Chg #3804: Added `fileinfo` PHP extension to the basic requirement of Yii (Ragazzo)
......@@ -181,6 +187,7 @@ Yii Framework 2 Change Log
- Chg #4147: `BaseMailer::compose()` will not overwrite the `message` parameter if it is explicitly provided (qiangxue)
- Chg #4201: change default value of `SyslogTarget::facility` from LOG_SYSLOG to LOG_USER (dizews)
- Chg #4227: `\yii\widgets\LinkPager::$hideOnSinglePage` is now `true` by default (samdark)
- Chg #4310: Removed `$data` from signature of `yii\rbac\ManagerInterface` (samdark)
- Chg #4318: `yii\helpers\Html::ul()` and `ol()` will return an empty list tag if an empty item array is given (qiangxue)
- Chg #4331: `yii\helpers\Url` now uses `UrlManager` to determine base URL when generating URLs (qiangxue)
- Chg: Replaced `clearAll()` and `clearAllAssignments()` in `yii\rbac\ManagerInterface` with `removeAll()`, `removeAllRoles()`, `removeAllPermissions()`, `removeAllRules()` and `removeAllAssignments()` (qiangxue)
......
......@@ -161,3 +161,14 @@ new ones save the following code as `convert.php` that should be placed in the s
// ...
];
```
* If you are using query caching, you should modify your relevant code as follows, as `beginCache()` and `endCache()` are
replaced by `cache()`:
```php
$db->cache(function ($db) {
// ... SQL queries that need to use query caching
}, $duration, $dependency);
```
......@@ -146,7 +146,7 @@ interface ActiveRecordInterface
* // SELECT FROM customer WHERE age>30
* $customers = Customer::find()->where('age>30')->all();
*
* @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
* @return static|ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
*/
public static function find();
......
......@@ -8,8 +8,8 @@
namespace yii\db;
use Yii;
use yii\base\Component;
use yii\base\NotSupportedException;
use yii\caching\Cache;
/**
* Command represents a SQL statement to be executed against a database.
......@@ -51,7 +51,7 @@ use yii\caching\Cache;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Command extends \yii\base\Component
class Command extends Component
{
/**
* @var Connection the DB connection that this command is associated with
......@@ -73,6 +73,19 @@ class Command extends \yii\base\Component
*/
public $params = [];
/**
* @var integer the default number of seconds that query results can remain valid in cache.
* Use 0 to indicate that the cached data will never expire. And use a negative number to indicate
* query cache should not be used.
* @see cache()
*/
public $queryCacheDuration;
/**
* @var \yii\caching\Dependency the dependency to be associated with the cached query result for this command
* @see cache()
*/
public $queryCacheDependency;
/**
* @var array pending parameters to be bound to the current PDO statement.
*/
private $_pendingParams = [];
......@@ -81,6 +94,32 @@ class Command extends \yii\base\Component
*/
private $_sql;
/**
* Enables query cache for this command.
* @param integer $duration the number of seconds that query result of this command can remain valid in the cache.
* If this is not set, the value of [[Connection::queryCacheDuration]] will be used instead.
* Use 0 to indicate that the cached data will never expire.
* @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query result.
* @return static the command object itself
*/
public function cache($duration = null, $dependency = null)
{
$this->queryCacheDuration = $duration === null ? $this->db->queryCacheDuration : $duration;
$this->queryCacheDependency = $dependency;
return $this;
}
/**
* Disables query cache for this command.
* @return static the command object itself
*/
public function noCache()
{
$this->queryCacheDuration = -1;
return $this;
}
/**
* Returns the SQL statement for this command.
* @return string the SQL statement to be executed
......@@ -155,6 +194,7 @@ class Command extends \yii\base\Component
public function prepare($forRead = null)
{
if ($this->pdoStatement) {
$this->bindPendingParams();
return;
}
......@@ -172,6 +212,7 @@ class Command extends \yii\base\Component
try {
$this->pdoStatement = $pdo->prepare($sql);
$this->bindPendingParams();
} catch (\Exception $e) {
$message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
$errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
......@@ -205,8 +246,6 @@ class Command extends \yii\base\Component
{
$this->prepare();
$this->bindPendingParams();
if ($dataType === null) {
$dataType = $this->db->getSchema()->getPdoType($value);
}
......@@ -288,44 +327,6 @@ class Command extends \yii\base\Component
}
/**
* Executes the SQL statement.
* This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
* No result set will be returned.
* @return integer number of rows affected by the execution.
* @throws Exception execution failed
*/
public function execute()
{
$sql = $this->getSql();
$rawSql = $this->getRawSql();
Yii::info($rawSql, __METHOD__);
if ($sql == '') {
return 0;
}
$this->prepare(false);
$this->bindPendingParams();
$token = $rawSql;
try {
Yii::beginProfile($token, __METHOD__);
$this->pdoStatement->execute();
$n = $this->pdoStatement->rowCount();
Yii::endProfile($token, __METHOD__);
return $n;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw $this->db->getSchema()->convertException($e, $rawSql);
}
}
/**
* Executes the SQL statement and returns query result.
* This method is for executing a SQL query that returns result set, such as `SELECT`.
* @return DataReader the reader object for fetching the query result
......@@ -393,74 +394,6 @@ class Command extends \yii\base\Component
}
/**
* Performs the actual DB query of a SQL statement.
* @param string $method method of PDOStatement to be called
* @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php)
* for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used.
* @return mixed the method execution result
* @throws Exception if the query causes any problem
*/
private function queryInternal($method, $fetchMode = null)
{
$db = $this->db;
$rawSql = $this->getRawSql();
Yii::info($rawSql, 'yii\db\Command::query');
/* @var $cache \yii\caching\Cache */
if ($db->enableQueryCache && $method !== '') {
$cache = is_string($db->queryCache) ? Yii::$app->get($db->queryCache, false) : $db->queryCache;
}
if (isset($cache) && $cache instanceof Cache) {
$cacheKey = [
__CLASS__,
$method,
$db->dsn,
$db->username,
$rawSql,
];
if (($result = $cache->get($cacheKey)) !== false) {
Yii::trace('Query result served from cache', 'yii\db\Command::query');
return $result;
}
}
$this->prepare(true);
$this->bindPendingParams();
$token = $rawSql;
try {
Yii::beginProfile($token, 'yii\db\Command::query');
$this->pdoStatement->execute();
if ($method === '') {
$result = new DataReader($this);
} else {
if ($fetchMode === null) {
$fetchMode = $this->fetchMode;
}
$result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
$this->pdoStatement->closeCursor();
}
Yii::endProfile($token, 'yii\db\Command::query');
if (isset($cache, $cacheKey) && $cache instanceof Cache) {
$cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency);
Yii::trace('Saved query result in cache', 'yii\db\Command::query');
}
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, 'yii\db\Command::query');
throw $this->db->getSchema()->convertException($e, $rawSql);
}
}
/**
* Creates an INSERT command.
* For example,
*
......@@ -806,4 +739,127 @@ class Command extends \yii\base\Component
return $this->setSql($sql);
}
/**
* Executes the SQL statement.
* This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
* No result set will be returned.
* @return integer number of rows affected by the execution.
* @throws Exception execution failed
*/
public function execute()
{
$sql = $this->getSql();
$rawSql = $this->getRawSql();
Yii::info($rawSql, __METHOD__);
if ($sql == '') {
return 0;
}
$this->prepare(false);
$token = $rawSql;
try {
Yii::beginProfile($token, __METHOD__);
$this->pdoStatement->execute();
$n = $this->pdoStatement->rowCount();
Yii::endProfile($token, __METHOD__);
return $n;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw $this->db->getSchema()->convertException($e, $rawSql);
}
}
/**
* Returns the effective query cache information.
* @return array the current query cache information, or null if query cache is not used.
*/
private function getQueryCacheInfo()
{
$info = $this->db->getQueryCacheInfo();
if (is_array($info)) {
if ($this->queryCacheDuration !== null) {
$info[1] = $this->queryCacheDuration;
}
if ($this->queryCacheDependency !== null) {
$info[2] = $this->queryCacheDependency;
}
if ($info[1] !== null && $info[1] >= 0) {
return $info;
}
}
return null;
}
/**
* Performs the actual DB query of a SQL statement.
* @param string $method method of PDOStatement to be called
* @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php)
* for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used.
* @return mixed the method execution result
* @throws Exception if the query causes any problem
*/
private function queryInternal($method, $fetchMode = null)
{
$rawSql = $this->getRawSql();
Yii::info($rawSql, 'yii\db\Command::query');
if ($method !== '') {
$info = $this->getQueryCacheInfo();
if (is_array($info)) {
/* @var $cache \yii\caching\Cache */
$cache = $info[0];
$cacheKey = [
__CLASS__,
$method,
$this->db->dsn,
$this->db->username,
$rawSql,
];
if (($result = $cache->get($cacheKey)) !== false) {
Yii::trace('Query result served from cache', 'yii\db\Command::query');
return $result;
}
}
}
$this->prepare(true);
$token = $rawSql;
try {
Yii::beginProfile($token, 'yii\db\Command::query');
$this->pdoStatement->execute();
if ($method === '') {
$result = new DataReader($this);
} else {
if ($fetchMode === null) {
$fetchMode = $this->fetchMode;
}
$result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
$this->pdoStatement->closeCursor();
}
Yii::endProfile($token, 'yii\db\Command::query');
} catch (\Exception $e) {
Yii::endProfile($token, 'yii\db\Command::query');
throw $this->db->getSchema()->convertException($e, $rawSql);
}
if (isset($cache, $cacheKey, $info)) {
$cache->set($cacheKey, $result, $info[1], $info[2]);
Yii::trace('Saved query result in cache', 'yii\db\Command::query');
}
return $result;
}
}
......@@ -204,30 +204,22 @@ class Connection extends Component
* @var boolean whether to enable query caching.
* Note that in order to enable query caching, a valid cache component as specified
* by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true.
*
* Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on
* and off query caching on the fly.
* @see queryCacheDuration
* Also, only the results of the queries enclosed within [[cache()]] will be cached.
* @see queryCache
* @see queryCacheDependency
* @see beginCache()
* @see endCache()
* @see cache()
* @see noCache()
*/
public $enableQueryCache = false;
public $enableQueryCache = true;
/**
* @var integer number of seconds that query results can remain valid in cache.
* Defaults to 3600, meaning 3600 seconds, or one hour.
* @var integer the default number of seconds that query results can remain valid in cache.
* Use 0 to indicate that the cached data will never expire.
* Defaults to 3600, meaning 3600 seconds, or one hour. Use 0 to indicate that the cached data will never expire.
* The value of this property will be used when [[cache()]] is called without a cache duration.
* @see enableQueryCache
* @see cache()
*/
public $queryCacheDuration = 3600;
/**
* @var \yii\caching\Dependency the dependency that will be used when saving query results into cache.
* Defaults to null, meaning no dependency.
* @see enableQueryCache
*/
public $queryCacheDependency;
/**
* @var Cache|string the cache object or the ID of the cache application component
* that is used for query caching.
* @see enableQueryCache
......@@ -369,6 +361,10 @@ class Connection extends Component
* @var Connection the currently active slave connection
*/
private $_slave = false;
/**
* @var array query cache parameters for the [[cache()]] calls
*/
private $_queryCacheInfo = [];
/**
......@@ -381,29 +377,104 @@ class Connection extends Component
}
/**
* Turns on query caching.
* This method is provided as a shortcut to setting two properties that are related
* with query caching: [[queryCacheDuration]] and [[queryCacheDependency]].
* @param integer $duration the number of seconds that query results may remain valid in cache.
* If not set, it will use the value of [[queryCacheDuration]]. See [[queryCacheDuration]] for more details.
* @param \yii\caching\Dependency $dependency the dependency for the cached query result.
* See [[queryCacheDependency]] for more details.
* Uses query cache for the queries performed with the callable.
* When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
* queries performed within the callable will be cached and their results will be fetched from cache if available.
* For example,
*
* ```php
* // The customer will be fetched from cache if available.
* // If not, the query will be made against DB and cached for use next time.
* $customer = $db->cache(function (Connection $db) {
* return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
* });
* ```
*
* Note that query cache is only meaningful for queries that return results. For queries performed with
* [[Command::execute()]], query cache will not be used.
*
* @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
* The signature of the callable is `function (Connection $db)`.
* @param integer $duration the number of seconds that query results can remain valid in the cache. If this is
* not set, the value of [[queryCacheDuration]] will be used instead.
* Use 0 to indicate that the cached data will never expire.
* @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query results.
* @return mixed the return result of the callable
* @throws \Exception if there is any exception during query
* @see enableQueryCache
* @see queryCache
* @see noCache()
*/
public function cache(callable $callable, $duration = null, $dependency = null)
{
$this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
try {
$result = call_user_func($callable, $this);
array_pop($this->_queryCacheInfo);
return $result;
} catch (\Exception $e) {
array_pop($this->_queryCacheInfo);
throw $e;
}
}
/**
* Disables query cache temporarily.
* Queries performed within the callable will not use query cache at all. For example,
*
* ```php
* $db->cache(function (Connection $db) {
*
* // ... queries that use query cache ...
*
* return $db->noCache(function (Connection $db) {
* // this query will not use query cache
* return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
* });
* });
* ```
*
* @param callable $callable a PHP callable that contains DB queries which should not use query cache.
* The signature of the callable is `function (Connection $db)`.
* @return mixed the return result of the callable
* @throws \Exception if there is any exception during query
* @see enableQueryCache
* @see queryCache
* @see cache()
*/
public function beginCache($duration = null, $dependency = null)
public function noCache(callable $callable)
{
$this->enableQueryCache = true;
if ($duration !== null) {
$this->queryCacheDuration = $duration;
$this->_queryCacheInfo[] = false;
try {
$result = call_user_func($callable, $this);
array_pop($this->_queryCacheInfo);
return $result;
} catch (\Exception $e) {
array_pop($this->_queryCacheInfo);
throw $e;
}
$this->queryCacheDependency = $dependency;
}
/**
* Turns off query caching.
* Returns the current query cache information.
* This method is used internally by [[Command]].
* @return array the current query cache information, or null if query cache is not enabled.
* @internal
*/
public function endCache()
public function getQueryCacheInfo()
{
$this->enableQueryCache = false;
$info = end($this->_queryCacheInfo);
if ($this->enableQueryCache) {
if (is_string($this->queryCache) && Yii::$app) {
$cache = Yii::$app->get($this->queryCache, false);
} else {
$cache = $this->queryCache;
}
if ($cache instanceof Cache) {
return is_array($info) ? [$cache, $info[0], $info[1]] : [$cache, null, null];
}
}
return null;
}
/**
......
......@@ -61,4 +61,23 @@ class PDO extends \PDO
return true;
}
/**
* Retrieve a database connection attribute.
* It is necessary to override PDO's method as some MSSQL PDO driver (e.g. dblib) does not
* support getting attributes
*/
public function getAttribute($attribute)
{
try {
return parent::getAttribute($attribute);
} catch (\PDOException $e) {
switch ($attribute) {
case PDO::ATTR_SERVER_VERSION:
return $this->query("SELECT SERVERPROPERTY('productversion')")->fetchColumn();
default:
throw $e;
}
}
}
}
......@@ -17,6 +17,8 @@ use yii\base\InvalidParamException;
*/
class QueryBuilder extends \yii\db\QueryBuilder
{
protected $_oldMssql;
/**
* @var array mapping from abstract column types (keys) to physical column types (values).
*/
......@@ -189,8 +191,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
$originalOrdering = $this->buildOrderBy($query->orderBy);
if ($query->select) {
$select = implode(', ', $query->select);
}
else {
} else {
$select = $query->select = '*';
}
if ($select === '*') {
......@@ -238,8 +239,11 @@ class QueryBuilder extends \yii\db\QueryBuilder
*/
protected function isOldMssql()
{
$pdo = $this->db->getSlavePdo();
$version = preg_split("/\./", $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION));
return $version[0] < 11;
if ($this->_oldMssql === null) {
$pdo = $this->db->getSlavePdo();
$version = preg_split("/\./", $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION));
$this->_oldMssql = $version[0] < 11;
}
return $this->_oldMssql;
}
}
......@@ -79,7 +79,7 @@ class Cors extends ActionFilter
'Origin' => ['*'],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Allow-Credentials' => null,
'Access-Control-Max-Age' => 86400,
];
......@@ -151,14 +151,19 @@ class Cors extends ActionFilter
}
}
$this->prepareAllowHeaders('Method', $requestHeaders, $responseHeaders);
$this->prepareAllowHeaders('Headers', $requestHeaders, $responseHeaders);
if (isset($requestHeaders['Access-Control-Request-Method'])) {
$responseHeaders['Access-Control-Allow-Methods'] = implode(', ', $this->cors['Access-Control-Request-Method']);
}
if ($this->cors['Access-Control-Allow-Credentials'] === true) {
$responseHeaders['Access-Control-Allow-Credentials'] = 'true';
} elseif ($this->cors['Access-Control-Allow-Credentials'] === false) {
$responseHeaders['Access-Control-Allow-Credentials'] = 'false';
}
if (($_SERVER['REQUEST_METHOD'] === 'OPTIONS') && ($this->cors['Access-Control-Max-Age'] !== null)) {
$responseHeaders['Access-Control-Max-Age'] = $this->cors['Access-Control-Max-Age'];
}
......@@ -178,20 +183,12 @@ class Cors extends ActionFilter
$responseHeaderField = 'Access-Control-Allow-'.$type;
if (isset($requestHeaders[$requestHeaderField])) {
if (in_array('*', $this->cors[$requestHeaderField])) {
if ($type === 'Method') {
$responseHeaders[$responseHeaderField] = strtoupper($requestHeaders[$requestHeaderField]);
} elseif ($type === 'Headers') {
$responseHeaders[$responseHeaderField] = $this->headerize($requestHeaders[$requestHeaderField]);
}
$responseHeaders[$responseHeaderField] = $this->headerize($requestHeaders[$requestHeaderField]);
} else {
$requestedData = preg_split("/[\s,]+/", $requestHeaders[$requestHeaderField], -1, PREG_SPLIT_NO_EMPTY);
$acceptedData = [];
foreach ($requestedData as $req) {
if ($type === 'Method') {
$req = strtoupper($req);
} elseif ($type === 'Headers') {
$req = $this->headerize($req);
}
$req = $this->headerize($req);
if (in_array($req, $this->cors[$requestHeaderField])) {
$acceptedData[] = $req;
}
......
......@@ -618,7 +618,7 @@ class BaseHtml
* - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass
* in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks.
* When this option is specified, the radio button will be enclosed by a label tag.
* - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified.
* - labelOptions: array, the HTML attributes for the label tag. Do not set this option unless you set the "label" option.
* - container: array|boolean, the HTML attributes for the container tag. This is only used when the "label" option is specified.
* If it is false, no container will be rendered. If it is an array or not, a "div" container will be rendered
* around the the radio button.
......@@ -668,7 +668,7 @@ class BaseHtml
* - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass
* in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks.
* When this option is specified, the checkbox will be enclosed by a label tag.
* - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified.
* - labelOptions: array, the HTML attributes for the label tag. Do not set this option unless you set the "label" option.
* - container: array|boolean, the HTML attributes for the container tag. This is only used when the "label" option is specified.
* If it is false, no container will be rendered. If it is an array or not, a "div" container will be rendered
* around the the radio button.
......
......@@ -595,7 +595,7 @@ class DbManager extends BaseManager
/**
* @inheritdoc
*/
public function assign($role, $userId, $rule = null, $data = null)
public function assign($role, $userId, $rule = null)
{
$assignment = new Assignment([
'userId' => $userId,
......
......@@ -167,11 +167,10 @@ interface ManagerInterface
* @param string|integer $userId the user ID (see [[\yii\web\User::id]])
* @param Rule $rule the rule to be associated with this assignment. If not null, the rule
* will be executed when [[allow()]] is called to check the user permission.
* @param mixed $data additional data associated with this assignment.
* @return Assignment the role assignment information.
* @throws \Exception if the role has already been assigned to the user
*/
public function assign($role, $userId, $rule = null, $data = null);
public function assign($role, $userId, $rule = null);
/**
* Revokes a role from a user.
......
......@@ -223,7 +223,7 @@ class PhpManager extends BaseManager
/**
* @inheritdoc
*/
public function assign($role, $userId, $ruleName = null, $data = null)
public function assign($role, $userId, $ruleName = null)
{
if (!isset($this->items[$role->name])) {
throw new InvalidParamException("Unknown role '{$role->name}'.");
......
......@@ -14,6 +14,8 @@ use yii\helpers\FileHelper;
/**
* FileValidator verifies if an attribute is receiving a valid uploaded file.
*
* Note that you should enable `fileinfo` PHP extension.
*
* @property integer $sizeLimit The size limit for uploaded files. This property is read-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
......@@ -213,7 +215,7 @@ class FileValidator extends Validator
return [$this->tooSmall, ['file' => $file->name, 'limit' => $this->minSize]];
} elseif (!empty($this->extensions) && !$this->validateExtension($file)) {
return [$this->wrongExtension, ['file' => $file->name, 'extensions' => implode(', ', $this->extensions)]];
} elseif (!empty($this->mimeTypes) && !in_array(FileHelper::getMimeType($file->tempName), $this->mimeTypes, true)) {
} elseif (!empty($this->mimeTypes) && !in_array(FileHelper::getMimeType($file->tempName), $this->mimeTypes, false)) {
return [$this->wrongMimeType, ['file' => $file->name, 'mimeTypes' => implode(', ', $this->mimeTypes)]];
} else {
return null;
......@@ -268,7 +270,7 @@ class FileValidator extends Validator
*/
public function isEmpty($value, $trim = false)
{
$value = is_array($value) && !empty($value) ? $value[0] : $value;
$value = is_array($value) ? reset($value) : $value;
return !($value instanceof UploadedFile) || $value->error == UPLOAD_ERR_NO_FILE;
}
......@@ -306,7 +308,7 @@ class FileValidator extends Validator
if ($this->checkExtensionByMimeType) {
$mimeType = FileHelper::getMimeType($file->tempName);
$mimeType = FileHelper::getMimeType($file->tempName, null, false);
if ($mimeType === null) {
return false;
}
......
......@@ -101,6 +101,13 @@ index yii2_test_rt_index
}
index yii2_test_distributed
{
type = distributed
local = yii2_test_article_index
}
indexer
{
mem_limit = 32M
......
......@@ -324,4 +324,27 @@ class QueryTest extends SphinxTestCase
->all($connection);
$this->assertNotEmpty($rows);
}
/**
* @depends testRun
*
* @see https://github.com/yiisoft/yii2/issues/4375
*/
public function testRunOnDistributedIndex()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('yii2_test_distributed')
->match('about')
->options([
'cutoff' => 50,
'field_weights' => [
'title' => 10,
'content' => 3,
],
])
->all($connection);
$this->assertNotEmpty($rows);
}
}
......@@ -2,6 +2,8 @@
namespace yiiunit\framework\db;
use yii\caching\FileCache;
use yii\db\Connection;
use yii\db\DataReader;
/**
......@@ -240,6 +242,7 @@ class CommandTest extends DatabaseTestCase
$this->assertEquals(2, $command->execute());
}
/*
public function testInsert()
{
}
......@@ -299,6 +302,7 @@ class CommandTest extends DatabaseTestCase
public function testDropIndex()
{
}
*/
public function testIntegrityViolation()
{
......@@ -311,4 +315,49 @@ class CommandTest extends DatabaseTestCase
$command->execute();
$command->execute();
}
public function testQueryCache()
{
$db = $this->getConnection();
$db->enableQueryCache = true;
$db->queryCache = new FileCache(['cachePath' => '@yiiunit/runtime/cache']);
$command = $db->createCommand('SELECT name FROM customer WHERE id=:id');
$this->assertEquals('user1', $command->bindValue(':id', 1)->queryScalar());
$update = $db->createCommand('UPDATE customer SET name=:name WHERE id=:id');
$update->bindValues([':id' => 1, ':name' => 'user11'])->execute();
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$db->cache(function (Connection $db) use ($command, $update) {
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
$update->bindValues([':id' => 2, ':name' => 'user22'])->execute();
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
$db->noCache(function () use ($command) {
$this->assertEquals('user22', $command->bindValue(':id', 2)->queryScalar());
});
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
}, 10);
$db->enableQueryCache = false;
$db->cache(function ($db) use ($command, $update) {
$this->assertEquals('user22', $command->bindValue(':id', 2)->queryScalar());
$update->bindValues([':id' => 2, ':name' => 'user2'])->execute();
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
}, 10);
$db->enableQueryCache = true;
$command = $db->createCommand('SELECT name FROM customer WHERE id=:id')->cache();
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$update->bindValues([':id' => 1, ':name' => 'user1'])->execute();
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$this->assertEquals('user1', $command->noCache()->bindValue(':id', 1)->queryScalar());
$command = $db->createCommand('SELECT name FROM customer WHERE id=:id');
$db->cache(function (Connection $db) use ($command, $update) {
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$this->assertEquals('user1', $command->noCache()->bindValue(':id', 1)->queryScalar());
}, 10);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment