FurryCat 04 сентября 2019

Сдвиг во времени: хватит использовать DateTime!

Разбираемся, почему даты не должны изменяться, какими проблемами чревато использование класса DateTime в PHP, и чем его можно заменить.
Сдвиг во времени: хватит использовать DateTime!

Во многих языках программирования – и PHP тут не исключение – манипуляции с датой и временем – занятие раздражающее, кропотливое и чреватое неожиданными ошибками. Возьмем простейший пример:

        $startedAt = new DateTime('2019-06-30 10:00:00');
$finishedAt = $startedAt->add(new DateInterval('PT3M'));
var_dump($startedAt->format('Y-m-d H:i:s')); //2019-06-30 10:03:00
var_dump($finishedAt->format('Y-m-d H:i:s')); //2019-06-30 10:03:00
    

Что пошло не так? Почему время $startedAtсдвинулось на три минуты?

Дело в том, что методы add()sub() и modify() изменяют исходный объект DateTime, а не создают новый. Такое поведение описано в документации – вы ведь читаете документацию? Но в нашем примере оно, конечно, является нежелательным – нужно же как-то сохранить стартовое время.

Можно, например, склонировать исходный объект, прежде чем производить с ним различные манипуляции:

        $startedAt = new DateTime('2019-06-30 10:00:00');
$finishedAt = clone $startedAt;
$finishedAt->add(new DateInterval('PT3M'));
    

Но clone уродует вашу программу – это верный признак "кода с запашком". Как будто вы пытаетесь прикрутить функциональность, которой здесь быть не должно.

Другой вариант решения проблемы – использовать DateTimeImmutable вместо обычного DateTime:

        $startedAt = new DateTime('2019-06-30 10:00:00');
$finishedAt = DateTimeImmutable::createFromMutable($startedAt)->add(new DateInterval('PT3M'));
    

Но почему бы не использовать иммутабельный объект с самого начала – для $startedAt?

Бескомпромиссное использование DateTimeImmutable

Вместо того, чтобы вручную предотвращать неожиданные мутации объектов даты/времени с помощью сложных конструкций, сразу используйте DateTimeImmutable. Этот класс инкапсулирует всю защитную логику, что делает ваш код более надежным и читабельным.

        $startedAt = new DateTimeImmutable('2019-06-30 10:00:00');
$finishedAt = $startedAt->add(new DateInterval('PT3M'));
var_dump($startedAt->format('Y-m-d H:i:s')); //2019-06-30 10:00:00 

var_dump($finishedAt->format('Y-m-d H:i:s')); //2019-06-30 10:03:00
    

Чаще всего в программировании мы используем даты как значения, например, сравниваем их по значению. Когда мы изменяем дату, она становится другой датой. Это прекрасно соответствует концепции Value Object (объект-значение), одной из важных характеристик которой является неизменяемость.

Стиль кодинга

Из-за требования неизменяемости вы должны явно переназначать объект DateTimeImmutable каждый раз, когда совершаете с ним действия, чтобы получить новую копию.

В императивных языках программирования изменяемость является дефолтной концепцией. После долгой работы с мутабельным классом DateTime трудно избавиться от плохих привычек изменения и перейти на новый стиль кодинга с переназначением:

        $this->expiresAt = $this->expiresAt->modify('+1 week');
    

Инструменты статического анализа вроде PHPStan и одного из его расширений могут предупредить вас о неправильном использовании DateTimeImmutable без присваивания.

Однако при арифметических операциях с примитивными значениями этот когнитивный уклон в сторону изменчивости подавляется. Очевидно, что в бессмысленном выражении $a + 3 отсутствует операция присваивания: $a = $a + 3 или $a += 3. Было бы здорово использовать что-то подобное и для объектов-значений.

Некоторые языки программирования предоставляют возможность перегрузки операторов. Это синтаксический сахар, который позволяет реализовать операторы в пользовательских типах и классах, чтобы они вели себя как примитивы. Хотелось бы, чтобы PHP тоже позаимствовал этот трюк. Тогда мы могли бы писать просто вот так:

        $this->expiresAt += '1 week';
    

Расчеты

Некоторые разработчики утверждают, что с точки зрения производительности лучше использовать DataTime. Это так, но если вы не выполняете сотни операций, разница будет незначительной, ведь ссылки на старые объекты DateTimeImmutable будут удалены сборщиком мусора.

Библиотеки даты и времени

Одна из самых популярных библиотек для работы с датой и временем в PHP – это Carbon. Она предоставляет огромный набор удобных методов, однако ее базовая функциональность основана на расширении изменяемого класса DateTime. Неизменяемые варианты тоже есть, но они не являются основными.

Если вам нравится работать с Carbon, но хочется стабильности и иммутабельности, обратите внимание на Chronos. Это самостоятельная библиотека, изначально основанная на Carbon, но по умолчанию работающая с неизменяемыми объектами даты и времени.

Заключение

Класс DateTimeImmutable впервые появился еще в древнем PHP 5.5, но многие разработчики открывают его для себя только сейчас. Старайтесь использовать его всегда, когда это возможно. Со временем у вас сформируется полезная привычка, которая убережет от многих ошибок.

Тоже страдали от DateTime? Поделитесь опытом ;)

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ