Специально для студентов курса «Backend разработчик на PHP» подготовили перевод интересной статьи о сайд-эффекте популярного инструмента.
Работа с датами и временем в 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
Обе функции $startdate
и $finishdate
спешат на три минуты, потому как такие методы, как add ()
, sub()
или modify()
также изменяют объект DateTime, для которого они вызываются, прежде чем вернуть его. В приведенном выше примере, конечно же, показано нежелательное поведение.
Мы можем исправить эту ошибку, скопировав объект, на который происходит ссылка, прежде чем взаимодействовать с ним, например:
$startedAt = new DateTime('2019-06-30 10:00:00');
$finishedAt = clone $startedAt;
$finishedAt->add(new DateInterval('PT3M'));
Каждый раз, когда я сталкиваюсь с клоном в коде на PHP, это пахнет, как хак чьей-то неудачной архитектуры кода. В данном случае мы использовали клонирование, чтобы избежать изменения поведения, но в то же время код стал некрасивым и приобрел много ненужного шума.
В качестве альтернативы, проблему можно решить преобразованием исходного DateTime
экземпляра в DateTimeImmutable
:
$startedAt = new DateTime('2019-06-30 10:00:00');
$finishedAt = DateTimeImmutable::createFromMutable($startedAt)->add(new DateInterval('PT3M'));
Почему же с самого начала не использовать DateTimeImmutable
?
Бескомпромиссное использование 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';
Одноразовые расчеты
Некоторые люди утверждают, что с точки зрения производительности лучше использовать DateTime
, поскольку вычисления выполняются в пределах одной области выполнения. Это допустимо, однако если вам не нужно выполнять сотни операций, и вы помните о том, что ссылки на старые объекты DateTimeImmutable
будут собраны сборщиком мусора, в большинстве случаев на практике потребление памяти не будет проблемой.
Библиотеки Date/time
Carbon – это крайне популярная библиотека, которая расширяет Date/Time API в PHP, добавляя богатый набор функций. Точнее говоря, она расширяет API изменяемого класса DateTime
, использование которого идет вразрез с темой этой статьи.
Поэтому если вам нравится работать с Carbon, но вы предпочитаете неизменяемость, я предлагаю вам ознакомиться с Chronos. Это standalone библиотека, которая изначально основывалась на Carbon, особенное внимание уделяя предоставлению неизменяемых объектов даты/времени по умолчанию, однако в своем составе она имеет и изменяемые варианты на случай необходимости.
Отредактировано (05/07/2019): Оказалось, что у Carbon есть неизменяемый вариант date/time, что является большим плюсом. Тем не менее, причина, по которой я отдаю предпочтение Chronos, заключается в том, что в отличие от Carbon, он поощряет и способствует неизменяемости по умолчанию, как в коде, так и в документации, и это является решающим факторов в контексте данной статьи.
Последняя мысль
DateTimeImmutable
был впервые представлен еще в древнем PHP 5.5, но к моему удивлению, многие разработчики открывают его для себя только сейчас. Используйте DateTimeImmutable
по умолчанию, когда это возможно, но имейте ввиду некоторые компромиссы, о которых я говорил и которые я считаю скорее делом привычки и сдвигом в образе
На этом все. По устоявшейся традиции ждем ваши комментарии, друзья.
Автор: MaxRokatansky