フレームワークや外部ライブラリ使用禁止のプロジェクトでは、日付計算でCarbonのような超絶便利なものが使えず、PHP組込のDateTimeオブジェクトを使うことになります。
これがまた使いにくいので、もう少し使いやすくしてみましょう。

どこまでやるのか
下の1~3が出来るだけでかなり使い勝手が良くなります。
Carbon程の機能は無理ですが、Carbonが無くても色々と簡単に日付操作ができるようになります。
- 1.デフォルトタイムゾーンを自動設定できるようにする
- 2.簡単に日付を進められるようにする
- 3.簡単に日付を遡れるようにする
- 4.有効な日付の文字列か判定できるようにする
- 5.有効な相対日付の文字列か判定できるようにする
前提条件
- PHP8.2以降
- Ubuntu上で作業(多分プラットフォームは無関係)
DateTimeのラッパーを作成する
次のような感じで、DateTimeオブジェクトを継承したラッパークラスを作成します。
※Carbon自体が DateTimeオブジェクトのラッパーです。
<?php
namespace MyLib;
use DateTime as DateTimeBase;
use DateInterval;
use DateTimeZone;
/**
* DateTimeラッパー
* Asia/Tokyo をデフォルトタイムゾーンとすることが目的
*/
class DateTime extends DateTimeBase
{
public const DEFAULT_TIMEZONE_STRING = "Asia/Tokyo";
public function __construct(
public string $datetimeString = "now",
public DateTimeZone|null $timezone = null,
) {
if (is_null($this->timezone)) {
$this->timezone = new DateTimeZone(static::DEFAULT_TIMEZONE_STRING);
}
parent::__construct($this->datetimeString, $this->timezone);
}
/**
* 相対的に進めた日付のDateTimeオブジェクトを返す
* サポートする日付と時刻の書式は次のURLの「相対的な書式」を参照
* https://www.php.net/manual/ja/datetime.formats.php#datetime.formats.relative
*/
public function forward(string $relativeDateTimeString): self
{
$interval = DateInterval::createFromDateString($relativeDateTimeString);
return $this->add($interval);
}
/**
* 相対的に遡った日付のDateTimeオブジェクトを返す
* サポートする日付と時刻の書式は次のURLの「相対的な書式」を参照
* https://www.php.net/manual/ja/datetime.formats.php#datetime.formats.relative
*/
public function backward(string $relativeDateTimeString): self
{
$interval = DateInterval::createFromDateString($relativeDateTimeString);
return $this->sub($interval);
}
public static function isValidDateString(string $dateTimeString): bool
{
try {
new DateTime($dateTimeString);
return true;
} catch (\Throwable $e) {
return false;
}
}
public static function isValidRelativeDateString(string $dateTimeString): bool
{
try {
DateInterval::createFromDateString($dateTimeString);
return true;
} catch (\Throwable $e) {
return false;
}
}
}
使ってみましょう
まずは、PHP組込のDateTimeオブジェクトとの違いを見てみましょう。
<?php
require __DIR__ . "/DateTime.php";
use DateTime as DateTimeOrigin;
use MyLib\DateTime;
var_dump(new DateTimeOrigin, new DateTime);
実行すると次のようになります。

PHP組込のDateTimeオブジェクトは日付・タイムゾーンがUTCなのに対し、
ラッパーの方はAsia/Tokyoになっています。
日付を進めたり遡ったり
多分、日付処理で頻繁に使いたい機能だと思います。
日付を進める場合は、forward() メソッド、遡る場合は backward() メソッドを使います。
両メソッドの引数は、DateTimeオブジェクトがサポートする「相対的な書式」の文字列です。

▼使用例
<?php
require __DIR__ . "/DateTime.php";
use MyLib\DateTime;
echo (new DateTime)->forward("2 days")->format("Y/m/d") . PHP_EOL;
echo (new DateTime)->backward("3 days")->format("Y/m/d") . PHP_EOL;
echo (new DateTime("June 1st, 2024"))->forward("2 months")->format("Y/m/d") . PHP_EOL;
echo (new DateTime("monday this week"))->backward("3 days")->format("Y/m/d") . PHP_EOL;
echo (new DateTime("2024/7/4"))->forward("40 weeks")->format("Y/m/d") . PHP_EOL;
echo (new DateTime("2028-3-1"))->backward("3 days")->format("Y/m/d") . PHP_EOL;
▼実行結果:

特に、最終行の「2028年3月1日」の前日は閏年の「2028年2月29日」なので、その3日前は「2028年2月27日」になります。
DateTimeオブジェクトの方で閏年の判定はしているので、いちいち閏年の計算なんてしなくてもいいんですよね。
どうしても閏年かどうかの判定がしたければ、「IntlGregorianCalendar::isLeapYear(int $year)」を使うか、
または次のように、このDateTimeラッパーにメソッド追加して、「その年の2月末日が29」かどうかを判定すれば済みます。または、format(“L”) で 1なら閏年、0なら閏年でないとすれば済みます。
public function isLeapYear(int $year): bool
{
return (new static("last day of February " . $year))->format("d") === "29";
//return (new static("last day of February " . $year))->format("L") === "1";
}
そうすれば、
(new DateTime)->isLeapYear(2028);
(new DateTime)->isLeapYear(2029);
のような使い方ができます。
が、個人的には好まない機能なのでこのDateTimeラッパーには組み込みません。
あと、書いてから気づきましたが、この使い方は違いますね。。本来は
(new DateTime("2028/05/01"))->isLeapYear();
のような感じでしょう。
有効な日付文字列か判定
「isValidDateString()」と「isValidRelativeDateString()」は、日付文字列のバリデーションのようなものですが、設置した目的は DateTimeラッパーで受け付けられる文字列かどうかの判定です。
DBに格納するデータのバリデーションとしては使うものではありません。
▼使用例:
<?php
require __DIR__ . "/DateTime.php";
use MyLib\DateTime;
echo ((new DateTime)->isValidDateString("4th july 2025") ? "Valid" : "Invalid") . PHP_EOL;
echo ((new DateTime)->isValidDateString("おととい") ? "Valid" : "Invalid") . PHP_EOL;
echo ((new DateTime)->isValidRelativeDateString("5 days ago") ? "Valid" : "Invalid") . PHP_EOL;
echo ((new DateTime)->isValidRelativeDateString("2025/06/05") ? "Valid" : "Invalid") . PHP_EOL;
▼実行結果:

因みに、DBに格納するデータのバリデーションでは、次のように正規表現チェックを加える必要があるでしょう。
public function validateDate(string $key, string $name, mixed $value): void
{
if (is_null($value)) {
$this->setError($key, "{$name}は必須です。");
} elseif (! is_string($value)) {
$this->setError($key, "{$name}は文字列でなければなりません。");
} else {
$pattern1 = "[0-9]{4}\/(0[1-9]|1[0-2])\/(0[0-9]|[12][0-9]|3[01])"; // ゼロ埋め スラッシュ区切り
$pattern2 = "[0-9]{4}\/([1-9]|1[0-2])\/([1-9]|[12][0-9]|3[01])"; // ゼロ埋めなし スラッシュ区切り
$pattern3 = "[0-9]{4}\-(0[1-9]|1[0-2])\-(0[0-9]|[12][0-9]|3[01])"; // ゼロ埋め ハイフン区切り
$pattern4 = "[0-9]{4}\-([1-9]|1[0-2])\-([1-9]|[12][0-9]|3[01])"; // ゼロ埋めなし ハイフン区切り
$pattern = "/^" . $pattern1 . "|" . $pattern2 . "|" . $pattern3 . "|" . $pattern4 . "$/";
if (! preg_match($pattern, $value)) {
$this->setError($key, "{$name}の書式は YYYY-mm-dd または YYYY/mm/dd でなければなりません。");
}
}
}
それか、或いはモデル側で受け取るデータはこのDateTimeオブジェクトラッパーだけを許容して、モデル側でデータバインドする際に format() メソッドで整形して渡してしまえばOKでしょう。多分、その方が安全な設計になると思います。
カレンダーでも作ってみますか
せっかくなので、このDateTimeラッパーを使って簡単なアプリケーションでも作ってみましょう。
年、月をそれぞれ数値で指定すると、その月のカレンダーを出力してくれるクラスでも作ってみます。
▼「Calendar.php」
<?php
namespace MyLib;
require __DIR__ . "/DateTime.php";
use MyLib\DateTime;
class Calendar
{
public static array $weekdays = ["日", "月", "火", "水", "木", "金", "土"];
public static function getDays(int $year, int $month): array
{
$datetimeFirstDay = new DateTime($year . "/" . $month . "/1");
$datetimeLastDay = (new DateTime($year . "/" . $month . "/1"))->forward("1 month")->backward("1 day");
$lastDay = (int) $datetimeLastDay->format("d");
$firstWeekday = (int) $datetimeFirstDay->format("w");
$lastWeekday = (int) $datetimeLastDay->format("w");
return array_merge(
$firstWeekday > 0 ? array_fill(0, $firstWeekday, null) : [],
range(1, $lastDay),
$lastWeekday < 6 ? array_fill(0, 6 - $lastWeekday, null) : []
);
}
public static function display(int $year, int $month): void
{
echo (new DateTime($year . "/" . $month . "/1"))->format("Y年m月") . PHP_EOL;
echo " " . implode(" ", static::$weekdays) . PHP_EOL;
foreach (static::getDays($year, $month) as $i => $day) {
$dayString = is_null($day) ? " - " : sprintf(" %2s ", $day);
echo $dayString . ($i % 7 === 6 ? PHP_EOL : "");
}
}
}
▼呼出側「display_calendar.php」
<?php
require __DIR__ . "/Calendar.php";
use MyLib\Calendar;
Calendar::display(2025, 6);
Calendar::display(2025, 7);
Calendar::display(2025, 8);
▼実行結果:

出力箇所をHTML出力にすれば、WEBアプリとしてカレンダー出力ができます。
やっていることは簡単で、出力制御を簡単にするために、
1か月表示用の配列を、日曜スタートで最終週の土曜の分まで作成します。
その月の1日が何曜日かを数値で取得(日曜=0~土曜=6)し、
1日の前の分を null 要素で埋めます。
最終週も同様に、最終日の後ろを土曜の分まで null 要素で埋めます。
あとは、生成された配列をループ表示し、土曜の時だけ改行コードを出力するだけです。
DateTimeラッパーを作って、日付の前後を簡単にできるようにするだけで、
かなり楽に日付処理ができるようになりました。
以上です。
コメント