カレンダーアプリの作成(#1) #Laravel基礎 #Laravelの教科書
本稿ではカレンダーアプリを作成することで、複雑なViewや複雑なモデルをどのように実現していくかについて学んでいきます。
掲示板やお知らせのアプリでは比較的シンプルなHTMLを作成するだけでしたが、カレンダーはテーブルタグを使って作ったとしても複雑にならざるを得ません。
そのため初心者向けの教材でもしばしばとりあげられます。
Laravelではカレンダーのような複雑なHTMLはどのように作っていくのが良いのか、どのように作れば機能拡張がしやすいのか、本稿を通して学んでいけたらと考えています。
Laravel基礎としていますが、オブジェクト指向プログラミングが取り入れられているため、読み解くことに少し力がいるかもしれません。
# プロジェクトの準備
まずはプロジェクトの作成から順に、準備を行います。
今回は「calendar_app」というアプリを作成していきます。
composer create-project --prefer-dist laravel/laravel calendar_app
cd calendar_app
データーベースを利用するので、.envを設定しMySQLの接続情報を設定しましょう。設定方法は過去の記事を参考にしてください。
<参考資料>
.envの編集が終われば、マイグレーションを実行してDBへの接続が問題ないか確認します。
php artisan migrate
bootstrapを使ったテンプレートを利用するので、laravel/uiをインストールしておきます。
composer require laravel/ui
php artisan ui vue --auth
npm install && npm run dev
こちらのコマンドが実行できない場合は下記記事を参考にセットアップしてください。
<参考資料>
laravel/uiのセットアップが終われば、サーバーを実行してブラウザで動作確認をしましょう。
php artisan serve
# カレンダーをどのように作るか
一般的にカレンダーをPHPで作る場合、次のような形で行います。
1. 1日の曜日を調べて、余白を出力
2. 日曜日まで出力
3. 日曜日まで出力したら次の週に折り返すためのタグを出力
4. 月曜日〜日曜日まで出力
5. 最終日まで4を繰り返す
6. 月末の曜日を調べて、余白を出力
PHPでこれを書くならまだしも、Laravelのbladeテンプレートでこれを実現しようとするとちょっと難しいと感じるのではないでしょうか。
LaravelではbladeテンプレートではなくPHPでテンプレートを作ることもできます。また、ControllerでHTMLを組み立てるといったアプローチもできます。
しかしながらControllerで作るとControllerのコードが長くなり、メンテナンス性が下がります。
PHPでテンプレートを作るとbladeテンプレートとPHPのコードが混ざることでの煩雑さが発生します。
シンプルに解決するため、このような時は「カレンダーを出力するためのクラス」を作成するというアプローチがあります。
# CalendarViewクラスの作成
カレンダーを出力するためのCalendarViewクラスを作成していきます。
設置場所は「app/Calendar/CalendarView.php」に作成します。これはLaravelの機能で作るものではない自作のクラスなので、コマンドなどではなく手動で作成します。
app/Calendar/CalendarView.php
<?php
namespace App\Calendar;
use Carbon\Carbon;
class CalendarView {
private $carbon;
function __construct($date){
$this->carbon = new Carbon($date);
}
/**
* タイトル
*/
public function getTitle(){
return $this->carbon->format('Y年n月');
}
/**
* カレンダーを出力する
*/
function render(){
$html = [];
$html[] = '<div class="calendar">';
$html[] = '<table class="table">';
$html[] = '<thead>';
$html[] = '<tr>';
$html[] = '<th>月</th>';
$html[] = '<th>火</th>';
$html[] = '<th>水</th>';
$html[] = '<th>木</th>';
$html[] = '<th>金</th>';
$html[] = '<th>土</th>';
$html[] = '<th>日</th>';
$html[] = '</tr>';
$html[] = '</thead>';
$html[] = '</table>';
$html[] = '</div>';
return implode("", $html);
}
}
namespace App\Calendar;
設置場所がapp/CalendarなのでnamespaceをApp\Calendarで設定します。
use Carbon\Carbon;
CarbonはLaravelで日付を扱う時に利用可能な便利なライブラリです。色々な機能があるためすべて解説できませんが、気になる方は公式サイトのドキュメントなどを見てください。
CalendarViewクラスは例えば次のように呼び出して使うことを想定しています。
$calendar = new CalendarView("2020-07")
$next_calendar = new CalendarView("2020-08")
利用方法に合わせて、コンストラクタで受け取った日付を元にCarbonオブジェクトを作成しています。
function __construct($date){
$this->carbon = new Carbon($date);
}
function getTitle(){ ... }
function render(){ ... }
カレンダー全体をrender()で作成するのではなく、タイトルとそれ以外でわける形式にするとテンプレートからの利用がしやすいです。
まずは大枠だけ作成し、週部分のループは後から実装していきます。
# コントローラーの作成、ルーティングの設定
カレンダーを表示するためのCalendarControllerをmake:controllerコマンドを利用して作成します。
php artisan make:controller CalendarController
作成したコントローラーを次のように編集していきます。
app/Http/Controllers/CalendarController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Calendar\CalendarView;
class CalendarController extends Controller
{
public function show(){
$calendar = new CalendarView(time());
return view('calendar', [
"calendar" => $calendar
]);
}
}
use App\Calendar\CalendarView;
CalendarViewをコントローラーから使うためuse句を忘れずに書きます。
$calendar = new CalendarView(time());
time()を使って現在時刻を渡し、今月のカレンダーを用意します。
return view('calendar', [
"calendar" => $calendar
]);
Viewに作成したCalendarViewオブジェクトを渡します。
作成したCalendarControllerをトップページに表示するため、ルーティングを編集します。
routes/web.php
Route::get('/', 'CalendarController@show');
※ 標準のwelcomeの指定は削除してください
# Viewの作成
次に、Controllerで指定したcalendar.blade.phpを作成していきます。
resources/views/calendar.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ $calendar->getTitle() }}</div>
<div class="card-body">
{!! $calendar->render() !!}
</div>
</div>
</div>
</div>
</div>
@endsection
{{ $calendar->getTitle() }}
{!! $calendar->render() !!}
CalendarViewの各関数を利用して、タイトルとカレンダー本体をわけて出力します。
カレンダーを出力するためのクラスを作成することで、コントローラーもテンプレートもとても綺麗に書くことができます。
Viewの作成が終わったらブラウザでトップページを表示して確認しましょう。
続いて、現在出力できていない週部分を作っていきましょう。
# CalendarViewの週部分を作る
週の部分をどのように作るかですが、PHPでHTML全体を作ることも出来ますが、ここではしっかりクラスを分けて対応していきましょう。
用途に合わせて次の3個のクラスを作成していきます。
・週を出力するためのCalendarWeekクラス
・日を出力するためのCalendarWeekDayクラス
・前の月、次の月の余白を出力するためのCalendarWeekBlankDayクラス
CalendarViewを修正し、週の情報を取得するためのgetWeeks()関数を作成します。
app/Calendar/CalendarView.php
class CalendarView {
protected function getWeeks(){
$weeks = [];
//初日
$firstDay = $this->carbon->copy()->firstOfMonth();
//月末まで
$lastDay = $this->carbon->copy()->lastOfMonth();
//1週目
$week = new CalendarWeek($firstDay->copy());
$weeks[] = $week;
//作業用の日
$tmpDay = $firstDay->copy()->addDay(7)->startOfWeek();
//月末までループさせる
while($tmpDay->lte($lastDay)){
//週カレンダーViewを作成する
$week = new CalendarWeek($tmpDay, count($weeks));
$weeks[] = $week;
//次の週=+7日する
$tmpDay->addDay(7);
}
return $weeks;
}
}
getWeeks()関数は週カレンダーを一月分用意した配列$weeksを返却するのが目的です。
//初日
$firstDay = $this->carbon->copy()->firstOfMonth();
//月末まで
$lastDay = $this->carbon->copy()->lastOfMonth();
月の開始日と、末尾を取得する処理です。Carbonを使うと、便利なメソッドを使って日付の操作を行うことができます。copy()を間に挟むことで日付操作をしても影響が出ないようにしています。
//1週目
$week = new CalendarWeek($firstDay->copy());
$weeks[] = $week;
一週目、1日を指定してCalendarWeekを作成します。
//作業用の日
$tmpDay = $firstDay->copy()->addDay(7)->startOfWeek();
作業用の日を作成します。翌週の月曜日が欲しいので、+7日した後、週の開始日に移動する記述です。
その後、月末までループしながら一週毎にCalendarWeekを作成していきます。
while($tmpDay->lte($lastDay)){
...
$tmpDay->addDay(7);
}
一週毎に+7日することで$tmpDayを翌週に移動しています。
$week = new CalendarWeek($tmpDay, count($weeks));
$weeks[] = $week;
CalendarWeekを作成する際に、第2引数でcount($weeks)を指定しています。これは何週目かを週カレンダーオブジェクトに伝えるために設置しています。
少しわかりにくいですが、一回目のループは既に1週目を追加するので1、次のループでは2と順に増えていきます。
# カレンダー週クラスの作成
カレンダー週クラスは「その週のカレンダーを出力する」ためのクラスです。
週の開始日〜終了日までを作成するgetDays()関数と、HTMLを表示する時に後からCSSを当てることが出来るようにクラス名を出力するgetClassName()関数を持ちます。
app/Calendar/CalendarWeek.php
<?php
namespace App\Calendar;
use Carbon\Carbon;
class CalendarWeek {
protected $carbon;
protected $index = 0;
function __construct($date, $index = 0){
$this->carbon = new Carbon($date);
$this->index = $index;
}
function getClassName(){
return "week-" . $this->index;
}
/**
* @return CalendarWeekDay[]
*/
function getDays(){
$days = [];
//開始日〜終了日
$startDay = $this->carbon->copy()->startOfWeek();
$lastDay = $this->carbon->copy()->endOfWeek();
//作業用
$tmpDay = $startDay->copy();
//月曜日〜日曜日までループ
while($tmpDay->lte($lastDay)){
//前の月、もしくは後ろの月の場合は空白を表示
if($tmpDay->month != $this->carbon->month){
$day = new CalendarWeekBlankDay($tmpDay->copy());
$days[] = $day;
$tmpDay->addDay(1);
continue;
}
//今月
$day = new CalendarWeekDay($tmpDay->copy());
$days[] = $day;
//翌日に移動
$tmpDay->addDay(1);
}
return $days;
}
}
use Carbon\Carbon;
週カレンダーでも日付操作を行うのでCarbonを利用します。
$startDay = $this->carbon->copy()->startOfWeek();
$lastDay = $this->carbon->copy()->endOfWeek();
Carbonを利用して、週の開始日〜終了日を作成します。
//作業用
$tmpDay = $startDay->copy();
//月曜日〜日曜日までループ
while($tmpDay->lte($lastDay)){
....
$tmpDay->addDay(1);
}
開始日から終了日=月曜日〜日曜日までループさせて作成しています。
if($tmpDay->month != $this->carbon->month){ }
この部分は月を比較しています。違う月の場合は前または後ろの余白なので、処理をわけています。
例えば7月第一週の場合は、6月28日〜30日までが「$tmpDay->month => 6」、7月の最終週の場合は8月1日が「$tmpDay->month => 8」となります。
違う月の場合は余白用のカレンダー日オブジェクトを追加します。
$day = new CalendarWeekBlankDay($tmpDay->copy());
$days[] = $day;
$tmpDay->addDay(1);
continue;
同じ月の場合は通常のカレンダー日オブジェクトを追加します。
$day = new CalendarWeekDay($tmpDay->copy());
$days[] = $day;
$daysは一週間=7個のオブジェクトが入っていますが、先頭の週は「CalendarWeekBlankDay, CalendarWeekBlankDay, CalendarWeekDay, CalendarWeekDay, CalendarWeekDay, CalendarWeekDay, CalendarWeekDay」のようにCalendarWeekBlankDayとCalendarWeekDayのオブジェクトが混ざっています。
オブジェクトが混在しても問題無いように日カレンダーオブジェクトを作っていきます。
# カレンダー日クラスを作成
カレンダー日クラスは「その日のカレンダーを出力する」ためのクラスです。
カレンダーの日の内部を出力するrender()関数と、HTMLを表示する時に後からCSSを当てることが出来るようにクラス名を出力するgetClassName()関数を持ちます。
app/Calendar/CalendarWeekDay.php
<?php
namespace App\Calendar;
use Carbon\Carbon;
class CalendarWeekDay {
protected $carbon;
function __construct($date){
$this->carbon = new Carbon($date);
}
function getClassName(){
return "day-" . strtolower($this->carbon->format("D"));
}
/**
* @return
*/
function render(){
return '<p class="day">' . $this->carbon->format("j"). '</p>';
}
}
function getClassName(){
return "day-" . strtolower($this->carbon->format("D"));
}
format()関数に「D」を指定すると「Sun」「Mon」などの曜日を省略形式で取得できます。
フォーマットはPHPの公式マニュアルにまとまっているので確認してみてください。
小文字に変換をしているので、日曜日はday-sun、月曜日はday-monというクラス名を出力できます。
function render(){
return '<p class="day">' . $this->carbon->format("j"). '</p>';
}
format()関数に「j」を指定すると先頭にゼロをつけない日付けを取得できます。
例えば15日であれば<p class="day">15</p>というHTMLを返します。
# 余白用日カレンダークラスの作成
余白の日は、内部の文字列を空にします。また、余白用のクラスがあると良いでしょう。
app/Calendar/CalendarWeekBlankDay.php
<?php
namespace App\Calendar;
/**
* 余白を出力するためのクラス
*/
class CalendarWeekBlankDay extends CalendarWeekDay {
function getClassName(){
return "day-blank";
}
/**
* @return
*/
function render(){
return '';
}
}
class CalendarWeekBlankDay extends CalendarWeekDay
日カレンダーをカスタマイズして、クラス名とHTMLだけ別の処理になるようなクラスを作成しています。
クラス名は「day-blank」、render()で何も出力しないように上書きしています。
このように基本の動作を作成した上でバリエーションを作る形で作っていくと仕様変更にも強い作りになります。
# 週カレンダーの組み込み
作成した週カレンダーをCalendarViewクラスに追加し、実際に表示出来るように組み込んでいきましょう。
+で書いた部分が追加部分です。
app/Calendar/CalendarView.php
class CalendarView {
/**
* カレンダーを出力する
*/
function render(){
$html = [];
$html[] = '<div class="calendar">';
$html[] = '<table class="table">';
$html[] = '<thead>';
$html[] = '<tr>';
$html[] = '<th>月</th>';
$html[] = '<th>火</th>';
$html[] = '<th>水</th>';
$html[] = '<th>木</th>';
$html[] = '<th>金</th>';
$html[] = '<th>土</th>';
$html[] = '<th>日</th>';
$html[] = '</tr>';
$html[] = '</thead>';
+ $html[] = '<tbody>';
+
+ $weeks = $this->getWeeks();
+ foreach($weeks as $week){
+ $html[] = '<tr class="'.$week->getClassName().'">';
+ $days = $week->getDays();
+ foreach($days as $day){
+ $html[] = '<td class="'.$day->getClassName().'">';
+ $html[] = $day->render();
+ $html[] = '</td>';
+ }
+ $html[] = '</tr>';
+ }
+
+ $html[] = '</tbody>';
$html[] = '</table>';
$html[] = '</div>';
return implode("", $html);
}
}
$weeks = $this->getWeeks();
週カレンダーオブジェクトの配列を取得します。
foreach($weeks as $week){ ... }
週カレンダーオブジェクトを一週ずつ処理していきます。
$html[] = '<tr class="'.$week->getClassName().'">';
週カレンダーオブジェクトを使ってHTMLのクラス名を出力します。
$days = $week->getDays();
週カレンダーオブジェクトから、日カレンダーオブジェクトの配列を取得します。
foreach($days as $day){
$html[] = '<td class="'.$day->getClassName().'">';
$html[] = $day->render();
$html[] = '</td>';
}
日カレンダーオブジェクトをループさせながら、クラス名を出力し、<td>の中に日カレンダーを出力していきます。
しっかり用途をわけたことでCalendarViewクラスの表示部分はシンプルになりました。
render()関数ができたら、ブラウザで表示させて確認してみましょう。
カレンダーの作成はPHPでそのまま作るこもとできます。
このようにHTMLを作成する処理を細かくクラスに分けるメリットはなんでしょうか?
まだ実感は得にくいと思いますが、変更に強いことが最大のメリットです。今後のこのカレンダーの機能を実装していくとオブジェクト指向のメリットがわかっていくと思います。
# デザイン調節
最後にカレンダーのCSSを調節します。
public以下にカレンダー用のCSSを作成します。
public/css/calendar.css
.day-blank {
background-color: #efefef;
}
.day-sun .day{
color:red;
}
.day-sat .day{
color: blue;
}
.calendar table td {
padding: 3px;
border: solid 1px #999;
}
.calendar table td:before {
display: block;
float: left;
height: 50px;
content: "";
}
.calendar table td .day {
margin-bottom: 0;
}
.calendar table th {
text-align: center;
border: solid 1px #999;
}
レイアウトファイルを編集し、作成したcalendar.cssを読み込みます。
resources/views/layouts/app.blade.php
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
+ <link href="{{ asset('css/calendar.css') }}" rel="stylesheet">
CSSの作成、読み込みの設定が終わりましたらブラウザで表示を確認してみましょう。
# まとめ
単純なカレンダーでも処理を細かくわけていくとこのような作りになります。
例えば、テーブルで出力している部分を<div>とflex-boxを使ってくださいと言われたら?そのままPHPを書いていた場合、直す箇所が非常に多いと思います。
その場合でもクラスをしっかりとわけているため、CalendarViewクラスを書き換えるだけで対応ができます。
次回はこのカレンダーを利用して、予定を登録する機能を作っていきます。
おつかれさまでした!
完成まで突っ走る意気込みです。サポートしていただけると非常に嬉しいです。応援よろしくお願いします。