Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Phan Xuan Dung
2019-03-13 10:08:43 +07:00
11 changed files with 180 additions and 26 deletions

View File

@ -521,7 +521,25 @@ $customer->loadDefaultValues();
> Совет: вы можете использовать поведение [[yii\behaviors\AttributeTypecastBehavior]] для того, чтобы производить
приведение типов для ActiveRecord во время валидации или сохранения.
Начиная с 2.0.14, Yii ActiveRecord поддерживает сложные типы данных, такие как JSON или многомерные массивы.
#### JSON в MySQL и PostgreSQL
После заполнения данных, значение из столбца JSON будет автоматически декодировано из JSON в соответствии со стандартными правилами декодирования JSON.
Чтобы сохранить значение атрибута в столбец JSON, ActiveRecord автоматически создаст объект [[yii\db\JsonExpression|JsonExpression]], который будет закодирован в строку JSON на уровне [QueryBuilder](db-query-builder.md).
#### Массивы в PostgreSQL
После заполнения данных значение из столбца `Array` будет автоматически декодировано из нотации PgSQL в объект [[yii\db\ArrayExpression|ArrayExpression]]. Он реализует интерфейс PHP `ArrayAccess`, так что вы можете использовать его в качестве массива, или вызвать `->getValue ()`, чтобы получить сам массив.
Чтобы сохранить значение атрибута в столбец массива, ActiveRecord автоматически создаст объект [[yii\db\Array Expression|ArrayExpression]], который будет закодирован [QueryBuilder](db-query-builder.md) в строковое представление массива PgSQL.
Можно также использовать условия для столбцов JSON:
```php
$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])
```
Дополнительные сведения о системе построения выражений см. [Query Builder – добавление пользовательских условий и выражений](db-query-builder.md#adding-custom-conditions-and-expressions)
### Обновление нескольких строк данных <span id="updating-multiple-rows"></span>

View File

@ -4,6 +4,9 @@ Yii Framework 2 Change Log
2.0.17 under development
------------------------
- Bug #9438, #13740, #15037: Handle DB session callback custom fields before session closed (lubosdz)
- Bug #16681: `ActiveField::inputOptions` were not used during some widgets rendering (GHopperMSK)
- Bug #17133: Fixed aliases rendering during help generation for a console command (GHopperMSK)
- Bug #17185: Fixed `AssetManager` timestamp appending when a file is published manually (GHopperMSK)
- Bug #17156: Fixes PHP 7.2 warning when a data provider has no data as a parameter for a GridView (evilito)
- Bug #17083: Fixed `yii\validators\EmailValidator::$checkDNS` tells that every domain is correct on alpine linux (mikk150)

View File

@ -532,7 +532,7 @@ class HelpController extends Controller
protected function formatOptionAliases($controller, $option)
{
foreach ($controller->optionAliases() as $name => $value) {
if ($value === $option) {
if (Inflector::camel2id($value, '-', true) === $option) {
return ', -' . $name;
}
}

View File

@ -685,7 +685,7 @@ class QueryBuilder extends \yii\base\BaseObject
/**
* Builds a SQL statement for creating a new DB table.
*
* The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
* The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
* where name stands for a column name which will be properly quoted by the method, and definition
* stands for the column type which can contain an abstract DB type.
* The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.

View File

@ -76,6 +76,11 @@ class DbSession extends MultiFieldSession
*/
public $sessionTable = '{{%session}}';
/**
* @var array Session fields to be written into session table columns
* @since 2.0.17
*/
protected $fields = [];
/**
* Initializes the DbSession component.
@ -136,6 +141,19 @@ class DbSession extends MultiFieldSession
}
}
/**
* Ends the current session and store session data.
* @since 2.0.17
*/
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}
/**
* Session read handler.
* @internal Do not call this method directly.
@ -169,14 +187,28 @@ class DbSession extends MultiFieldSession
// exception must be caught in session write handler
// https://secure.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes
try {
$fields = $this->composeFields($id, $data);
$fields = $this->typecastFields($fields);
$this->db->createCommand()->upsert($this->sessionTable, $fields)->execute();
// ensure backwards compatability (fixed #9438)
if ($this->writeCallback && !$this->fields) {
$this->fields = $this->composeFields();
}
// ensure data consistency
if (!isset($this->fields['data'])) {
$this->fields['data'] = $data;
} else {
$_SESSION = $this->fields['data'];
}
// ensure 'id' and 'expire' are never affected by [[writeCallback]]
$this->fields = array_merge($this->fields, [
'id' => $id,
'expire' => time() + $this->getTimeout(),
]);
$this->fields = $this->typecastFields($this->fields);
$this->db->createCommand()->upsert($this->sessionTable, $this->fields)->execute();
$this->fields = [];
} catch (\Exception $e) {
Yii::$app->errorHandler->handleException($e);
return false;
}
return true;
}

View File

@ -89,30 +89,19 @@ abstract class MultiFieldSession extends Session
/**
* Composes storage field set for session writing.
* @param string $id session id
* @param string $data session data
* @param string $id Optional session id
* @param string $data Optional session data
* @return array storage fields
*/
protected function composeFields($id, $data)
protected function composeFields($id = null, $data = null)
{
$fields = [
'data' => $data,
];
if ($this->writeCallback !== null) {
$fields = array_merge(
$fields,
call_user_func($this->writeCallback, $this)
);
if (!is_string($fields['data'])) {
$_SESSION = $fields['data'];
$fields['data'] = session_encode();
}
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
if ($id !== null) {
$fields['id'] = $id;
}
if ($data !== null) {
$fields['data'] = $data;
}
// ensure 'id' and 'expire' are never affected by [[writeCallback]]
$fields = array_merge($fields, [
'id' => $id,
'expire' => time() + $this->getTimeout(),
]);
return $fields;
}

View File

@ -763,6 +763,11 @@ class ActiveField extends Component
*/
public function widget($class, $config = [])
{
foreach ($this->inputOptions as $key => $value) {
if (!isset($config['options'][$key])) {
$config['options'][$key] = $value;
}
}
/* @var $class \yii\base\Widget */
$config['model'] = $this->model;
$config['attribute'] = $this->attribute;

View File

@ -23,6 +23,7 @@ class ControllerTest extends TestCase
$this->mockApplication();
Yii::$app->controllerMap = [
'fake' => 'yiiunit\framework\console\FakeController',
'fake_witout_output' => 'yiiunit\framework\console\FakeHelpControllerWithoutOutput',
'help' => 'yiiunit\framework\console\FakeHelpController',
];
}
@ -127,6 +128,10 @@ class ControllerTest extends TestCase
$this->assertFalse(FakeController::getWasActionIndexCalled());
$this->assertEquals(FakeHelpController::getActionIndexLastCallParams(), ['posts/index']);
$helpController = new FakeHelpControllerWithoutOutput('help', Yii::$app);
$helpController->actionIndex('fake/aksi1');
$this->assertContains('--test-array, -ta', $helpController->outputString);
}
/**

View File

@ -0,0 +1,21 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\console;
use yii\console\controllers\HelpController;
use yii\helpers\Console;
class FakeHelpControllerWithoutOutput extends HelpController
{
public $outputString = '';
public function stdout($string)
{
return $this->outputString .= $string;
}
}

View File

@ -10,6 +10,7 @@ namespace yiiunit\framework\web\session;
use Yii;
use yii\db\Connection;
use yii\db\Query;
use yii\db\Migration;
use yii\web\DbSession;
use yiiunit\framework\console\controllers\EchoMigrateController;
use yiiunit\TestCase;
@ -147,6 +148,33 @@ abstract class AbstractDbSessionTest extends TestCase
$this->assertSame('changed by callback data', $session->readSession('test'));
}
/**
* @depends testReadWrite
*/
public function testWriteCustomFieldWithUserId()
{
$session = new DbSession();
$session->open();
$session->set('user_id', 12345);
// add mapped custom column
$migration = new Migration;
$migration->addColumn($session->sessionTable, 'user_id', $migration->integer());
$session->writeCallback = function ($session) {
return ['user_id' => $session['user_id']];
};
// here used to be error, fixed issue #9438
$session->close();
// reopen & read session from DB
$session->open();
$loadedUserId = empty($session['user_id']) ? null : $session['user_id'];
$this->assertSame($loadedUserId, 12345);
$session->close();
}
protected function buildObjectForSerialization()
{
$object = new \stdClass();

View File

@ -14,6 +14,7 @@ use yii\web\View;
use yii\widgets\ActiveField;
use yii\widgets\ActiveForm;
use yii\widgets\InputWidget;
use yii\widgets\MaskedInput;
/**
* @author Nelson J Morais <njmorais@gmail.com>
@ -584,6 +585,30 @@ HTML;
$this->assertEqualsWithoutLE($expectedValue, trim($actualValue));
}
public function testInputOptionsTransferToWidget()
{
$widget = $this->activeField->widget(TestMaskedInput::className(), [
'mask' => '999-999-9999',
'options' => ['placeholder' => 'pholder_direct'],
]);
$this->assertContains('placeholder="pholder_direct"', (string) $widget);
// transfer options from ActiveField to widget
$this->activeField->inputOptions = ['placeholder' => 'pholder_input'];
$widget = $this->activeField->widget(TestMaskedInput::className(), [
'mask' => '999-999-9999',
]);
$this->assertContains('placeholder="pholder_input"', (string) $widget);
// set both AF and widget options (second one takes precedence)
$this->activeField->inputOptions = ['placeholder' => 'pholder_both_input'];
$widget = $this->activeField->widget(TestMaskedInput::className(), [
'mask' => '999-999-9999',
'options' => ['placeholder' => 'pholder_both_direct']
]);
$this->assertContains('placeholder="pholder_both_direct"', (string) $widget);
}
/**
* Helper methods.
*/
@ -667,3 +692,31 @@ class TestInputWidget extends InputWidget
return 'Render: ' . get_class($this);
}
}
class TestMaskedInput extends MaskedInput
{
/**
* @var static
*/
public static $lastInstance;
public function init()
{
parent::init();
self::$lastInstance = $this;
}
public function getOptions() {
return $this->options;
}
public function run()
{
return 'Options: ' . implode(', ', array_map(
function ($v, $k) { return sprintf('%s="%s"', $k, $v); },
$this->options,
array_keys($this->options)
));
}
}