Рецепт: Yii2 перевод числа в дату и обратно автоматически

Часто при работе над проектами встает вопрос в хранении и отображении дат, при работе с датами разработчик имеет выбор:

  1. Хранить дату ввиде числа (INTEGER);
  2. Ввиде специальных типов данных (в MySQL это например: DATETIME, DATE и TIMESTAMP), в зависимости от контекста задачи (нужна ли точность до секунд или достаточно знать день).

Особой разницы между INTEGER, DATETIME и DATE нету, все эти типы данных хранят дату без привязки к временной зоне, в случае использования INTEGER база данных вообще не знает, что вы храните дату и все операции по конвертации числа в дату вы выполняете самостоятельно, в случае использования DATETIME и DATE и выводе данных, БД конвертирует даты в человекочитаемый вид.

С другой стороны, есть тип данных TIMESTAMP, главный плюс которого это то, что при получении значений из базы данных, они отображаются с учетом часового пояса (на который настроена база данных), так же TIMESTAMP по-умолчанию NOT NULL, а его значение по-умолчанию равно NOW().

В случае, когда проекту не нужно учитывать часовые пояса, вполне нормальным является использование первого варианта, как более простого и понятного, этот вариант и хотелось бы рассмотреть, в контексте Yii2.

Когда вы создаете модель и один из атрибутов представляет из себя тип INTEGER, сложность состоит в том, что при выводе его нужно конвертировать в нужный вам формат (например: дд.мм.гггг), а при записи конвертировать обратно в число, при этом не забывая проверять установлено ли значение или нет, чтобы не записать в дату "0" или же подключать валидаторы, добавляя в них "кастыльные" правила: не равно нулю, больше нуля и т.п.

Для решения таких ситуаций в Yii очень хорошо подходят поведения (behaviors). Давайте посмотрим, как можно решить данную ситуацию на примере шаблона Yii2 Advanced.

Давайте представим, что у нас есть некая модель Human, которая имеет атрибуты id, имя и дату рождения:


namespace frontend\models;

use Yii;
use yii\db\ActiveRecord;

/**
 *
 * @property integer $id
 * @property string $name
 * @property integer $birthday
 */
class Human extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'human';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['name'], 'required'],
            [['birthday'], 'integer']
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'name' => 'Имя',
            'birthday' => 'День рождения',
        ];
    }
}

В итоге, для того, чтобы получить человекочитаемую дату рождения нам каждый раз нужно переводить число в дату и наоборот, для того, чтобы этого избежать давайте сделаем так:

1) Добавим поведение DateToTimeBehavior к себе, создадим папку behaviors в директории frontend и добавим в нее файл DateToTimeBehavior.php (не забываем про открытие и закрытие тегов php):


namespace frontend\behaviors;

use yii\behaviors\AttributeBehavior;
use yii\base\InvalidConfigException;


class DateToTimeBehavior extends AttributeBehavior {

    public $timeAttribute;

    public function getValue($event) {

        if (empty($this->timeAttribute)) {
            throw new InvalidConfigException(
                'Can`t find "fromAttribute" property in ' . $this->owner->className()
            );
        }

        if (!empty($this->owner->{$this->attributes[$event->name]})) {
            $this->owner->{$this->timeAttribute} = strtotime(
                $this->owner->{$this->attributes[$event->name]}
            );

            return date('d.m.Y', $this->owner->{$this->timeAttribute});
        } else if (!empty($this->owner->{$this->timeAttribute})
            && is_numeric($this->owner->{$this->timeAttribute})
        ) {
            $this->owner->{$this->attributes[$event->name]} = date(
                'd.m.Y',
                $this->owner->{$this->timeAttribute}
            );

            return $this->owner->{$this->attributes[$event->name]};
        }

        return null;
    }
}

2) Изменим код в модели Human:


namespace frontend\models;

use frontend\behaviors\DateToTimeBehavior;
use Yii;
use yii\db\ActiveRecord;

/**
 *
 * @property integer $id
 * @property string $name
 * @property integer $birthday
 */
class Client extends \yii\db\ActiveRecord
{
    public $birthday_formatted;

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'human';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['name'], 'required'],
            [['birthday'], 'integer'],
            ['birthday_formatted', 'date', 'format' => 'php:d.m.Y']
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'name' => 'Имя',
            'birthday' => 'День рождение',
            'birthday_formatted' => 'День рождение'
        ];
    }

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            [
                'class' => DateToTimeBehavior::className(),
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_VALIDATE => 'birthday_formatted',
                    ActiveRecord::EVENT_AFTER_FIND => 'birthday_formatted',
                ],
                'timeAttribute' => 'birthday'
            ]
        ];
    }
}

Мы добавили публичное свойство birthday_formatted, в валидации добавили для него проверку, что это дата в формате php:d.m.Y, добавили для него описание (для вывода в виджетах) и самое главное - подключили поведение, в котором указали, что числовое поле у нас birthday и при действиях EVENT_BEFORE_VALIDATE, EVENT_AFTER_FIND, т.е. "до валидации" и "после получения данных модели из базы данных" перевести числовую дату в нормальную дату и добавить к свойству birthday_formatted.

Теперь при выводе данных в формы нам нужно использовать не поле birthday, а поле birthday_formatted, данные в которое будут добавляться автоматически при получении модели из базы данных и наоборот, в случае сохранения модели, данные из поля birthday_formatted будут переводиться в число и помещаться в поле birthday.