PHPでカレンダーを作ってみる①
当月分のカレンダー

PHPにはDateTimeクラスという日付や時刻の演算・整形を行うためのクラスがあります。

このDateTimeクラスを使うことで、現在の日時から起算して〇時間後とか〇日後などの日付を簡単に取得することができます。

また、DateTimeクラスを使うことで自分でカレンダーを作ることもできます。

今日はPHPを使って、DateTimeクラスの練習がてら以下のようなカレンダーを作ってみたいと思います。

作成するのは今月分だけが表示されるカレンダーです。

どうやって作るか

カレンダーというのは一般的に以下のような形をしています。

カレンダー

月初が日曜日以外の時は、前月の最終週が今月の1週目に食い込んできます。

そして月末が土曜日以外の時は来月の1週目の土曜日までが今月の最終週に食い込んできます。

このことから

カレンダーの始まりとなる日付と終わりとなる日付のDateTimeインスタンスを作成し、それを利用してその期間内の1日ごとのDateTimeインスタンスを作成し、それを順次表示していけば1か月分のカレンダーが完成することになります。

作ってみよう!

それでは実際に作ってみます。

ゆんつ
やるぞ!

基礎部分

まずは今現在のDateTimeインスタンスを作成します。

$today = new DateTime();

カレンダーでは本日の日付は太字で表示します。

カレンダー

この$todayはのちほど、カレンダーに表示する日付が本日の日付かどうかを判定して太字のスタイルを適用するのに用います。

そして次に今月の初日のDateTimeインスタンスを作成。

$start_day = new DateTime('first day of this month');

DateTimeクラスは「first day of this month」などのわかりやすい言葉を引数に使ってDateTimeインスタンスを作成することができます。

いろんな書き方があるので詳しくは公式サイトをご覧ください。

このインスタンスからカレンダーの見出しとなる西暦と月を取得しておきます。

$year_month = $start_day->format('Y-m');

DateTimeクラスはformatを使うと自分の好きな表記で日付や時刻を取得することができます。

引数のYは西暦、mは月です。

これにより$year_monthには「年-月(例:2020-9)」という文字列が代入されました。

カレンダーの最初の日付けを作成する

カレンダーの最初の日付けを作成します。

カレンダー

DateTimeインスタンスには曜日に関する情報が入っています。

先ほど作成したインスタンス($start_day)を使って月初の曜日を求めることでカレンダーに食い込んでくる前月部分の日数が求められます。

月初の曜日を求めてみましょう。

$w = $start_day->format('w');

formatの引数の「w」は曜日を数値で取得するためのものです。

これにより日曜日を0~土曜日を6として曜日を数値として取得できます。

例えば2020年9月1日は火曜日なので「2」となります。

これによりカレンダーの1週目に食い込んでくる、前月の日数が「日曜日」と「月曜日」の2日であることが判明します。

よって、カレンダーの開始日を今月の月初から2日さかのぼります。

カレンダー
$start_day->modify('-' . $w . ' day');

modifyを使うとDateTimeインスタンスのタイムスタンプを変更することができます。

modify('-' . $w . ' day')はmodify('-2 day')となり、例えば9月を例にすれば$start_dayは2020年9月1日の2日前の2020年8月30日に変更されます。

これで今月のカレンダーのスタート時点の日時が作成できました!

カレンダーの最後の日付けの1日あとを作成する

カレンダーの最後の日付の1日あとを作成します。

ゆんつ
1日あとといいうのがミソです

やりかたは先ほどやったカレンダーの最初の日にちの作成と変わりません。

まず今月末日のDateTimeインスタンスを作成します。

$end_day = new DateTime('last day of this month');

末日の曜日を数値で求めます。

$w = $end_day->format('w');

例えば2020年9月30日の場合は水曜日なので数値にすると3になります。

カレンダーは土曜日まで表示されるので、土曜日を表す数値の6から月末の曜日に対応する数値を引けば今月のカレンダーに食い込んでくる来月の1週目の日数が判明します。

$w = 6 - $w + 1;

6から月末の曜日に対応する数値を引いて1を加算しました。

この「+1」は、この後のDatePeriodクラスという期間を算出してくれるクラスを使うための調整のためのものです。

詳しい理由はDatePeriodの工程で書きます。

今月末日を先ほどもとめた数値の分だけ先に進めます。

$end_day->modify('+' . $w . ' day');

これにより、例えば2020年9月を対象とした場合、$end_dayは9月末日から4日(6-3+1)進み10月最初の日曜日に変更されたことになります。

カレンダー

カレンダーに表示される期間のDateTimeインスタンスを作成する

DatePeriodは

  • 第1引数に開始日
  • 第2引数に間隔
  • 第3引数に終了日

を設定することで、その期間内のDateTimeインスタンスを作成してくれます。

この際にポイントとなるのが終了日。

第3引数で設定する終了日は当日を含みません!

終了日が含まれないので、第3引数の終了日は取得したい期間の終了日の「+1日」してあげないと終了日当日が除外されてしまいます。

先ほどの「+1」は、このDatePeriodの特性を考慮した「+1」だったのです。

これにより

$period = new DatePeriod(
  $start_day,
  new DateInterval('P1D'),
  $end_day
);

でカレンダーに表示する期間のDateTimeインスタンスを丸々作成することができます。

これでカレンダーに表示するDateTimeインスタンスの作成は完了です。

表示のための処理

HTMLに表示するための処理を書いていきます。

まずは$bodyという変数を準備して、ここにタグをどんどん代入していくことにします。

$body = '';

カレンダーに必要なDateTimeインスタンスが入っている$periodをforeachで回していきます。

foreach ($period as $day) {
  //当月以外の日付はgreyクラスを付与してCSSで色をグレーにする
  $grey_class = $day->format('Y-m') === $year_month ? '' : 'grey';
  
  //本日にはtodayクラスを付与してCSSで数字の見た目を変える
  $today_class = $day->format('Y-m-d') === $today->format('Y-m-d') ? 'today' : '';
  
  //その曜日が日曜日なら<tr>タグを挿入する
  if ($day->format('w') == 0) {
    $body .= '<tr>';
  }
  
  //sprintfを使って整形しながらhtml部分を作成する
  $body .= sprintf(
    '<td class="youbi_%d %s %s">%d</td>',
    $day->format('w'),
    $today_class,
    $grey_class,
    $day->format('d')
  );
  
  //その曜日が土曜日なら</tr>タグを挿入する
  if ($day->format('w') == 6) {
    $body .= '</tr>';
  }
}

当月以外の日付は文字色をグレイにしたいので、オブジェクトの年月を比較して違う場合にはgreyクラスを付与するためのgreyという文字列を$greyに代入しています。

本日の日付は太字にしたいので、本日のDateTimeオブジェクトと比較して同じ日付をもつループ順になったばあいは、$todayにtodayという文字列を代入しています。

そしてsprintfを使って日付を表示するためのtdタグを整形。

%がプレイスホルダとなっており、第2引数~第5引数に対応しています。

ループの最後に土曜日の判定を行い、もし土曜日ならを</tr>挿入して1行の終わりとします。

これを繰り返してループが終了すると、日付部分のタグが完成します。

こうして作成したタグをHTMLの適当な場所でechoして表示してあげればカレンダーは完成です。

完成したカレンダーのコード

上記のコードを1つにまとめた完成形が以下のコードです。

<?php
//現在のDateTimeインスタンスを作成
$today = new DateTime();

//月初のDateTimeインスタンスを作成
$start_day = new DateTime('first day of this month');

//月初の西暦と月を取得
$year_month = $start_day->format('Y-m');

//月初の曜日を数値で取得
$w = $start_day->format('w');

//月初をカレンダーの開始日に変更する
$start_day->modify('-' . $w . ' day');

//月末のDateTimeインスタンスを作成
$end_day = new DateTime('last day of this month');

//カレンダーの終了日を取得するため月末の曜日を数値で取得
$w = $end_day->format('w');

//土曜日を数値にすると6。そこから月末の曜日に対応する数を引いてやれば、カレンダー末尾に追加すべき日数が判明する。
//+1しているのはDatePeriodの特性を考慮するため
$w = 6 - $w + 1;

//月末をカレンダーの終了日の翌日に変更する
$end_day->modify('+' . $w . ' day');

//カレンダーに表示する期間のインスタンスを作成する
$period = new DatePeriod(
  $start_day,
  new DateInterval('P1D'),
  $end_day
);

//htmlに描写するための変数
$body = '';

foreach ($period as $day) {
  //当月以外の日付はgreyクラスを付与してCSSで色をグレーにする
  $grey_class = $day->format('Y-m') === $year_month ? '' : 'grey';
  
  //本日にはtodayクラスを付与してCSSで数字の見た目を変える
  $today_class = $day->format('Y-m-d') === $today->format('Y-m-d') ? 'today' : '';
  
  //その曜日が日曜日なら<tr>タグを挿入する
  if ($day->format('w') == 0) {
    $body .= '<tr>';
  }
  
  //sprintfを使って整形しながらhtml部分を作成する
  $body .= sprintf(
    '<td class="youbi_%d %s %s">%d</td>',
    $day->format('w'),
    $today_class,
    $grey_class,
    $day->format('d')
  );
  
  //その曜日が土曜日なら</tr>タグを挿入する
  if ($day->format('w') == 6) {
    $body .= '</tr>';
  }
}

?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Calendar</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <table>
    <thead>
      <tr>
        <th><a href="">&laquo;</a></th>
        <th colspan="5"><?php echo $year_month ?></th>
        <th><a href="">&raquo;</a></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>日</td>
        <td>月</td>
        <td>火</td>
        <td>水</td>
        <td>木</td>
        <td>金</td>
        <td>土</td>
      </tr>
      <?php echo $body ?>
    </tbody>
    <tfoot>
      <tr>
        <th colspan="7"><a href="">today</a></th>
      </tr>
    </tfoot>
  </table>
</body>
</html>

CSS

CSSは以下の通りです。

body {
  font-family: Arial, sans-serif;
  font-size: 14px;
}

a {
  text-decoration: none;
}

table {
  margin: 15px auto;
  border: 1px solid #ddd;
  border-collapse: collapse;
}

th {
  background: #eee;
}

th, td {
  text-align: center;
  padding: 7px;
}

.youbi_0 {
  color: red;
}

.youbi_6 {
  color: blue;
}

.today {
  font-weight: bold;
}

.grey {
  color: #dedede;
}

デモもあります

上記のコードのデモページを作成しました。

カレンダーデモ

当月分しか表示されないシンプルなカレンダーです。

次回に続きます

今回作成したカレンダーは当月分だけの表示です。

WEBで見かけるカレンダーは先月、来月のリンクがついていて、それをクリックすると先月、来月のカレンダーが表示されるようになっていることが多いです。

今回作成したカレンダーにも見た目のリンクはついているんですが、まだ機能は実装していません。

カレンダー

というわけで次回は、先月、来月のリンクをクリックすると該当する月のカレンダーが表示されるようにしたいと思います。

それでは、またー。