日期和时间处理,是PHP开发中非常常见的需求。文章发布时间、用户注册时间、订单时间、日志时间、倒计时、时间戳转换、时区处理……这些都需要用到日期和时间函数。

PHP提供了丰富的日期和时间处理函数,从基础的date()、time(),到面向对象的DateTime类,再到时区处理、日期计算、格式化输出等,功能非常强大。

但日期和时间处理,也是容易出错的地方。时区问题、时间戳溢出、格式化错误、日期计算、闰年处理……这些坑,很多开发者都踩过。

今天,我们来详细学习PHP的日期和时间处理,从基础函数到DateTime类,从时区处理到日期计算,帮你掌握这个常用但容易出错的技能。

基础日期时间函数

time()

获取当前的Unix时间戳(从1970年1月1日00:00:00 UTC到现在的秒数)。

echo time();  // 输出类似1438723200的数字

时间戳是一个整数,方便计算和存储。但32位系统的时间戳在2038年1月19日会溢出(2038年问题),所以推荐用64位系统或DateTime类。

date()

格式化时间戳为可读的日期字符串。

echo date('Y-m-d H:i:s');  // 输出2015-08-07 22:00:00
echo date('Y年m月d日');     // 输出2015年08月07日
echo date('l, F j, Y');    // 输出Friday, August 7, 2015

date()的第一个参数是格式字符串,常用格式字符:

  • Y:4位年份(2015)
  • y:2位年份(15)
  • m:2位月份(01-12)
  • n:月份(1-12,无前导零)
  • d:2位日期(01-31)
  • j:日期(1-31,无前导零)
  • H:24小时制小时(00-23)
  • h:12小时制小时(01-12)
  • i:分钟(00-59)
  • s:秒(00-59)
  • a:am/pm
  • A:AM/PM
  • l:星期几的完整名称(Friday)
  • D:星期几的缩写(Fri)
  • w:星期几的数字(0周日-6周六)
  • F:月份的完整名称(August)
  • M:月份的缩写(Aug)
  • t:本月有多少天(28-31)
  • L:是否闰年(1是,0否)
  • U:Unix时间戳

date()的第二个参数是时间戳,可选,默认是当前时间。

echo date('Y-m-d', time() - 86400);  // 昨天的日期
echo date('Y-m-d', 1438723200);      // 指定时间戳的日期

strtotime()

将英文文本日期解析为时间戳,非常强大。

echo strtotime('2015-08-07');        // 解析日期字符串
echo strtotime('now');                // 当前时间
echo strtotime('+1 day');             // 明天
echo strtotime('-1 day');             // 昨天
echo strtotime('+1 week');            // 下周
echo strtotime('+1 month');           // 下个月
echo strtotime('last Monday');        // 上周一
echo strtotime('next Friday');        // 下周五
echo strtotime('first day of next month');  // 下个月第一天
echo strtotime('last day of next month');   // 下个月最后一天

strtotime()非常灵活,能解析各种英文日期表达式,但要注意:

  • 不同版本的PHP解析可能有差异
  • 某些模糊的表达式可能解析不符合预期
  • 建议用DateTime类替代,更可靠

mktime()

根据指定的时分秒月日年,获取时间戳。

// mktime(时, 分, 秒, 月, 日, 年)
echo mktime(0, 0, 0, 8, 7, 2015);  // 2015年8月7日0点的时间戳

mktime()会自动处理越界,比如mktime(0,0,0,13,1,2015)会自动变成2016年1月1日。

getdate()

获取日期信息数组。

$today = getdate();
print_r($today);
// Array (
//   [seconds] => 0
//   [minutes] => 0
//   [hours] => 22
//   [mday] => 7
//   [wday] => 5
//   [mon] => 8
//   [year] => 2015
//   [yday] => 218
//   [weekday] => Friday
//   [month] => August
//   [0] => 1438953600
// )

checkdate()

验证日期是否合法。

var_dump(checkdate(2, 29, 2015));  // bool(false),2015不是闰年
var_dump(checkdate(2, 29, 2016));  // bool(true),2016是闰年
var_dump(checkdate(13, 1, 2015));  // bool(false),月份不合法

时区处理

时区,是日期时间处理中最容易出错的地方。PHP默认使用服务器的时区,如果服务器时区和用户时区不一致,就会出现时间错误。

设置时区

// 在php.ini中设置
date.timezone = Asia/Shanghai

// 在代码中设置
date_default_timezone_set('Asia/Shanghai');

常用时区:

  • Asia/Shanghai:中国标准时间(UTC+8)
  • Asia/Hong_Kong:香港时间
  • Asia/Tokyo:东京时间
  • America/New_York:纽约时间
  • America/Los_Angeles:洛杉矶时间
  • Europe/London:伦敦时间
  • UTC:协调世界时

获取时区

echo date_default_timezone_get();  // 获取当前时区

时区转换

// 方法1:用date()和时间戳(时间戳是UTC的,date()会根据当前时区转换)
date_default_timezone_set('Asia/Shanghai');
echo date('Y-m-d H:i:s', 1438953600);  // 北京时间

date_default_timezone_set('America/New_York');
echo date('Y-m-d H:i:s', 1438953600);  // 纽约时间

// 方法2:用DateTime类(推荐,更清晰)
$date = new DateTime('2015-08-07 22:00:00', new DateTimeZone('Asia/Shanghai'));
$date->setTimezone(new DateTimeZone('America/New_York'));
echo $date->format('Y-m-d H:i:s');  // 转换为纽约时间

DateTime类(推荐)

PHP 5.2引入了DateTime类,提供了面向对象的日期时间处理方式,比过程式函数更强大、更可靠、更易读。推荐使用DateTime类替代date()、strtotime()等函数。

创建DateTime对象

// 当前时间
$date = new DateTime();
echo $date->format('Y-m-d H:i:s');

// 指定时间
$date = new DateTime('2015-08-07 22:00:00');
echo $date->format('Y-m-d H:i:s');

// 指定时间和时区
$date = new DateTime('2015-08-07 22:00:00', new DateTimeZone('Asia/Shanghai'));
echo $date->format('Y-m-d H:i:s');

// 从时间戳创建
$date = new DateTime();
$date->setTimestamp(1438953600);
echo $date->format('Y-m-d H:i:s');

// 从格式创建(指定格式解析)
$date = DateTime::createFromFormat('Y-m-d', '2015-08-07');
echo $date->format('Y-m-d H:i:s');

格式化输出

$date = new DateTime('2015-08-07 22:00:00');
echo $date->format('Y-m-d H:i:s');  // 2015-08-07 22:00:00
echo $date->format('Y年m月d日 H:i');  // 2015年08月07日 22:00
echo $date->format('l, F j, Y');     // Friday, August 7, 2015

format()方法的格式字符和date()一样。

获取和设置日期时间

$date = new DateTime();

// 设置日期
$date->setDate(2015, 8, 7);  // setDate(年, 月, 日)

// 设置时间
$date->setTime(22, 0, 0);  // setTime(时, 分, 秒)

// 设置时间戳
$date->setTimestamp(1438953600);

// 获取时间戳
echo $date->getTimestamp();

// 获取时区
$timezone = $date->getTimezone();
echo $timezone->getName();

日期计算(DateInterval)

DateInterval类表示时间间隔,可以用来对DateTime进行加减运算。

$date = new DateTime('2015-08-07');

// 加1天
$date->add(new DateInterval('P1D'));
echo $date->format('Y-m-d');  // 2015-08-08

// 减1天
$date->sub(new DateInterval('P1D'));
echo $date->format('Y-m-d');  // 2015-08-07

// 加1周
$date->add(new DateInterval('P1W'));
echo $date->format('Y-m-d');  // 2015-08-14

// 加1个月
$date->add(new DateInterval('P1M'));
echo $date->format('Y-m-d');  // 2015-09-07

// 加1年
$date->add(new DateInterval('P1Y'));
echo $date->format('Y-m-d');  // 2016-08-07

// 加1年2个月3天
$date->add(new DateInterval('P1Y2M3D'));
echo $date->format('Y-m-d');  // 2017-10-10

// 加时间(1小时2分钟3秒)
$date->add(new DateInterval('PT1H2M3S'));
echo $date->format('Y-m-d H:i:s');

DateInterval的格式:

  • P:周期(Period),必须在最前面
  • Y:年
  • M:月
  • D:日
  • W:周
  • T:时间(Time),分隔日期和时间
  • H:小时
  • M:分钟(在T后面是分钟,在T前面是月)
  • S:秒

例子:

  • P1D:1天
  • P1M:1个月
  • PT1M:1分钟
  • P1Y2M3DT4H5M6S:1年2个月3天4小时5分钟6秒

日期比较

$date1 = new DateTime('2015-08-07');
$date2 = new DateTime('2015-09-01');

// 比较大小
if ($date1 < $date2) {
    echo 'date1在date2之前';
}

// 计算差值
$diff = $date1->diff($date2);
echo $diff->format('%a天');  // 25天
echo $diff->format('%m个月%d天');  // 0个月25天
echo $diff->y . '年' . $diff->m . '月' . $diff->d . '天';

diff()方法返回DateInterval对象,表示两个日期的差值。

常用格式:

  • %a:总天数
  • %y:年
  • %m:月
  • %d:日
  • %h:小时
  • %i:分钟
  • %s:秒
  • %R:正负号(+或-)

修改日期(modify)

modify()方法可以用字符串修改日期,类似strtotime()。

$date = new DateTime('2015-08-07');

$date->modify('+1 day');
echo $date->format('Y-m-d');  // 2015-08-08

$date->modify('last Monday');
echo $date->format('Y-m-d');  // 上周一

$date->modify('first day of next month');
echo $date->format('Y-m-d');  // 下个月第一天

常用日期时间操作

获取今天/昨天/明天

$today = new DateTime();
echo $today->format('Y-m-d');

$yesterday = new DateTime('-1 day');
echo $yesterday->format('Y-m-d');

$tomorrow = new DateTime('+1 day');
echo $tomorrow->format('Y-m-d');

获取本周/上周/下周

// 本周一
$monday = new DateTime('monday this week');
echo $monday->format('Y-m-d');

// 本周日
$sunday = new DateTime('sunday this week');
echo $sunday->format('Y-m-d');

// 上周一
$lastMonday = new DateTime('monday last week');
echo $lastMonday->format('Y-m-d');

获取本月/上月/下月

// 本月第一天
$firstDay = new DateTime('first day of this month');
echo $firstDay->format('Y-m-d');

// 本月最后一天
$lastDay = new DateTime('last day of this month');
echo $lastDay->format('Y-m-d');

// 上月第一天
$lastMonthFirst = new DateTime('first day of last month');
echo $lastMonthFirst->format('Y-m-d');

// 下月第一天
$nextMonthFirst = new DateTime('first day of next month');
echo $nextMonthFirst->format('Y-m-d');

获取今年/去年/明年

// 今年第一天
$yearFirst = new DateTime('first day of January this year');
echo $yearFirst->format('Y-m-d');

// 今年最后一天
$yearLast = new DateTime('last day of December this year');
echo $yearLast->format('Y-m-d');

判断闰年

$year = 2015;
$date = new DateTime("$year-02-01");
if ($date->format('L') == 1) {
    echo "$year是闰年";
} else {
    echo "$year不是闰年";
}
// 或者用date()
if (date('L', mktime(0,0,0,2,1,$year)) == 1) { ... }

计算两个日期相差天数

$date1 = new DateTime('2015-02-15');
$date2 = new DateTime('2015-08-07');
$diff = $date1->diff($date2);
echo '相差' . $diff->days . '天';  // 相差173天

倒计时

$target = new DateTime('2016-01-01 00:00:00');
$now = new DateTime();
$diff = $now->diff($target);
echo '距离2016年还有' . $diff->days . '天' . $diff->h . '小时' . $diff->i . '分钟';

友好的时间显示("刚刚"、"x分钟前")

function friendlyTime($datetime) {
    $time = is_numeric($datetime) ? $datetime : strtotime($datetime);
    $diff = time() - $time;
    
    if ($diff < 60) return '刚刚';
    if ($diff < 3600) return floor($diff / 60) . '分钟前';
    if ($diff < 86400) return floor($diff / 3600) . '小时前';
    if ($diff < 2592000) return floor($diff / 86400) . '天前';
    if ($diff < 31536000) return floor($diff / 2592000) . '个月前';
    return floor($diff / 31536000) . '年前';
}

echo friendlyTime('2015-08-07 22:00:00');

常见坑和注意事项

1. 时区问题

最常见的坑。服务器时区和用户时区不一致,导致时间显示错误。

解决:

  • 在php.ini中设置date.timezone = Asia/Shanghai
  • 或者在代码入口处datedefaulttimezone_set('Asia/Shanghai')
  • 存储时间用UTC时间戳,显示时根据用户时区转换
  • 用DateTime类明确指定时区

2. 2038年问题

32位系统的时间戳是32位有符号整数,最大值是2147483647,对应2038年1月19日03:14:07 UTC。超过这个时间,时间戳会溢出变成负数。

解决:

  • 使用64位系统
  • 使用DateTime类(PHP 5.2+支持64位日期)
  • 数据库用DATETIME类型而不是INT存储时间

3. strtotime()的模糊解析

strtotime()对某些模糊的表达式解析可能不符合预期,比如:

  • "08/07/2015"会被解析为月/日/年(美式),而不是日/月/年
  • 不同版本的PHP解析可能有差异

解决:

  • 用DateTime::createFromFormat()明确指定格式
  • 用标准的ISO格式(Y-m-d)存储和传输日期

4. 月份加减的边界问题

给1月31日加1个月,会变成3月3日(因为2月没有31天),而不是2月28日。这可能不符合预期。

$date = new DateTime('2015-01-31');
$date->modify('+1 month');
echo $date->format('Y-m-d');  // 2015-03-03,不是2015-02-28

解决:根据需求处理,比如先设为月初再加减月,或者用其他方式。

5. 夏令时

某些国家和地区有夏令时,切换时会出现时间跳跃(少1小时或多1小时)。

解决:用DateTime类和DateTimeZone,PHP会自动处理夏令时。

6. 微秒处理

PHP 5.2.2+的DateTime支持微秒,但date()不支持。需要用u格式字符。

$date = new DateTime();
echo $date->format('Y-m-d H:i:s.u');  // 带微秒

7. 数据库时间类型

MySQL中:

  • DATETIME:范围1000-9999年,无时区,8字节
  • TIMESTAMP:范围1970-2038年,自动转时区,4字节
  • 推荐用DATETIME,范围大,不受2038年问题影响

最佳实践

  1. 统一时区:在应用入口设置统一时区,所有时间基于这个时区处理。
  2. 存储用UTC:数据库存储UTC时间,显示时转换为用户时区。或者统一用北京时间存储和显示(国内应用)。
  3. 用DateTime类:优先用DateTime类,而不是date()、strtotime()等过程式函数。DateTime更强大、更可靠、更易读。
  4. 用标准格式:日期用Y-m-d,时间用Y-m-d H:i:s,避免用模糊格式。
  5. 验证日期:用户输入的日期,用checkdate()或DateTime::createFromFormat()验证合法性。
  6. 避免2038年问题:用64位系统,数据库用DATETIME,用DateTime类。
  7. 缓存时间戳:在一次请求中,多次用到当前时间时,缓存time()的结果,避免不一致。
  8. 友好时间显示:文章、评论等的发布时间,用"x分钟前"、"x小时前"等友好显示,比具体时间更直观。

总结

PHP日期和时间处理,是开发中非常常用但容易出错的技能。

核心要点:

  1. 基础函数:time()(时间戳)、date()(格式化)、strtotime()(文本转时间戳)、mktime()(指定时间转时间戳)、checkdate()(验证日期)
  2. 时区处理:datedefaulttimezone_set()设置时区,DateTimeZone处理时区转换
  3. DateTime类(推荐):创建、格式化、设置、计算(DateInterval)、比较(diff)、修改(modify)
  4. 常用操作:今天/昨天/明天、本周/上周/下周、本月/上月/下月、闰年判断、天数差、倒计时、友好时间显示
  5. 常见坑:时区问题、2038年问题、strtotime模糊解析、月份加减边界、夏令时、微秒处理、数据库时间类型
  6. 最佳实践:统一时区、存储用UTC、用DateTime类、用标准格式、验证日期、避免2038问题、缓存时间戳、友好时间显示

日期和时间处理,看起来简单,但细节很多,坑也很多。掌握DateTime类,注意时区和边界问题,就能避免大部分错误。希望这篇文章能帮你更好地处理PHP中的日期和时间。