Работа с датами и временем в 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
по умолчанию, когда это возможно, но имейте ввиду некоторые компромиссы, о которых я говорил и которые я считаю скорее делом привычки и сдвигом в образе мышления. На этом все. По устоявшейся традиции ждем ваши комментарии, друзья.
Комментарии (13)
lytican
19.08.2019 21:13+2Отвыкнув клонировать объекты, большая часть начинающих программистов в других объектах все же столкнется с этим задекларированным свойством всех объектов. Каждому классу следует делать Immutable-оболочку, или в DateTime есть какая-то особенность?
lytican
19.08.2019 22:14+2Жду статью «Перестаньте плодить Immutable-оболочки. Используйте директиву clone».
alexmat
20.08.2019 05:58+2Работа с датами и временем в PHP порой раздражает, поскольку приводит к неожиданным багам в коде
Игнорирование документации еще и не к таким багам может привести. В документации черным по белому написано:
Adds an amount of days, months, years, hours, minutes and seconds to a DateTime object
Как тогда можно ожидать от кода в примере иного поведения?
$finishedAt = (clone $startedAt)->add(new DateInterval('PT3M'));
Этот код тоже некрасивый с множеством ненужного шума?zim32
20.08.2019 09:33Ага, ваш код потенциально опасен баном если забыть написать clone, а имутабл код опасен если забыть назначить новой переменной. Вообще опасно забывать в программировании
peresada
20.08.2019 07:19Когда впервые после PHP столкнулся с Python'ом был крайне удивлен, что словари и списки являются мутабельными, словил на этом немало багов на первых порах, все потому, что не прочитал базовую спецификацию языка, но почему-то мне в голову не пришло писать статью об этом.
GLeBaTi
20.08.2019 11:19Странно, что архитекторы сделали возвращаемое время у add/modify.
Обычно если возвращает, то это копия.Finesse
20.08.2019 11:42Это распространённая практика, которая называется chainable methods (методы, которые можно выстраивать в цепочку)
peresada
20.08.2019 11:43+1В PHP все объекты передаются по ссылке, нельзя передать объект по значению без клонирования, а возврат значения сделали для реализации FluentInterface скорее всего, чтобы можно было делать цепочку вызовов.
SerafimArts
20.08.2019 12:16+1Ну скорее объекты в PHP передаются через указатель на указатель. Для передачи его по ссылке надо использовать "&".
P.S. Передача объекта по ссылке — это очередная путаница в документации, такая же как и со "свойствами", которых в PHP просто нет и такая же как и с Closure, который является анонимной функцией, а замыканием лишь в частных случаях (за исключением короткого синтаксиса).
vtc
20.08.2019 17:27+1Откровенно странная проблема… и не могу сказать чтобы текущее поведение DateTime меня напрягао… Работает согласно документации — в чем проблема вообще?
EvgeniiR
Я бы обобщил — делайте Value Objects иммутабельными, DateTime в том числе :)