Я редко использую 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