CakePHP3のChronosを使ってカレンダーを作ってみる


自作アプリ内でカレンダーを表示したいなと思い、勉強がてら作ることにした。

前提

CakePHP3

Chronosクラスを使う

ChronosクラスとはCakePHP3.2から追加された日付時刻を扱う為のクラスです。
詳しくは公式ドキュメントを参照。
https://book.cakephp.org/3.0/ja/chronos.html

今回はChronosの下記メソッド、プロパティを使っていきます。
メソッド
・startOfMonth
・endOfMonth
・addDay
・format
プロパティ
・day
・dayOfWeek

実装

まず下記のような感じでコンポーネントにメソッドを追加しました。

CalendarComponent.php


/**
 * 指定した月の日付をリストで返す。デフォルトは当月
 * @param int $addMonth 当月から見て、何ヶ月前(後)かを数字で渡す
 * @return array $daysOfMonth 対象となる月の日付のリスト(Chronosオブジェクトのリスト)を返す。
 *                            デフォルトは当月のリストを返す。
 */
public function getDaysOfMonth(int $addMonth = 0) {
    $daysOfMonth = [];
    $time = Chronos::now()->startOfMonth();
    if ($addMonth !== 0) {
        $time = $time->addMonths($addMonth);
    }
    $start = $time->startOfMonth()->day;
    $end = $time->endOfMonth()->day;

    for($i = $start; $start <= $end; $start++) {
        $daysOfMonth[] = $time;
        $time = $time->addDay();
    }
    return $daysOfMonth;
}


  1. Chronos::now()で現在時刻のオブジェクトを作成
  2. startOfMonthでオブジェクトの時刻を月始めに設定(後でaddDayしていくので)
  3. startOfMonthのdayプロパティで初日取得
  4. endOfMonthのdayプロパティで月末取得
  5. 後は初日から月末までaddDay()して、一日毎に配列に詰めてreturn

returnしたリストはコントローラーで受け取る
下記コントローラー

CalendarController.php
public function index() {
  // 月の日付リストを取得
  $daysOfMonth = $this->Calendar->getDaysOfMonth(0);

  // 画面にセット
  $this->set(compact('daysOfMonth'));
}

セットしてView側で表示

Template/Calendar/index.ctp
<table class="calendar">
  <thead>
    <?=$this->Html->tableHeaders(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])?>
  </thead>
  <tbody>
    <?php foreach($daysOfMonth as $day): ?>
      <?php // 初日、もしくは週始めは行を作成 ?>
      <?php if ($day->day === $day->startOfMonth() || $day->dayOfWeek === 7): ?>
        <tr>
      <?php endif ?>

      <?php // 空白埋め ?>
      <?php if ($day->day === 1 && $day->dayOfWeek !== 7): ?>
        <?php for($i = 0; $i < $day->dayOfWeek; $i++):?>
          <td></td>
        <?php endfor ?>
      <?php endif ?>

      <?php // ここに日付やら予定やら表示 ?>
      <td>
        <?= $day->format('n/j'); ?>
      </td>

      <?php // 週終は行を閉じる ?>
      <?php if ($day->dayOfWeek === 6): ?>
        </tr>
      <?php endif ?>
    <?php endforeach ?>
  </tbody>
</table>

今回は一般的な日曜日始まり、土曜終わりのカレンダーを想定
なので、いい感じに出力するように曜日から計算して、空白セルを作ったり、一週間毎に行が切り替わるように週始めで<tr>吐いて、週終わりに<tr>を閉じるなどの操作を行なっている。曜日判断はdayOfWeekで。1が月曜。7が日曜
日付の出力はformatを使って、いい感じにフォーマット。

そんな感じで当月を上手く表示できたところで、先月や来月のカレンダーに移動したいなと思ったので、
下記のようにパラメータを持たせたリンクを貼ることにした。

Template/Calendar/index.ctp
<div class="">
    <a class="previous" href="/my_dev_app/Calendar/index?add=<?=($addMonth-1)?>"><< <?=$daysOfMonth[0]->addMonths(-1)->format('Y年n月')?></a>
    <h2><?= $daysOfMonth[0]->format('Y年n月のカレンダー') ?></h2>
    <a class="next" href="/my_dev_app/Calendar/index?add=<?=($addMonth+1)?>"><?=$daysOfMonth[0]->addMonths(1)->format('Y年n月')?> >></a>
</div>

持たせたパラメータはコントローラーで受け取る

CalendarController.php
public function index() {
  // クエリ取得
  $addMonth = $this->request->query('add') ?? 0;

  // パラメータ形式チェック
  if ($addMonth !== 0) {
    $pattern = '/^[-][1-9]+[0-9]*$|^[1-9]+[0-9]*$/';
    if (!preg_match($pattern, $addMonth)) {
        // 不正なパラメータの場合は、パラメータを消してリダイレクト
        return $this->redirect(
            ['controller' => 'Calendar', 'action' => 'index']
        );
    }
  }

  // 月の日付リストを取得
  $daysOfMonth = $this->Calendar->getDaysOfMonth($addMonth);

  // 画面にセット
  $this->set(compact('daysOfMonth', 'addMonth'));
}

こんな感じでコントローラーでパラメータ受け取って、一応形式チェックして、不正ならパラメータなしの当月にリダイレクト。
コンポーネントに実装したgetDaysOfMonthメソッドは、なんらかの数字を渡すと当月から計算した月のリストが返ってくるようになっている。プラスなら未来、マイナスなら過去。

CalendarComponent.php
/**
 * 指定した月の日付をリストで返す。デフォルトは当月
 * @param int $addMonth 当月から見て、何ヶ月前(後)かを数字で渡す
 * @return array $daysOfMonth 対象となる月の日付のリスト(Chronosオブジェクトのリスト)を返す。
 *                            デフォルトは当月のリストを返す。
 */
public function getDaysOfMonth(int $addMonth = 0) {

見た目をいじる

h2 {
  font-size: 2.0rem;
  margin: 50px auto 50px auto;
  text-align: center;
}
.previous, .next{
  position: absolute;
  right: 20px;
  top: 110px;
  width: 120px;
}
.previous {
  left: 20px;
}
.calendar th,
.calendar td {
  font-size: 1.25rem;
  text-align: center;
}
.calendar td {
  border: 1px solid #ebebec;
  height: 80px;
  vertical-align: middle;
}
.calendar tr th:nth-child(1),
.calendar tr td:nth-child(1)
{
  color: red;
}
.calendar tr th:nth-child(7),
.calendar tr td:nth-child(7)
{
  color: blue;
}

完成形

最終形はこんな感じになった。

修正したいこと

・祝日も表示して、祝日は赤くしたい(GoogleカレンダーAPI使えばいけそう)
・Viewで表示するものなので、そもそもHelperとして実装した方がよかったかも?

【修正 2018/02/05】
・正規表現が間違っていたのを修正
・ファイル名修正
・クエリの取得方法変更