mirror of
https://github.com/yiisoft/yii2.git
synced 2025-11-01 11:39:41 +08:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@ -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>
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
21
tests/framework/console/FakeHelpControllerWithoutOutput.php
Normal file
21
tests/framework/console/FakeHelpControllerWithoutOutput.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user