Специально для студентов курса «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 по умолчанию, когда это возможно, но имейте ввиду некоторые компромиссы, о которых я говорил и которые я считаю скорее делом привычки и сдвигом в образе мышления.

На этом все. По устоявшейся традиции ждем ваши комментарии, друзья.

Комментарии (13)


  1. EvgeniiR
    19.08.2019 17:56
    +1

    Я бы обобщил — делайте Value Objects иммутабельными, DateTime в том числе :)


  1. greabock
    19.08.2019 18:17

    Carbon предоставляет как мутабельный так и иммутабельный варианты.


    1. Hett
      20.08.2019 17:22

      Поддерживаю!


  1. lytican
    19.08.2019 21:13
    +2

    Отвыкнув клонировать объекты, большая часть начинающих программистов в других объектах все же столкнется с этим задекларированным свойством всех объектов. Каждому классу следует делать Immutable-оболочку, или в DateTime есть какая-то особенность?


    1. lytican
      19.08.2019 22:14
      +2

      Жду статью «Перестаньте плодить Immutable-оболочки. Используйте директиву clone».


  1. 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'));
    

    Этот код тоже некрасивый с множеством ненужного шума?


    1. zim32
      20.08.2019 09:33

      Ага, ваш код потенциально опасен баном если забыть написать clone, а имутабл код опасен если забыть назначить новой переменной. Вообще опасно забывать в программировании


  1. peresada
    20.08.2019 07:19

    Когда впервые после PHP столкнулся с Python'ом был крайне удивлен, что словари и списки являются мутабельными, словил на этом немало багов на первых порах, все потому, что не прочитал базовую спецификацию языка, но почему-то мне в голову не пришло писать статью об этом.


  1. GLeBaTi
    20.08.2019 11:19

    Странно, что архитекторы сделали возвращаемое время у add/modify.
    Обычно если возвращает, то это копия.


    1. Finesse
      20.08.2019 11:42

      Это распространённая практика, которая называется chainable methods (методы, которые можно выстраивать в цепочку)


    1. peresada
      20.08.2019 11:43
      +1

      В PHP все объекты передаются по ссылке, нельзя передать объект по значению без клонирования, а возврат значения сделали для реализации FluentInterface скорее всего, чтобы можно было делать цепочку вызовов.


      1. SerafimArts
        20.08.2019 12:16
        +1

        Ну скорее объекты в PHP передаются через указатель на указатель. Для передачи его по ссылке надо использовать "&".


        P.S. Передача объекта по ссылке — это очередная путаница в документации, такая же как и со "свойствами", которых в PHP просто нет и такая же как и с Closure, который является анонимной функцией, а замыканием лишь в частных случаях (за исключением короткого синтаксиса).


  1. vtc
    20.08.2019 17:27
    +1

    Откровенно странная проблема… и не могу сказать чтобы текущее поведение DateTime меня напрягао… Работает согласно документации — в чем проблема вообще?