見出し画像

カレンダーアプリを作ろう(#3) #Laravel基礎 #Laravelの教科書

本稿はカレンダーアプリを作ろうの3回目です。

前回までに営業日(定休日)を設定する機能を作成しました。今回はそれらに対して臨時休業、臨時営業を設定出来るようにしていきます。


# はじめに

前回「HolidaySetting」という各曜日が営業か休みかを設定するモデルを作成しました。

今回はそれらを上書きする形で「○月△日は営業or休み」というデータを記録していく必要があります。

また臨時休業などのちょっとしたコメントが入力出来るようにしようと思います。

完成形はこちらです。

スクリーンショット 2020-07-16 0.47.08


ドキュメントの都合上、コードがすべて載せられていません。わかりにくい場合は完成しているサンプルコードを見ながら進めてください。




# テーブルの作成

臨時休業を保存するためのテーブルを作成します。次のようなカラムを持つテーブルを作成します。

・日付キー(YYYYMMDDの文字列)
・営業か、休日かを設定するフラグ
・コメント

テーブル名「extra_holiday」を作成するマイグレーションファイルをmake:migrationコマンドを使って作成します。

php artisan make:migration create_extra_holiday_table

作成されたマイグレーションファイルを修正していきます。今回は「2020_07_15_131046_create_extra_holiday_table」と出力されました。

database/migrations/2020_07_15_131046_create_extra_holiday_table.php

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateExtraHolidayTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('extra_holiday', function (Blueprint $table) {
			$table->id();
			$table->string("date_key", 8)->unique();
			$table->integer("date_flag")->default(0);
			$table->string("comment")->nullable();
           $table->timestamps();
       });
   }
   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('extra_holiday');
   }
}
$table->string("date_key", 8)->unique();

date_keyはYYYYMMDD(例: 20200715)という書式なので長さを8に指定しています。string()の2個目の引数で文字列の長さを指定できます。また、同じ日に対して複数設定する必要は無いのでunique()を指定しています。

マイグレーションファイルが作成できたらmigrateコマンドを使って実行していきましょう。

php artisan migrate

マイグレーションが実行出来ない場合はコードにミスが無いかなどを確認してください。


# モデルの作成

先程作成したextra_holidayテーブルに紐付いたモデルを作成していきます。

モデル名は「ExtraHoliday」とします。make:modelコマンドを使ってモデルを作成します。設置場所はHolidaySettingと揃えてCalendarディレクトリの下に行います。

php artisan make:model Calendar/ExtraHoliday

作成されたExtraHoliday.phpを修正していきます。

app/Calendar/ExtraHoliday.php

<?php
namespace App\Calendar;
use Illuminate\Database\Eloquent\Model;
class ExtraHoliday extends Model
{
	const OPEN = 1;
	const CLOSE = 2;
	protected $table = "extra_holiday";
	
	protected $fillable = [
		"date_flag",
		"comment"
	];
	function isClose(){
		return $this->date_flag == ExtraHoliday::CLOSE;
	}
	function isOpen(){
		return $this->date_flag == ExtraHoliday::OPEN;
	}
}

ここでfillableをdate_flagとcommentだけにしている理由は次に作成するフォームの作りに関連します。


# 臨時休業設定機能の開発の準備

臨時休業設定の設定機能を作成していきます。

この時ゼロから作るのではなく、表示用のCalendarViewクラスをカスタマイズして、臨時休業設定用のフォームを作っていきます。

新規に機能を作る時、同じコードを再度書かずに今まで作ったものを活用しながら作っていくことが大事です。これをDRY原則と言います。Don't Repeat Yourselfの略で、「繰り返しを避けること」という意味です。

カレンダーを表示する、という処理を何度も書かずに済ませるためにCalendarViewを拡張していくアプローチを取ります。


まずは設定用のフォーム「CalendarFormView」クラスを作成していきます。新規に「app/Calendar/Form/CalendarFormView.php」を作成します。オリジナルのクラスなのでLaravelのコマンドではなく手動で作成します。

app/Calendar/Form/CalendarFormView.php

<?php
namespace App\Calendar\Form;
use Carbon\Carbon;
use App\Calendar\CalendarView;
/**
* 表示用
*/
class CalendarFormView extends CalendarView {
	
}
use App\Calendar\CalendarView;

中身は今の所空です。CalendarViewを拡張していくためにuse句の指定が必要です。

ではこのCalendarFormViewクラスを使って設定画面を表示する所まで準備を行います。


Controller、View、ルーティングの設定を次の形で行います。

・コントローラー: Calendar/ExtraHolidaySettingController
・View: calendar/extra_holiday_setting_form.blade.php
・Route: /extra_holiday_setting


コントローラーの作成はmake:controllerコマンドです。

php artisan make:controller Calendar/ExtraHolidaySettingController

作成したコントローラーを修正します.

app/Http/Controllers/Calendar/ExtraHolidaySettingController.php

<?php
namespace App\Http\Controllers\Calendar;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Calendar\Form\CalendarFormView;
use App\Calendar\ExtraHoliday;
class ExtraHolidaySettingController extends Controller
{
	public function form(){
		
		$calendar = new CalendarFormView(time());
		return view('calendar/extra_holiday_setting_form', [
			"calendar" => $calendar
		]);
	}
	public function update(Request $request){
		return redirect()
			->action("Calendar\ExtraHolidaySettingController@form")
			->withStatus("保存しました");
	}
}
use App\Calendar\Form\CalendarFormView;
use App\Calendar\ExtraHoliday

先程作成したCalendarFormViewと、モデルのExtraHolidayを使うためにuse句を追加します。

form()メソッドは「CalendarViewクラス」ではなく「CalendarFormViewクラス」を使う以外は「CalendarController」とほとんど同じコードです。

update()メソッドは今は何もせずにリダイレクトするだけで作成します。


続いてViewの作成です。

resources/views/calendar.blade.phpと非常に似ていて、カレンダー本体は{!! $calendar->render() !!}で描画します。

resources/views/calendar/extra_holiday_setting_form.php

@extends('layouts.app')
@section('content')
<div class="container">
   <div class="row justify-content-center">
       <div class="col-md-12">
           <div class="card">
               <div class="card-header">{{ $calendar->getTitle() }}の臨時営業日設定</div>
               <div class="card-body">
					@if (session('status'))
                       <div class="alert alert-success" role="alert">
                           {{ session('status') }}
                       </div>
                   @endif
					<form method="post" action="{{ route('update_extra_holiday_setting') }}">
						@csrf
						<div class="card-body">
							{!! $calendar->render() !!}
							<div class="text-center">
								<button type="submit" class="btn btn-primary">保存</button>
							</div>
						</div>
						
					</form>
               </div>
           </div>
       </div>
   </div>
</div>
@endsection


ルーティングを設定して完了です。フォーム表示用のgetメソッドと、更新用のpostメソッドそれぞれを作成します。

//臨時営業設定
Route::get('/extra_holiday_setting', 
    'Calendar\HolidaySettingController@form')
    ->name("extra_holiday_setting");
    
Route::post('/extra_holiday_setting',
    'Calendar\HolidaySettingController@update')
    ->name("update_extra_holiday_setting");

ここまで保存できたらブラウザでhttp://localhost:8000/extra_holiday_settingを開いて動作を確認してみましょう。

スクリーンショット 2020-07-15 23.05.52


# CalendarViewの修正

カレンダーの画面にフォームを追加する前に、CalendarViewクラス、CalendarWeekクラスを変更し、カスタマイズしやすいようにしていきます。

app/Calendar/CalendarView.php

class CalendarView {
*	protected $carbon;
	protected function getWeeks(){
		$weeks = [];
		//初日
		$firstDay = $this->carbon->copy()->firstOfMonth();
		//月末まで
		$lastDay = $this->carbon->copy()->lastOfMonth();
		//1周週目
*		$weeks[] = $this->getWeek($firstDay->copy());
		//作業用の日
		$tmpDay = $firstDay->copy()->addDay(7)->startOfWeek();
		//月末までループさせる
		while($tmpDay->lte($lastDay)){
			//週カレンダーViewを作成する
*			$weeks[] = $this->getWeek($tmpDay->copy(), count($weeks));
			//次の週=+7日する
			$tmpDay->addDay(7);
		}
		return $weeks;
	}
	/**
	 * @return CalendarWeek
	 */
+	protected function getWeek(Carbon $date, $index = 0){
+		return new CalendarWeek($date, $index);
+	}
	
}
protected $carbon;

privateで作るとサブクラスから読むことが出来ないためprotectedに変更します。


今まで、次のようにCalendarWeekを作っていた場所を関数として分けています。

$week = new CalendarWeek($firstDay->copy());
$weeks[] = $week;
↓
$weeks[] = $this->getWeek($firstDay->copy());


protected function getWeek(Carbon $date, $index = 0){
    return new CalendarWeek($date, $index);
}

ループの中で書くよりも、ソースコードがスッキリして見やすくなっています。



CalendarWeekクラスも同様にCalendarWeekDay()を作成している場所をカスタマイズしていきます。

app/Calendar/CalendarWeek.php

class CalendarWeek {

*	protected $carbon;
*	protected $index = 0;

	/**
	 * @return CalendarWeekDay[]
	 */
	function getDays(HolidaySetting $setting){
		$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;	
			}
				
			//今月
*			$days[] = $this->getDay($tmpDay->copy(), $setting);
			//翌日に移動
			$tmpDay->addDay(1);
		}
		
		return $days;
	}

+	/**
+	 * @return CalendarWeekDay
+	 */
+	function getDay(Carbon $date, HolidaySetting $setting){
+		$day = new CalendarWeekDay($date);
+		$day->checkHoliday($setting);
+		return $day;
+	}
}
protected $carbon;
protected $index = 0;

こちらもサブクラスから読み取りが出来るようにprotectedに変更しています。


CalendarWeekDay()を作成している場所を関数として切り出しています。

$day = new CalendarWeekDay($tmpDay->copy());
$day->checkHoliday($setting);
$days[] = $day;
↓
$days[] = $this->getDay($tmpDay->copy(), $setting);

function getDay(Carbon $date, HolidaySetting $setting){
    $day = new CalendarWeekDay($date);
    $day->checkHoliday($setting);
    return $day;
}



CalendarWeekDayクラスは変更は無いですが設定値を後から上書きしたり取得出来るような便利な関数を用意しておきます。

app/Calendar/CalendarWeekDay.php

class CalendarWeekDay {
    ...
    
	function getDateKey(){
		return $this->carbon->format("Ymd");
	}
	function setHoliday($flag){
		$this->isHoliday = $flag;
	}
    ...
}

このように動作はそのまま同じにコードを整理する作業をリファクタリングといいます。一気に作成した段階では無駄が多いコードになるため、随時リファクタリングを行い、整理された読みやすいコードを書いていきましょう。


この修正に問題が無いか、ブラウザでカレンダーを表示して確認しましょう。

カレンダーの表示に崩れが発生していなければ修正完了です。



# フォームの表示

今まではCalendarViewから作成するクラスが決まっていたためカスタマイズしにくい作りになっていましたが、関数として切り出したことでカスタマイズがしやすくなりました。

フォームをどうやって作るかというと、

・CalendarViewがCalendarWeekを作成
・CalendarWeekがCalendarWeekDayを作成

という処理を

・CalendarFormViewからCalendarWeekFormを作成
・CalendarWeekFormからCalendarWeekDayFormを作成

という形に書き換えます。今まではクラス作成部分が決まっているため出来ませんでしたが、このリファクタリングでフォーム表示用のクラスに置き換えて作ることが出来るようになりました。


まずはCalendarFormViewを修正しCalendarWeekFormを返却出来るようにします。

app/Calendar/Form/CalendarFormView.php

class CalendarFormView extends CalendarView {
	/**
	 * @return CalendarWeekForm
	 */
	protected function getWeek(Carbon $date, $index = 0){
		$week = new CalendarWeekForm($date, $index);
		return $week;
	}
}


次にCalendarWeekFormを作成し、CalendarWeekDayFormを返すように修正します。

app/Calendar/Form/CalendarWeekForm.php

<?php
namespace App\Calendar\Form;

use Carbon\Carbon;
use App\Calendar\CalendarWeek;
use App\Calendar\HolidaySetting;

class CalendarWeekForm extends CalendarWeek {
	/**
	 * @return CalendarWeekDayForm
	 */
	function getDay(Carbon $date, HolidaySetting $setting){
		$day = new CalendarWeekDayForm($date);
		$day->checkHoliday($setting);
		return $day;
	}
}
use App\Calendar\CalendarWeek;
use App\Calendar\HolidaySetting;

設置場所が変わったのでuse句を忘れないようにしましょう。


最後にCalendarWeekDayFormクラスを作成します。

こちらはフォームを表示するため、コードが少し複雑です。

<?php
namespace App\Calendar\Form;

use Carbon\Carbon;

use App\Calendar\CalendarWeekDay;
use App\Calendar\HolidaySetting;
use App\Calendar\ExtraHoliday;

class CalendarWeekDayForm extends CalendarWeekDay {

	public $extraHoliday = null; 

	/**
	 * @return 
	 */
	function render(){
		//selectの名前
		$select_form_name = "extra_holiday[" . $this->carbon->format("Ymd") . "][date_flag]";
		//コメントのinputの名前
		$comment_form_name = "extra_holiday[" . $this->carbon->format("Ymd") . "][comment]";
		
		//定休日設定の値
		$defaultValue = ($this->isHoliday) ? "休み" : "営業日";
		//臨時休業が選択されているかどうか
		$isSelectedExtraClose = ($this->extraHoliday && $this->extraHoliday->isClose()) ? 'selected' : '';
		//臨時営業が選択されているかどうか
		$isSelectedExtraOpen = ($this->extraHoliday && $this->extraHoliday->isOpen()) ? 'selected' : '';
		//コメントの値
		$comment = ($this->extraHoliday) ? $this->extraHoliday->comment : '';
		
		//HTMLの組み立て
		$html = [];
		
		//日付
		$html[] = '<p class="day">' . $this->carbon->format("j"). '</p>';
		//臨時営業・臨時休業設定
		$html[] = '<select name="'. $select_form_name . '" class="form-control">';
		$html[] = '<option value="0">- (' . $defaultValue . ')</option>';
		$html[] = '<option value="'.ExtraHoliday::CLOSE.'" ' . $isSelectedExtraClose . '>臨時休業</option>';
		$html[] = '<option value="'.ExtraHoliday::OPEN.'" ' . $isSelectedExtraOpen . '>臨時営業</option>';
		$html[] = '</select>';
		//コメント
		if($isSelectedExtraClose || $isSelectedExtraOpen){
			$html[] = '<input class="form-control" type="text" name="'.$comment_form_name.'" value="'.e($comment).'" />';
		}
		
		return implode("", $html);
	}
	
	function getClassName(){
		$classNames = [ "day-" . strtolower($this->carbon->format("D")) ];
		if($this->extraHoliday){
			if($this->extraHoliday->isClose()){
				$classNames[] = "day-close"; //臨時営業
			}
		}else if($this->isHoliday){
			
			$classNames[] = "day-close";
		}
		return implode(" ", $classNames);
	}
}

各コードについて1行ずつ解説していきます。

//selectの名前
$select_form_name = "extra_holiday[" . $this->carbon->format("Ymd") . "][date_flag]";

//コメントのinputの名前
$comment_form_name = "extra_holiday[" . $this->carbon->format("Ymd") . "][comment]";

例えば2020年7月1日のフォームは、次のようなnameが指定されるように作っています。

extra_holiday[20200701][date_flag]
extra_holiday[20200701][comment]

nameの値をこの形式にすると、フォームのリクエスト送信先で次のように順にループで値を処理することが出来ます。

$extra_holiday = $request->get("extra_holiday");
foreach($extra_holiday as $date_key => $array){
	$date_flag = $array["date_flag"];
	$comment = $array["comment"];
}

入れ子になった複雑なnameを使ったフォームが作れるようになると表現がぐっと広がります。


//定休日設定の値
$defaultValue = ($this->isHoliday) ? "休み" : "営業日";

CalendarWeekDayに元々指定されている値がどうなっているかを確認するための変数を用意しています。ExtraHolidayが設定されていない時どうなるか、わかりやすい表示になるように追加しています。

//臨時休業が選択されているかどうか
$isSelectedExtraClose = ($this->extraHoliday && $this->extraHoliday->isClose()) ? 'selected' : '';

//臨時営業が選択されているかどうか
$isSelectedExtraOpen = ($this->extraHoliday && $this->extraHoliday->isClose()) ? 'selected' : '';

(条件) ? 値1 : 値2」の書き方は三項演算子と呼ばれるものです。条件がtrueの時は値1を、条件がfalseの時は値2を入力します。

これは<option>タグが選択されている場合にselectedを出力するためのコードです。


//臨時営業・臨時休業設定
$html[] = '<select name="'. $select_form_name . '" class="form-control">';
$html[] = '<option value="0">- (' . $defaultValue . ')</option>';
$html[] = '<option value="'.ExtraHoliday::CLOSE.'" ' . $isSelectedExtraClose . '>臨時休業</option>';
$html[] = '<option value="'.ExtraHoliday::OPEN.'" ' . $isSelectedExtraOpen . '>臨時営業</option>';
$html[] = '</select>';

このコードで下記のようなHTMLが組み立てられます。

<select name="extra_holiday[20200701][date_flag]" class="form-control">
<option value="0">- (休み)</option>
<option value="2" >臨時休業</option>
<option value="1" >臨時営業</option>
</select>


ここまで作成したらブラウザで臨時営業日設定画面を表示してみましょう。

スクリーンショット 2020-07-15 23.07.59

上手く表示されていますか?表示崩れなどがある場合はタグの閉じ忘れや「"(引用符)」の個数に誤りがある場合があります。


# フォームの値の受け取り

ExtraHolidaySettingControllerクラスのupdate()関数を修正し、フォームの値を保存していきます。

class ExtraHolidaySettingController extends Controller
{
    ...
	public function update(Request $request){

		$input = $request->get("extra_holiday");

		ExtraHoliday::updateExtraHolidayWithMonth(date("Ym"), $input);
		
		return redirect()
			->action("Calendar\ExtraHolidaySettingController@form")
			->withStatus("保存しました");
	}
    ...
}

臨時営業の設定はすべてExtraHoliday::updateExtraHolidayWithMonth()関数で行うようにします。コントローラーの更新のコードは最小限になるように心がけましょう。


#ExtraHoliday::updateExtraHolidayWithMonth()関数の作成

ExtraHolidayを修正し、次の関数を作成します。

1. getExtraHolidayWithMonth() 指定した月の臨時営業データをすべて取得する関数
2. updateExtraHolidayWithMonth() 指定した月の臨時営業データを更新する関数


まずは、指定した月の臨時営業データをすべて取得する関数getExtraHolidayWithMonth()をExtraHolidayに追加します。

class ExtraHoliday extends Model
{
	...
	/**
	 * 指定した月の臨時営業・休業を取得する
	 * @return ExtraHoliday[]
	 */
	public static function getExtraHolidayWithMonth($ym){
		return ExtraHoliday::where("date_key", 'like', $ym . '%')
            ->get()->keyBy("date_key");
	}
    ...
}

どのように「指定した月」のデータを取ってくるかというと、次のように考えていけば取得できます。

date_keyは20200701のように日付が入っています。つまり202007で始まるデータを取ってくれば2020年7月の臨時営業データが取得できます。202008で始まるデータであれば2020年8月の臨時営業日データです。

このような○○で始まる〜という条件を書くにはlikeを利用します。

where("カラム名", "like", "条件")

%を後ろにつければ前方一致(〜始まる)、%を前につければ後方一致(〜で終わる)です。

where("date_key", 'like', $ym . '%')

ここでは202007で始まるデータが欲しいので、後ろに「%」をつけた前方一致で値を取り出しています。


keyBy()関数はLaravelのCollectioと呼ばれる配列を便利に扱うためのクラスにある機能です。

->keyBy("date_key")」を指定することでdate_key => ExtraHolidayというキーがついた配列で取得出来ます。


次に一括で更新するためのupdateExtraHolidayWithMonth()関数を追加します。


class ExtraHoliday extends Model
{
	/**
	 * 一括で更新する
	 */
	public static function updateExtraHolidayWithMonth($ym, $input){
		
		$extreaHolidays = self::getExtraHolidayWithMonth($ym);
		
		foreach($input as $date_key => $array){
			
			if(isset($extreaHolidays[$date_key])){	//既に作成済の場合

				$extraHoliday = $extreaHolidays[$date_key];
				$extraHoliday->fill($array);

				//CloseかOpen指定の場合は上書き
				if($extraHoliday->isClose() || $extraHoliday->isOpen()){
					$extraHoliday->save();
				
				//指定なしを選択している場合は削除
				}else{
					$extraHoliday->delete();
				}
				continue;
			}

			$extraHoliday = new ExtraHoliday();
			$extraHoliday->date_key = $date_key;
			$extraHoliday->fill($array);

			//CloseかOpen指定の場合は保存
			if($extraHoliday->isClose() || $extraHoliday->isOpen()){
				$extraHoliday->save();
			}
		}
	}
}
if(isset($extreaHolidays[$date_key])){ ... }

getExtraHolidayWithMonth()で取得したデータはkeyBy関数を利用して$date_keyがキーの配列になっています。作成済かどうかをisset()で分岐できます。

指定なし(isOpenでもisCloseでも無い時)は削除を行います。削除することで指定なしに戻すことができます。


$extraHoliday->fill($array);

fill()関数でdate_flagとcommentのみ上書きしています。


# 保存したデータを反映する

保存が出来るようになったのでこれをフォームの画面に反映していきます。

まずはCalendarViewを修正し、臨時営業のデータを読み込むようにします。

app/Calendar/CalendarView.php

use Carbon\Carbon;
use App\Calendar\ExtraHoliday;
class CalendarView {
	protected $carbon;
	protected $holidays = [];

	function __construct($date){
		$this->carbon = new Carbon($date);
	}

	/**
	 * タイトル
	 */
	public function getTitle(){
		return $this->carbon->format('Y年n月');
	}

	/**
	 * カレンダーを出力する
	 */
	function render(){
		//HolidaySetting
		$setting = HolidaySetting::firstOrNew();
		$setting->loadHoliday($this->carbon->format("Y"));

		//臨時営業日の読み込み
		$this->holidays = ExtraHoliday::getExtraHolidayWithMonth($this->carbon->format("Ym"));
		
		....	
	}
}
use App\Calendar\ExtraHoliday;

臨時営業データを読み込めるようにExtraHolidayを使うためのuse句を追加します。

protected $holidays = [];

臨時営業日のデータを保持するためのクラスです。

//臨時営業日の読み込み
$this->holidays = ExtraHoliday::getExtraHolidayWithMonth($this->carbon->format("Ym"));

render()関数で読み込むことができればよいので、render()関数の内部で読み込みます。


ベースとなるCalendarViewクラスで臨時営業日が設定できるようになったので、次にフォーム用のCalendarFormViewクラスを修正し、臨時営業日データを処理していきます。

app/Calendar/Form/CalendarFormView.php


<?php
namespace App\Calendar\Form;
use Carbon\Carbon;
use App\Calendar\CalendarView;
use App\Calendar\ExtraHoliday;
/**
* 表示用
*/
class CalendarFormView extends CalendarView {
	/**
	 * @return CalendarWeek
	 */
	protected function getWeek(Carbon $date, $index = 0){
		$week = new CalendarWeekForm($date, $index);

		//臨時営業日を設定する
		$start = $date->copy()->startOfWeek()->format("Ymd");
		$end = $date->copy()->endOfWeek()->format("Ymd");

		$week->holidays = $this->holidays->filter(function($value, $key) use($start, $end){
			return $key >= $start && $key <= $end;
		})->keyBy("date_key");

		return $week;
	}
}


$start = $date->copy()->startOfWeek()->format("Ymd");
$end = $date->copy()->endOfWeek()->format("Ymd");

週の開始日〜終了日を取得しています。

$week->holidays = $this->holidays->filter(function($value, $key) use($start, $end){
	return $key >= $start && $key <= $end;
})->keyBy("date_key");

filter()関数を使って週の開始〜終了までの祝日を取得して割り当てています。filter()関数もLaravelのCollectionに含まれる関数です。特定の条件のオブジェクトだけを取り出すのに便利ですね。


CalendarFormViewからCalendarWeekFormに臨時営業日を渡す処理が追加されたので、CalendarWeekFormクラス側も対応を行います。

app/Calendar/Form/CalendarWeekForm.php

class CalendarWeekForm extends CalendarWeek {

+	/**
+	 * ExtraHoliday[]
+	 */
+	public $holidays = [];

	/**
	 * @return CalendarWeekDayForm
	 */
	function getDay(Carbon $date, HolidaySetting $setting){
		$day = new CalendarWeekDayForm($date);
		$day->checkHoliday($setting);

+		if(isset($this->holidays[$day->getDateKey()])){
+			$day->extraHoliday = $this->holidays[$day->getDateKey()];
+		}
		return $day;
	}

}
if(isset($this->holidays[$date->format("Ymd")])){
 $day->extraHoliday = $this->holidays[$date->format("Ymd")];
}

祝日の情報を知っているのであれば渡す処理です。

CalendarWeekDayForm側はextraHolidayを取り扱う処理は既に実装済みなためフォームの設定は以上となります。

ここまで完了したらブラウザで臨時営業日が保存できるか、確認してください。

また、臨時休業・臨時営業にしたらコメントが入力できることを確認してください。

スクリーンショット 2020-07-16 0.47.08



# カレンダーに反映

臨時営業日の設定を表示側のカレンダーにも反映していきましょう。

しかし、祝日のデータの取り扱いがカレンダーとそれ以外で若干異なるため、今回は新たに表示用のカレンダークラスを追加で作成していきます。

次のような派生のパターンがあります。

CalendarView (ベースとなるカレンダー)
├ CalendarFormView(臨時営業日用のカレンダー)
└ CalendarOutputView(ユーザーに表示するようのカレンダー)


まずはカスタマイズがしやすいようにCalendarViewのrender()関数を次のように書き換えます。

class CalendarView {
	function render(){
		...
		foreach($days as $day){
			$html[] = '<td class="'.$day->getClassName().'">';
			$html[] = $day->render();
			$html[] = '</td>';
		}
		....
	}
	
}
↓
class CalendarView {
	function render(){
		...
		foreach($days as $day){
			$html[] = $this->renderDay($day);
		}
		...
	}
	
	/**
	 * 日を描画する
	 */
	protected function renderDay(CalendarWeekDay $day){
		$html = [];
		$html[] = '<td class="'.$day->getClassName().'">';
		$html[] = $day->render();
		$html[] = '</td>';
		return implode("", $html);
	}
	
}

日を描画している部分だけ別の関数にしています。

次に、表示用のCalendarOutputViewクラスを作成します。こちらは「app/Calendar/Output/CalendarOutputView」に作成していきます。

app/Calendar/Output/CalendarOutputView.php


<?php
namespace App\Calendar\Output;
use Carbon\Carbon;
use App\Calendar\CalendarView;
use App\Calendar\CalendarWeekDay;
/**
* 表示用
*/
class CalendarOutputView extends CalendarView {
	
	/**
	 * 日を描画する
	 */
	protected function renderDay(CalendarWeekDay $day){

		$html = [];
		$extraHoliday = null;

		//臨時営業日設定で上書き
		if(isset($this->holidays[$day->getDateKey()])){
			$extraHoliday = $this->holidays[$day->getDateKey()];
			if($extraHoliday->isOpen()){
				$day->setHoliday(false);
			}else if($extraHoliday->isClose()){
				$day->setHoliday(true);
			}
		}

		$html[] = '<td class="'.$day->getClassName().'">';
		$html[] = $day->render();

        //コメントを表示
		if($extraHoliday){
			$html[] = '<p class="comment">' . e($extraHoliday->comment) . '</p>';
		}

		$html[] = '</td>';

		return implode("", $html);
	}
}

CalendarOutputViewはCalendarWeekなどに渡さずに、renderDay()関数で直接処理しています。臨時営業日設定があれば上書きする処理を追加しています。また、コメントがあればそれを表示しています。

renderDay()関数を切り出したおかげで修正箇所はここだけで済みました。実はフォーム部分もrenderDay()関数を使うとCalendarWeekFormクラスを使わないでも実装することが出来ます。しっかりと用途別にクラスを分ける、ということを表現したかったので若干遠回りな内容になっています。

CalendarWeekFormクラスを使わない実装も試してみてください。


CalendarOutputViewは作っただけで現在使われていないので、CalendarControllerクラスをCalendarOutputViewクラスを利用する形に修正します。

app/Http/Controllers/CalendarController.php

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Calendar\Output\CalendarOutputView;

class CalendarController extends Controller
{
   public function show(){
		
		$calendar = new CalendarOutputView(time());
		return view('calendar', [
			"calendar" => $calendar
		]);
	}
	
}

コメントをそのまま表示するとCSSに崩れが発生するので下記のCSSをcalendar.cssに追加して、ブラウザで確認しましょう。

public/css/calendar.css

.calendar table {
	table-layout: fixed;
}
.calendar table .comment{
	font-size: small;
}

スクリーンショット 2020-07-16 0.35.47


# まとめ

描画のためのクラスを作り、様々なパターンに応じてサブクラスを作っていくやり方は色々な場面に応用できるやり方です。

これ以外のクラスの分け方や作り方もあります。もっと良い方法、もっとわかりやすい方法を自分で考えて作ってみるのも学習になるので是非挑戦してみてください。

大事なのは何かを作る時、同じ機能を再利用出来るのではないか?以前同じ処理を書かなかったか?を考えて、無駄を無くしていくことです。

似たような処理だけど少し違うを綺麗に書こうとすることが、技術力を身につける良いきっかけとなります。

予約システムなど、これらのカレンダー形式のフォームは様々な場面で目にすることができます。このカレンダーアプリを基本に何か応用できないか考えてみてください。

今回は長くなったのでここまでとし、次回に月の移動のナビゲーションを作成してカレンダーアプリは完了とします。


おつかれさまでした!


https://note.com/laravelstudy/m/m7f68f7db4055


完成まで突っ走る意気込みです。サポートしていただけると非常に嬉しいです。応援よろしくお願いします。