在 PHP 中使用可变和不可变 DateTime
发布于 作者 Paul Redmond
可变日期可能导致代码中的混淆和意外错误。我的目标不是告诉您 DateTime 是邪恶的因为它可变,而是要考虑使用可变与不可变 DateTime 对象的权衡和优势。无论哪种方法,都需要良好的测试套件,并了解修改方法如何影响您的日期对象。
直到最近,我甚至没有意识到 PHP 为 DateTime 类提供了一个对应项:DateTimeImmutable
。DateTimeIummtable
类的工作方式与 DateTime
类完全相同,只是它永远不会修改自身,而是返回一个新对象。因此,如果您知道如何使用 DateTime
,那么您可以立即使用 DateTimeImmutable
。
在 PHP 中使用可变 DateTime 对象
对我们这些凡人来说幸运的是,我们可以进一步抽象这些类,并使用像 Carbon 这样的库。稍后,我将向您介绍一个类似的基于 DateTimeIummtable
的不可变库。
Carbon 扩展了 DateTime
的可变变体,这意味着当您传递 Carbon 实例时,对其进行的任何修改调用(例如,addDay()
)都会 **导致更改**
$person = Person::find(1);echo $person->born // 1998-05-29 02:35:53$person->born->addDay(1);echo $person->born // 1998-05-30 02:35:53$person->save();
在 Eloquent 模型的上下文中,这是非常合理的。否则,每次我们想使用不可变方法更新日期时,我们都会有这种奇怪的赋值。
// This code doesn't work, it's what it would look like if Carbon instances were immutable$newBirthday = $person->born->addDay(1);$person->born = $newBirthday; // Or...$person->born = $person->born->addDay(1);
如果您打算进行一些需要修改的比较检查,您需要制作此日期的副本,以避免改变原始属性。例如,这将改变模型属性。
function birthdayIsTomorrow($birthday) { return $birthday->addDay(1)->isTomorrow();} // Mutation takes place...if (birthdayIsTomorrow($person->born)) { } // No mutation takes place...if (birthdayIsTomorrow($person->born->copy())) { } // Via clone, no mutation takes place...if (birthdayIsTomorrow(clone $person->born)) { }
这里另一个有趣的事情是,如果您的代码改变了日期,但您没有 save()
模型,您可能会遇到一个难以追踪的奇怪边缘情况错误。这里最大的收获是要了解代码如何改变您的日期,并将日期的副本而不是整个模型传递给需要修改日期的方法。
我并不是想说使用像 Carbon 这样的可变库是 错误的;我专注于演示这种对象的行为方式以及您必须应用的防御性技术,以避免意外改变。
在 PHP 中使用不可变 DateTime 对象
如果您喜欢使用 Carbon,并且想要尝试具有类似 API 的不可变版本,您可能对 CakePHP 基金会提供的 Chronos 库 感兴趣。Chronos 最初是基于 Carbon 的,是一个独立的库,除了 PHP 版本 ^5.5.9|^7
之外,没有任何外部依赖项。
此外,Chronos 包含可变日期/时间变体,总共有五个类
-
Cake\Chronos\Chronos
是一个不可变的日期和时间对象。 -
Cake\Chronos\Date
是一个不可变的日期对象。 -
Cake\Chronos\MutableDateTime
是一个可变的日期和时间对象。 -
Cake\Chronos\MutableDate
是一个可变的日期对象。 -
Cake\Chronos\ChronosInterval
是 DateInterval 对象的扩展。
在前面的代码示例中使用 Chronos,日期对象不关心改变,因为任何更改都会返回一个新对象。
if (birthdayIsTomorrow($person->born)) { // Immutable, $person->born would equal `today` still}
每次您修改对象时,都会返回一个新副本,使您的代码免受基于顺序的依赖性问题的困扰。请看以下示例。
// i.e., 2018-05-29 04:23:01.342143$meeting = \Cake\Chronos\Chronos::tomorrow();$meeting->addDay(1); // Returns a new object // No mutation, it returns the original 2018-05-29...return $meeting;
要使用不可变日期,您需要在使用修改器时替换变量。
$meeting = \Cake\Chronos\Chronos::tomorrow();$meeting = $meeting->addDay(1); // Returns 2018-05-30...return $meeting
即使您想将日期重置到一天的开始,您也需要执行相同的操作,或者将其链接到原始赋值。
// Assignment$meeting = \Cake\Chronos\Chronos::tomorrow();$meeting = $meeting->addDay(1);$meeting = $meeting->startOfDay(); // Chain the original// i.e., 2018-05-31 00:00:00.000000$meeting = \Cake\Chronos\Chronos::tomorrow() ->addDay(1) ->startOfDay();
不可变性使您通过显式重新分配日期对象来强制执行有意更改,并且您不必担心传递原始对象。
了解更多
尽管差异很微妙,但使用不可变日期对象可以使您确信,传递日期不会导致原始对象发生改变,除非您显式重新分配。
我并不是说一种风格优于另一种,我只是指出我认为了解 PHP 的 DateTimeImmutable
可以帮助澄清不可变日期为您代码提供的某些防御性技术。
考虑到这一点,使用 Carbon 时,您需要注意打算传递给其他方法以进行比较或其他逻辑的任何日期。我建议始终复制 DateTime 实例以避免改变,并注意日期中可能导致意外结果的潜在改变点。
如果您想使用具有像 Carbon 这样好的 API 的不可变 DateTime 库进行更多尝试,请查看 Chronos 文档。