Декілька корисних прийомів для розробки на Yii 2

Зібрав кілька класів і фрагментів із серії «tips & tricks», які можуть виявитися комусь корисними.
Зміст:
Кілька атрибутів в одній колонці гріду
Виправлення навігації для активних пунктів меню
Маппінг таблиць на інші назви
Чому TimestampBehavior оновлює властивість updated_at, якщо нічого не змінено
Bootstrap DateTimePicker — 2 різних формату для показу в інтерфейсі і для надсилання значення
Облік тимчасової зони користувача для полів з DateTimePicker


Для початку створимо простий CRUD-додаток з однією моделлю Product.





Кілька атрибутів в одній колонці гріду

Припустимо, ми хочемо об'єднати колонки «Created At» і «Updated At» в одну для економії місця, але при цьому хочемо зберегти конфігурацію колонок та сортування за ним. Для цього треба створити окремий невеликий клас для комбінованої колонки, успадкований від звичайного «DataColumn» і вказати його в конфігурації.

CombinedDataColumn
// common/components/grid/CombinedDataColumn.php

namespace common\components\grid;

use yii\grid\DataColumn;

/**
* Renders several attributes in one column grid
*/
class CombinedDataColumn extends DataColumn
{
/* @var $labelTemplate string */
public $labelTemplate = null;

/* @var $valueTemplate string */
public $valueTemplate = null;

/* @var $attributes string[] | null */
public $attributes = null;

/* @var $formats string[] | null */
public $formats = null;

/* @var $values string[] | null */
public $values = null;

/* @var $labels string[] | null */
public $labels = null;

/* @var $sortLinksOptions string[] | null */
public $sortLinksOptions = null;


/**
* Sets parent object parameters for current attribute
* @param $string key Key of current attribute
* @param $attribute string Current attribute
*/
protected function setParameters($key, $attribute)
{
list($attribute, $format) = array_pad(explode(':', $attribute), 2, null);

$this->attribute = $attribute;

if (isset($format)) {
$this->format = $format;
} else if (isset($this->formats[$key])) {
$this->format = $this->formats[$key];
} else {
$this->format = null;
}

if (isset($this->labels[$key])) {
$this->label = $this->labels[$key];
} else {
$this->label = null;
}

if (isset($this->sortLinksOptions[$key])) {
$this->sortLinkOptions = $this->sortLinksOptions[$key];
} else {
$this->sortLinkOptions = [];
}

if (isset($this->values[$key])) {
$this->value = $this->values[$key];
} else {
$this->value = null;
}
}

/**
* Sets parent object parameters and calls parent method for each attribute, then renders combined cell content
* @inheritdoc
*/
protected function renderHeaderCellContent()
{
if (!is_array($this->attributes)) {
return parent::renderHeaderCellContent();
}

$labels = [];
foreach ($this->attributes as $i => $attribute) {
$this->setParameters($i $attribute);
$labels['{'.$i.'}'] = parent::renderHeaderCellContent();
}

if ($this->labelTemplate === null) {
return implode('<br>', $labels);
} else {
return strtr($this->labelTemplate, $labels);
}
}

/**
* Sets parent object parameters and calls parent method for each attribute, then renders combined cell content
* @inheritdoc
*/
protected function renderDataCellContent($model, $key, $index)
{
if (!is_array($this->attributes)) {
return parent::renderDataCellContent($model, $key, $index);
}

$values = [];
foreach ($this->attributes as $i => $attribute) {
$this->setParameters($i $attribute);
$values['{'.$i.'}'] = parent::renderDataCellContent($model, $key, $index);
}

if ($this->valueTemplate === null) {
return implode('<br>', $values);
} else {
return strtr($this->valueTemplate, $values);
}
}
}



// frontend/views/product/index.php

GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],

'id',
'name',
[
'class' => 'common\components\grid\CombinedDataColumn',
'labelTemplate' => '{0} / {1}',
'valueTemplate' => '{0} / {1}',
'labels' => [
'Created At',
'[ Updated At ]',
],
'attributes' => [
'created_at:datetime',
'updated_at:html',
],
'values' => [
null,
function ($model, $_key, $_index, $_column) {
return '[ ' . Yii::$app->formatter->asDatetime($model->updated_at) . ']';
},
],
'sortLinksOptions' => [
['class' => 'text-nowrap'],
null,
],
],

['class' => 'yii\grid\ActionColumn'],
],
]);




Зробимо, щоб сортування за замовчуванням була id DESC.

// frontend/models/ProductSearch.php

public function search($params)
{
...
if (empty($dataProvider->sort->getAttributeOrders())) {
$dataProvider->query->orderBy(['id' => SORT_DESC]);
}
...
}

Якщо робити через
$dataProvider->sort->defaultOrder
, то в гріді у назві колонки додається іконка сортування.


Виправлення навігації для активних пунктів меню

Додамо управління користувачами. Поставимо модуль «dektrium/yii2-user», застосуємо міграції, додамо користувача admin (з паролем 123456, як же без нього), поправимо посилання в меню «layouts/main.php». Зайдемо на сторінку "/user/login". Посилання «Login» у меню неактивна.

// frontend/views/layouts/main.php

if (Yii::$app->user->isGuest) {
$menuItems[] = ['label' => 'Login', 'url' => ['/user/login']];
}



Це відбувається тому, що модуль додає свої правила роутінга. Щоб такі посилання були активними, треба вказувати в них результуючий URL, який вийде після застосування цих правил (у даному випадку "/user/security/login"). Нам це не підходить, тому що навіщо нам тоді роуты для красивих URL.

Зробимо клас
common\components\bootstrap\Nav
, успадкований від
yii\bootstrap\Nav
, і переопределим в ньому метод
isItemActive()
, в якому додамо пару перевірок на збіг з
Yii::$app->request->getUrl()
. В «layouts/main.php» у секції «use» вкажемо наш клас.

Nav
// common/components/bootstrap/Nav.php

namespace common\components\bootstrap;

use Yii;
use yii\bootstrap\Nav as YiiBootstrapNav;

/**
* @inheritdoc
*/
class Nav extends YiiBootstrapNav
{
/**
* Adds additional check - directly compare item URL and request URL.
* Used to make an item active when item URL is handled by module routing
*
* @inheritdoc
*/
protected function isItemActive($item)
{
if (parent::isItemActive($item)) {
return true;
}

if (!isset($item['url'])) {
return false;
}

$route = null;
$itemUrl = $item['url'];

if (is_array($itemUrl) && isset($itemUrl[0])) {
$route = $itemUrl[0];
if ($route[0] !== '/' && Yii::$app->controller) {
$route = Yii::$app->controller->module->getUniqueId() . '/' . $route;
}
} else {
$route = $itemUrl;
}

$requestUrl = Yii::$app->request->getUrl();
$isActive = ($route === $requestUrl || (Yii::$app->homeUrl . $route) === '/' . $requestUrl);

return $isActive;
}
}





Додамо TimestampBehavior в модель Product

// common/models/Product.php

public function behaviors()
{
return [
'TimestampBehavior' => [
'class' => \yii\behaviors\TimestampBehavior::className(),
'value' => function () { return date('Y-m-d H:i:s'); },
],
];
}


Поки що все працює нормально. Ми до цього ще повернемося.


Маппінг таблиць на інші назви

Зробимо додаток трохи складніше. Додамо зберігання сесій в базі даних і RBAC для управління правами доступу.
Також додамо в таблицю «product» колонки «user_id» і «category_id».

команди
php yii migrate --migrationPath=@vendor/yiisoft/yii2/web/migrations
php yii migrate --migrationPath=@yii/rbac/migrations
php yii migrate


міграції
// product_user
$this->addColumn('{{%product}}',
'user_id', $this->integer)->after('id')
);
$this->addForeignKey('fk_product_user', '{{%product}}', 'user_id', '{{%user}}', 'id');


// product_category
$this->createTable('{{%category}}', [
'id' => $this->primaryKey(),
'name' => $this->string(100),
]);

$this->addColumn('{{%product}}',
'category_id', $this->integer)->after('user_id')
);
$this->addForeignKey('fk_product_category', '{{%product}}', 'category_id', '{{%category}}', 'id');



Зазирнемо в нашу базу даних



Щось багато у нас таблиць стало, так відразу і не зрозумієш що звідки взялося, які до додатка відносяться, а які до другорядних модулів. Якщо їх перейменувати? І бажано при цьому нічого не міняти в коді.



Для цього потрібно зробити свої класи для роботи з з'єднанням БД і зі схемою, успадковані від стандартних, в яких перевизначити методи
quoteSql()
та
getRawTableName()
. У класі з'єднання буде нове властивість
$tableMap
, в якому можна задавати відповідність внутрішнього імені таблиці, яке використовується в програмі, і реального, яка використовується в БД.

Connection
// common/components/db/Connection.php

namespace common\components\db;

use Yii;
use yii\db\Connection as BaseConnection;

/**
* Allows to add mapping between internal table name used in application and real table name
* Can be used to set different prefixes tables for from different modules, just to group them in DB
*/
class Connection extends BaseConnection
{
/**
* @var array Mapping between internal table name used in application and real table name
* Can be used to add different prefixes tables for from different modules
* Example: 'tableMap' => ['%session' => '%__web__session']
*/
public $tableMap = [];


/**
* @inheritdoc
*/
public function quoteSql($sql)
{
return preg_replace_callback(
'/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
function ($matches) {
if (isset($matches[3])) {
return $this->quoteColumnName($matches[3]);
} else {
return $this->getRealTableName($matches[2]);
}
},
$sql
);
}

/**
* Returns real table name which is used in database
* @param $tableName string
* @param $useMapping bool
*/
public function getRealTableName($tableName, $useMapping = true)
{
$tableName = ($useMapping && isset($this->tableMap[$tableName]) ? $this->tableMap[$tableName] : $tableName);
$tableName = str_replace('%', $this->tablePrefix, $this->quoteTableName($tableName));

return $tableName;
}
}


mysql/Schema
// common/components/db/mysql/Schema.php

namespace common\components\db\mysql;

use yii\db\mysql\Schema as BaseSchema;

/**
* @inheritdoc
*/
class Schema extends BaseSchema
{
/**
* @inheritdoc
* Also gets real table name from object database connection before replacing table prefix
*/
public function getRawTableName($name)
{
if (strpos($name, '{{') !== false) {
$name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
$name = $this->db->getRealTableName($name);

return $name;
} else {
return $name;
}
}
}



// common/config/main.php

'components' => [
...
'db' => [
'class' => 'common\components\db\Connection',
'schemaMap' => [
'mysql' => 'common\components\db\mysql\Schema',
],
'tableMap' => [
'%migration' => '%__db__migration',
'%session' => '%__web__session',

'%auth_assignment' => '%__rbac__auth_assignment',
'%auth_item' => '%__rbac__auth_item',
'%auth_item_child' => '%__rbac__auth_item_child',
'%auth_rule' => '%__rbac__auth_rule',

'%user' => '%__user__user',
'%profile' => '%__user__profile',
'%token' => '%__user__token',
'%social_account' => '%__user__social_account',
],
],
...
],

назва таблиці "__user__user" виглядає трохи дивно, можна її не перейменовувати, тут просто для наочності

Якщо конфігурація задається у файлі «config/main.php», то треба прибрати з «config/main-local.php» рядок
'class' => 'yii\db\Connection'
, так як він підключається пізніше, і цей параметр буде скасоване. Або ставити весь конфіг в «config/main-local.php». Можливо, так навіть краще, при розробці будуть зрозумілі назви, а в продакшені нормальні.

Перейменування таблиць не потрібно робити міграцією. Якщо скопіювати проект з такими налаштуваннями і запустити міграції, то таблиці будуть створені вже з новими іменами. Також досить складно перейменувати в міграції саму таблицю «migration». Можна потанцювати з бубном навколо копіювання таблиці та перевірки наявності її з новою назвою, але навряд чи це виправдано.


Чому TimestampBehavior оновлює властивість updated_at, якщо нічого не змінено

Зайдемо в редагування якого-небудь продукту і встановимо користувача і категорію. Зауважимо, що властивість «Updated At» оновилося. Тепер знову зайдемо в редагування і, нічого не змінюючи, натискаємо «Зберегти». Властивість «Updated At» знову оновився. Так бути не повинно.

Це сталося, тому що ми додали «user_id» і «category_id».
Ланцюжок наступна. Ми відправляємо форму POST-запитом. Дані в ній, природно, у строковому вигляді. На сервері викликається
$model->load(Yii::$app->request->post())
. Він встановлює, наприклад, властивість
user_id = "1" (string)
.
Далі викликається
$model->save()
і спрацьовує
TimestampBehavior
(котрий
extends AttributeBehavior
).

AttributeBehavior.php
public function evaluateAttributes($event)
{
...
&& empty($this->owner->dirtyAttributes)
...
}

BaseActiveRecord.php
public function getDirtyAttributes($names = null)
{
...
|| $value !== $this->_oldAttributes[$name])
...
}


Значення
$this->_oldAttributes[$name]
завантажене з бази, і значить
$this->_oldAttributes['user_id'] = 1 (int)
. Суворе порівняння повертає false, і властивість вважається зміненим.

Щоб це виправити, треба додати фільтрацію значень метод rules().

Для властивостей, які not null, все досить просто, наводимо їх до int. Для властивостей, які можуть бути null, треба написати callback. У нашому додатку другий варіант.

// not null
[['user_id', 'category_id'], 'filter', 'filter' => 'intval'],

// null
[['user_id', 'category_id'], 'filter', 'filter' => function ($value) {
return ($value === " ? null : (int)$value);
}],



Bootstrap DateTimePicker — 2 різних формату для показу в інтерфейсі і для надсилання значення

Додамо фільтри
created_from, created_to, updated_from, updated_to
. Для дати/часу я зазвичай використовую віджети від kartik для Bootstrap Datepicker/Datetimepicker.



Але є одна проблема, в них можна задати різні формати для відображення і для зберігання значення. В результаті на сервер може відправлятися щось типу «14 junio 2016, mar.». Виправити це можна, додавши в рендеринг hidden-поле з новим форматом. У Datetimepicker можна задати опції linkField і linkFormat, а в Datepicker треба ловити подія changeDate і форматувати вручну. Також треба обробляти натискання на кнопку очищення значення.

DatePicker
// common/widgets/DatePicker.php

namespace common\widgets;

use Yii;
use yii\helpers\Html;
use yii\helpers\FormatConverter;
use yii\base\InvalidParamException;

/**
* Extended DatePicker, allows to set different formats for sending and value displaying
*/
class DatePicker extends \kartik\data\DatePicker
{
public $saveDateFormat = 'php:Y-m-d';

private $savedValueInputID = ";
private $attributeValue = null;


public function __construct($config = [])
{
$defaultOptions = [
'type' => static::TYPE_COMPONENT_APPEND,
'convertFormat' => true,
'pluginOptions' => [
'autoclose' => true,
'format' => Yii::$app->formatter->dateFormat,
],
];
$config = array_replace_recursive($defaultOptions, $config);

parent::__construct($config);
}

public function init()
{
if ($this->hasModel()) {
$model = $this->model;
$attribute = $this->attribute;
$value = $model->$attribute;

$this->model = null;
$this->attribute = null;
$this- > name = Html::getInputName($model, $attribute);
$this->attributeValue = $value;

if ($value) {
try {
$this->value = Yii::$app->formatter->asDateTime($value, $this->pluginOptions['format']);
} catch (InvalidParamException $e) {
$this->value = null;
}
}
}

return parent::init();
}

protected function parseMarkup($input)
{
$res = parent::parseMarkup($input);

$res .= $this->renderSavedValueInput();
$this->registerScript();

return $res;
}

protected function renderSavedValueInput()
{
$value = $this->attributeValue;

if ($value !== null && $value !== ") {
// format value according to saveDateFormat
try {
$value = Yii::$app->formatter->asDate($value, $this->saveDateFormat);
} catch(InvalidParamException $e) {
// ignore exception and keep original value if it is not a valid date
}
}

$this->savedValueInputID = $this->options['id'].'-saved-value';

$options = $this->options;
$options['id'] = $this->savedValueInputID;
$options['value'] = $value;

// render hidden input
if ($this->hasModel()) {
$contents = Html::activeHiddenInput($this->model, $this->attribute, $options);
} else {
$contents = Html::hiddenInput($this->name, $value, $options);
}

return $contents;
}

protected function registerScript()
{
$language = $this->language ? $this->language : Yii::$app->language;

$format = $this->saveDateFormat;
$format = strncmp($format 'php:', 4) === 0 ? substr($format, 4) :
FormatConverter::convertDateIcuToPhp($format $type);
$saveDateFormatJs = static::convertDateFormat($format);


$containerID = $this->options['data-datepicker-source'];
$hiddenInputID = $this->savedValueInputID;
$script = "
$('#{$containerID}').on('changeDate', function(e) {
var savedValue = e.format(0, '{$saveDateFormatJs}');
$('#{$hiddenInputID}').val(savedValue).trigger('change');
}).on('clearDate', function(e) {
var savedValue = e.format(0, '{$saveDateFormatJs}');
$('#{$hiddenInputID}').val(savedValue).trigger('change');
});

$('#{$containerID}').data('datepicker').update();
$('#{$containerID}').data('datepicker')._trigger('changeDate');
";
$view = $this->getView();
$view->registerJs($script);
}
}


DateTimePicker
// common/widgets/DateTimePicker.php

namespace common\widgets;

use Yii;
use yii\helpers\Html;
use yii\helpers\FormatConverter;
use yii\base\InvalidParamException;

/**
* Extended DateTimePicker, allows to set different formats for sending and value displaying
*/
class DateTimePicker extends \kartik\datetime\DateTimePicker
{
public $saveDateFormat = 'php:Y-m-d H:i';
public $removeButtonSelector = '.kv-date-remove';

private $savedValueInputID = ";
private $attributeValue = null;


public function __construct($config = [])
{
$defaultOptions = [
'type' => static::TYPE_COMPONENT_APPEND,
'convertFormat' => true,
'pluginOptions' => [
'autoclose' => true,
'format' => Yii::$app->formatter->datetimeFormat,
'pickerPosition' => 'top-left',
],
];
$config = array_replace_recursive($defaultOptions, $config);

parent::__construct($config);
}

public function init()
{
if ($this->hasModel()) {
$model = $this->model;
$attribute = $this->attribute;
$value = $model->$attribute;

$this->model = null;
$this->attribute = null;
$this- > name = Html::getInputName($model, $attribute);
$this->attributeValue = $value;

if ($value) {
try {
$this->value = Yii::$app->formatter->asDateTime($value, $this->pluginOptions['format']);
} catch (InvalidParamException $e) {
$this->value = null;
}
}
}

return parent::init();
}

public function registerAssets()
{
$format = $this->saveDateFormat;
$format = strncmp($format 'php:', 4) === 0
? substr($format, 4)
: FormatConverter::convertDateIcuToPhp($format $type);
$saveDateFormatJs = static::convertDateFormat($format);


$this->savedValueInputID = $this->options['id'].'-saved-value';

$this->pluginOptions['linkField'] = $this->savedValueInputID;
$this->pluginOptions['linkFormat'] = $saveDateFormatJs;

return parent::registerAssets();
}

protected function parseMarkup($input)
{
$res = parent::parseMarkup($input);

$res .= $this->renderSavedValueInput();
$this->registerScript();

return $res;
}

protected function renderSavedValueInput()
{
$value = $this->attributeValue;

if ($value !== null && $value !== ") {
// format value according to saveDateFormat
try {
$value = Yii::$app->formatter->asDateTime($value, $this->saveDateFormat);
} catch(InvalidParamException $e) {
// ignore exception and keep original value if it is not a valid date
}
}

$options = $this->options;
$options['id'] = $this->savedValueInputID;
$options['value'] = $value;

// render hidden input
if ($this->hasModel()) {
$contents = Html::activeHiddenInput($this->model, $this->attribute, $options);
} else {
$contents = Html::hiddenInput($this->name, $value, $options);
}

return $contents;
}

protected function registerScript()
{
$containerID = $this->options['id'] . '-datetime';
$hiddenInputID = $this->savedValueInputID;

if ($this->removeButtonSelector) {
$script = "
$('#{$containerID}').find('{$this->removeButtonSelector}').on('click', function(e) {
$('#{$containerID}').find('input').val(").trigger('change');
$('#{$containerID}').data('datetimepicker').reset();

$('#{$containerID}').trigger('changeDate', {
type: 'changeDate',
date: null,
});
});

$('#{$containerID}').trigger('changeDate', {
type: 'changeDate',
date: null,
});
";

$view = $this->getView();
$view->registerJs($script);
}
}
}



// frontend/views/product/_search.php
<?= $form->field($model, 'created_from')->widget(\common\widgets\DateTimePicker::classname()) ?>


Можна ще стилізувати кнопку очищення, щоб виглядала краще.

main.css
.input-group.date .kv-date-remove,
.input-group.date .kv-date-calendar {
color: #626262;
}

.input-group.date .kv-date-remove-custom {
position: absolute;
z-index: 3;
color: #000;
opacity: 0.4;
font-size: 16px;
font-weight: 700;
line-height: 0.6;
right: 50px;
top: 14px;
cursor: pointer;
}

.input-group.date .kv-date-remove-custom:hover {
opacity: 0.6;
}

.input-group.date input {
padding-right: 30px;
}

.input-group.date .input-group-addon.kv-date-calendar + .kv-date-remove-custom {
left: 50px;
right: auto;
}

.input-group.date .input-group-addon.kv-date-calendar + .kv-date-remove-custom + input {
padding-left: 32px;
}


_search.php
<?php
// frontend/views/product/_search.php

$dateTimePickerOptions = [
'removeButton' => '<span class="kv-date-remove kv-date-remove-custom">×</span>',
'removeButtonSelector' => '.kv-date-remove-custom',
'pluginEvents' => [
'changeDate' => "function(e) {
var isEmpty = ($(this).find('input').val() == ");
$(this).find('.kv-date-remove-custom').toggle(!isEmpty);
}",
],
];
?>
<?= $form->field($model, 'created_from')->widget(DateTimePicker::classname(), $dateTimePickerOptions) ?>





До речі, я трохи здивований тим фактом, що в багатьох datepicker-ах є підтримка локалізації, але немає можливості відображати і відправляти значення в різних форматах. По-моєму, datepicker — прямий аналог тег select. В select показуємо текст, відправляємо option value, datepicker показуємо дату в красивому і зрозумілому форматі, відправляємо в технічному.

У того ж kartik є модуль yii2-datecontrol, в якому можна задати інший формат збереження. Але мені він не сподобався, бо він за замовчуванням відправляє відображається текст на сервер, там його парсити, форматує в заданому форматі для збереження і відправляє назад. Можна задати налаштування для форматування на клієнті, але в цілому він якийсь громіздкий, і немає причин його ставити, щоб просто відформатувати дати YYYY-mm-dd.


Облік тимчасової зони користувача для полів з DateTimePicker

Отже, фільтри по датах у нас є. Тепер уявімо, що у нас користувачі з різних часових зон. Сервер і база у нас у UTC. Форматування виводу задається параметрами форматтера, а що робити з введенням? Користувач у фільтрі задає той час, який очікує побачити в даних гріду. Рішення просте, потрібно після завантаження форми конвертувати значення полів з часом таймзоны користувача в таймзону сервера. Таким чином, усередині додатку час завжди буде у UTC.

InputTimezoneConverter
// common/components/InputTimezoneConverter.php

namespace common\components;

use Yii;
use yii\i18n\Formatter;

/**
* Allows to convert time values in user timezone (usually from input fields)
* into appplication timezone which is used in models
* Conversion from application timezone into user timezone
* is usulally done by Yii::$app->formatter->asDatetime()
*/
class InputTimezoneConverter
{
/** @var Formatter */
private $formatter = null;


public function __construct($formatter = null)
{
if ($formatter === null) {
// we change formatter configuration so we need to clone it
$formatter = clone(Yii::$app->formatter);
}
$this->formatter = $formatter;
$this->formatter->datetimeFormat = 'php:Y-m-d H:i:s';

// swap timeZone and defaultTimeZone of default configuration formatter
// to perform conversion back to default timezone

$timeZone = $this->formatter->timeZone;
$this->formatter->timeZone = $this->formatter->defaultTimeZone;
$this->formatter->defaultTimeZone = $timeZone;
}

/**
* @param $string value
*/
public function convertValue($value)
{
if ($value === null || $value === ") {
return $value;
}

return $this->formatter->asDatetime($value);
}
}



// common/config/main.php

return [
'timeZone' => 'UTC',
...
'components' => [
...
'formatter' => [
'dateFormat' => 'php:m-d-Y',
'datetimeFormat' => 'php:m-d-Y H:i',
'timeZone' => 'Europe/Moscow',
'defaultTimeZone' => 'UTC',
],
...
],
...
];


// frontend/models/ProductSearch.php

/**
* @inheritdoc
* Additionally converts attributes containing time from user timezone to application timezone
*/
public function load($data, $formName = NULL)
{
$loaded = parent::load($data, $formName);

if ($loaded) {
$timeAttributes = ['created_from', 'created_to', 'updated_from', 'updated_to'];
$inputTimezoneConverter = new \common\components\InputTimezoneConverter();
foreach ($timeAttributes as $attribute) {
$this->$attribute = $inputTimezoneConverter->convertValue($this->$attribute);
}
}
}





Віджет для javascript-коду

Іноді є необхідність написати javascript-код в view-файлі. Звичайно, писати його краще в js-файлів, але випадки бувають різні. Часто пишуть його в рядку і реєструють через registerJs(), щоб вивести в кінці документа разом з іншими скриптами. Але у рядку не у всіх редакторах є підсвічування, так і з лапками можуть бути проблеми, а без рядка він виведеться в середині. Можна зробити віджет, який буде брати вміст між викликами
begin()
та
end()
, прибирати теги і викликати
registerJs()
.

Script
// common/widgets/Script.php

namespace common\widgets;

use Yii;
use yii\web\View;

/**
* Allows to write javascript in view inside '<script></script>' tags and render it at the end of body together with other scripts
* '<script></script>' tags are removed from result output
*/
class Script extends \yii\base\Widget
{
/** @var string Script position, used in registerJs() function */
public $position = View::POS_READY;


/**
* @inheritdoc
*/
public function init()
{
parent::init();
ob_start();
}

/**
* @inheritdoc
*/
public function run()
{
$script = ob_get_clean();
$script = preg_replace('|^\s*<script>|ui', ", $script);
$script = preg_replace('|</script>\s*$|ui', ", $script);
$this->getView()->registerJs($script, $this->position);
}
}



<?php
// frontend/views/product/_form.php

common use\widgets\Script;
?>

<?php Script::begin(); ?>
<script>
$(document).ready(function() {
console.log('Product form: $(document).ready()');
});
</script>
<?php Script::end(); ?>



Примітка

Також залишу посилання на документацію про те, як розмістити advanced-додаток на одному домені (наприклад, на хостингу). Гугл по запиту «yii2 advanced single domain» видає приклади з конфіг для Apache, а насправді все набагато простіше. А для правильної посилання треба здогадатися ввести «yii2 advanced shared hosting». Якщо коротко, то треба перемістити папку «backend/web, в папку «frontend/web/admin» і відредагувати шляху в «index.php».

Всі приклади можна подивитися на github в окремих коммитах.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.