Я редко использую UNIX_TIMESTAMP поля, предпочитаю DATETIME, но недавно мне все-таки потребовалось сделать редактирование этого поля с календарем, и прежде чем придумывать что-то, я попробовал найти на просторах сети готовое решение которое меня бы устроило. Cоветов как это сделать было много, но большинство из них просто решало задачу "в лоб" . Неужели нельзя сделать это красиво?
Идеальный код - это тот код которого не существует. В данном случае, это значит что оптимальным решением проблемы было бы её отсутствие. Если надо вывести календарь для UNIX_TIMESTAMP поля, то хорошо было бы чтобы календарь выводил пользователю поле в каком-то понятном виде, может быть даже разном, в зависимости от страны, но технически работал с теми типами данных которые есть в системе. Я видел примеры таких календарей, которые отдают серверу дату в одном формате, а показывают пользователю в другом, но в экосистеме Yii стандартным считается другой календарь, который этого не умеет, значит все-таки придется конвертировать данные на сервере.
Для начала, в форме где выводится календарь мы берем значения не из поля, а через геттер:
echo $form->field($model, 'updated_at')->widget(DatetimePicker::class, [
'options' => [
'value' => $model->createdAsDatetime
]
]);
В классе модели определяем этот геттер:
public function getCreatedAsDatetime()
{
return Yii::$app->formatter->asDatetime($this->created_at, 'php:Y-m-d H:i:s');
}
Теперь календарь корректно отображает даты из поля, но мы получаем ошибку валидации после передачи данных на сервер. В Yii фреймворке есть достаточно интересная штука, поведение для кастинга аттрибутов, оно позволяет привязаться к определенным событиям модели и приводить аттрибуты к нужному типу. Я подумал, а можно ли использовать его?
public function behaviors()
{
return [
[
'class' => \yii\behaviors\AttributeTypecastBehavior::class,
'typecastAfterValidate' => false
'attributeTypes' => [
'created_at' => function ($value) {
return !is_numeric($value) ? (string) strtotime($value): $value;
}
]
];
}
В бочке меда оказалось не бел ложки дегтя, во-первых \yii\behaviors\AttributeTypecastBehavior
не умеет конвертировать в UNIX_TIMESTAMP в базовом виде, пришлось настроить конвертирование вручную через анонимную функцию. А во-вторых, чтобы проверить правильность переданного значения, нужно делать кастинг аттрибутов перед валидацией, но это событие не поддерживается, есть только afterValidate
, которое тут не подходит.
Придется запускать кастинг вручную:
public function beforeValidate()
{
$this->typecastAttributes(['created_at']);
return parent::beforeValidate();
}
В процессе решения задачи я понял почему разработчики фреймворка не сделали поддержку события beforeValidate
. Если мы делаем кастинг обычных полей обычных типов, то в большинстве случаев пользователь не получит ошибку в случае неверных данных, механизм приведения данных не распознает что данные неверные, поэтому не следует делать кастинг аттрибутов до валидации данных, но в данном случае это будет работать так как нужно.
Если вы используете такой метод, то будьте внимательны, не добавляйте кастинг для тех аттрибутов которые должны конвертироваться после валидации в ту же конфигурацию что и те которые должны конвертироваться до валидации, используйте две разные конфигурации.
В итоге получилось ещё одно далеко не идеальное решение. Лучше запускать кастинг не перед валидацией, а переопределив функцию setAttributes, чтобы данные из календаря приводились к нужному формату сразу как модель их получила. А раз мы запускаем конвертацию вручную и настраиваем тип поля вручную, то использовать yii\behaviors\AttributeTypecastBehavior
для ковертирования даты из календаря лишено смысла.
Лучше просто сделать так:
public function setAttributes($values, $safeOnly = true)
{
parent::setAttributes($values, $safeOnly);
$this->created_at = ($this->created_at && !is_numeric($this->created_at))
? (string) strtotime($this->created_at)
: $this->created_at;
}
В итоге решение получилось простым и понятным, хотя и не настолько красивым как я хотел изначально. Половину текста статьи можно было бы удалить как неудачный эксперимент, но я надеюсь это будет полезно тем кто начинает работать с Yii 2 фреймворком, чтобы понять не только то что фреймворк может, но и то чего он не может.
UPD: Первое же тестирование кода в боевом режиме выявило один нюанс. Если компонент yii\i18n\Formatter
настроен на локальное время, которое отличается от серверного, то после сохранения формы вы будете видеть не то время, которое ввели в форму. В этом случае в setAttributes
надо конвертировать строку во время с учетом часового пояса.
18.04.2020