【最佳实践系列】PHP 日期、时间和时区处理 API 及组件

处理日期时间需要考虑很多事情,例如日期的格式、时区、闰年和天数各异的月份,自己处理太容易出错了,我们应该使用PHP 5.2.0引入的DateTime、DateIntervel和DateTimeZone这些类帮助我们创建及处理日期、时间和时区。

设置默认时区

首先我们要为PHP中处理日期和时间的函数设置默认时区,如果不设置的话,PHP会显示一个E_WARNING消息,设置默认时区有两种方式,可以像下面这样在php.ini中设置:

date.timezone = 'Asia/Shanghai';

也可以在运行时使用date_default_timezone_set()函数设置:

<?php
date_default_timezone_set('Asia/Shanghai');

这两种方式都要求使用有效的时区标识符,PHP完整的时区标识符可以在这里找到:http://php.net/manual/zh/timezones.php

DateTime类

DateTime类提供了一个面向对象接口,用于管理日期和时间,一个DateTime实例表示一个具体的日期和时间,DateTime构造方法是创建DateTime新实例最简单的方式:

<?php
$datetime = new DateTime();

如果没有参数,DateTime类的构造方法创建的是一个表示当前日期和时间的实例。我们可以把一个字符串传入DateTime类的构造方法以便指定日期和时间:

<?php
$datetime = new DateTime('2016-06-06 10:00 pm');

注:传入的字符串参数必须是有效的日期和时间格式(http://php.net/manual/zh/datetime.formats.php

理想情况下,我们会指定PHP能理解的日期和时间格式,可是实际情况并不总是如此,有时我们必须处理其它格式或出乎意料的格式,这时我们可以通过DateTime提供的静态方法createFromFormat来使用自定义的格式创建DateTime实例,该方法的第一个参数是表示日期和时间格式的字符串,第二个参数是要使用这种格式的日期和时间字符串:

<?php
$datetime = DateTime::createFromFormat('M j, Y H:i:s', 'June 6, 2016 22:00:00');

注:也许你很眼熟,没错,DateTime::createFromFormatdate函数类似。可用的日期时间格式参考这里:http://php.net/manual/zh/datetime.createfromformat.php

DateInterval类

处理DateTime实例之前需要先了解DateInterval类,DateInterval实例表示长度固定的时间段(比如两天),或者相对而言的时间段(例如昨天),我们通常使用该类的实例来修改DateTime实例。例如,DateTime提供了用于处理DateTime实例的addsub方法,这两个方法的参数是一个DateInterval实例,表示从DateTime中增加的时间量或减少的时间量。

我们使用构造函数实例化DateInterval实例,DateInterval构造函数的参数是一个表示时间间隔约定的字符串,这个时间间隔约定以字母P开头,后面跟着一个整数,最后是一个周期标识符,限定前面的整数。有效周期标识符如下:

  • Y(年)
  • M(月)
  • D(日)
  • W(周)
  • H(时)
  • M(分)
  • S(秒)

间隔约定中既可以有时间也可以有日期,如果有时间需要在日期和时间之间加上字母T,例如,间隔约定P2D表示间隔两天,间隔约定P2DT5H2M表示间隔两天五小时两分钟。

下面的实例演示了如何使用add方法将DateTime实例表示的日期和时间向后推移一段时间:

<?php
//创建DateTime实例
$datetime = new DateTime('2016-06-06 22:00:00');

//创建长度为两天的间隔
$interval = new DateInterval('P2D');

//修改DateTime实例
$datetime->add($interval);
echo $datetime->format('Y-m-d H:i:s');

我们还可以创建反向的DateInterval实例:

<?php
$datetime = new DateTime();
$interval = DateInterval::createFromDateString('-1 day');
$period = new DatePeriod($datetime, $interval, 3);
foreach ($period as $date) {
    echo $date->format('Y-m-d'), PHP_EOL;
}

以上代码输出为:

2016-06-06
2016-06-05
2016-06-04
2016-06-03

DateTimeZone类

PHP使用DateTimeZone类表示时区,我们只需要把有效的时区标识符传递给DateTimeZone类的构造函数:

<?php
$timezone = new DateTimeZone('Asia/Shanghai');

创建DateTime实例通常需要使用DateTimeZone实例,DateTime类构造方法的第二个参数(可选)就是一个DateTimeZone实例,传入这个参数后,DateTime实例的值以及对这个值的所有修改都相对于这个指定的时区,如果不传入则使用的是前面设置的默认时区:

<?php
$timezone = new DateTimeZone('Asia/Shanghai');
$datetime = new DateTime('2016-06-06', $timezone);

实例化之后还可以使用setTimezone方法修改DateTime实例的时区:

<?php
$timezone = new DateTimeZone('Asia/Shanghai');
$datetime = new DateTime('2016-06-06', $timezone);
$datetime->setTimezone(new DateTimeZone('Asia/Hong_kong'));

DatePeriod类

有时我们需要迭代处理一段时间内反复出现的一系列日期和时间,DatePeriod类可以解决这个问题(前面已经用到过),DatePeriod类的构造方法接受三个参数而且都必须提供:

  • 一个DateTime实例,表示迭代开始的日期和时间
  • 一个DateInterval实例,表示下一个日期和时间的间隔
  • 一个整数,表示迭代的总次数

DatePeriod是迭代器,每次迭代都会产出一个DateTime实例。DatePeriod的第四个参数是可选的,用于显式指定周期的结束日期和时间,如果迭代时想要排除开始日期和时间,可以把构造方法的最后一个参数设为DatePeriod::EXCLUDE_START_DATE常量:

<?php
$datetime = new DateTime();
$interval = new DateInterval('P2D');
$period = new DatePeriod($datetime, $interval, 3, DatePeriod::EXCLUDE_START_DATE);
foreach ($period as $date) {
    echo $date->format('Y-m-d H:i:s'), PHP_EOL;
}

打印的结果是:

2016-06-08
2016-06-10
2016-06-12

nesbot/carbon日期组件

如果经常需要处理日期和时间,应该使用nesbot/carbon组件(https://github.com/briannesbitt/Carbon),Laravel框架也是使用了这个组件处理日期和时间,该组件集成了常用的日期及时间处理API,其底层正是使用了我们上面提到的几个日期时间处理类实现了各种功能,有兴趣可以去研究下。

学院君

学院君 has written 554 articles

资深PHP工程师,Laravel学院院长