見出し画像

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

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

前回までに営業日と臨時営業、臨時休業を作成しました。今回は前後の月のカレンダーに移動できるようにしてカレンダーアプリとしての体裁を整えていきます。

# はじめに

Webアプリケーションで何らかの機能を作成する時にまず考えるべきことはURLです。

どんなURLでアクセスされた時にどのような画面を表示するのか、という点に注目しながら機能を設計していきます。


前後の月のカレンダーに移動する

前の月へのリンクはどのようなURLになる?
次の月へのリンクはどのようなURLになる?

という問題を考えていく形になります。

今回はクエリーに月情報を追加することで特定の月へ移動できる、という形で作ろうと思っています。


カレンダーの表示場所は表示側と臨時営業日設定の2箇所あります。

それぞれ次のようなURLになることを想定します。

表示側カレンダーのURL
/?date=2020-07 => 2020年07月のカレンダーを表示
/?date=2020-08 => 2020年08月のカレンダーを表示
/?date=2020-09 => 2020年09月のカレンダーを表示
臨時営業日カレンダーのURL
/extra_holiday_setting?date=2020-07 => 2020年07月のカレンダーを表示
/extra_holiday_setting?date=2020-08 => 2020年08月のカレンダーを表示
/extra_holiday_setting?date=2020-09 => 2020年09月のカレンダーを表示


現在が2020年8月であれば前の月は「?date=2020-07」、次の月は「?date=2020-07」となるようにリンクを作ることで前後の月のナビゲーションが実現できます。


# 他のパターンも検討する

クエリーで行うのが唯一の正解ではありません。

例えばURLのパスに年月の情報を入れるパターンもあります。

/calendar/2020/07/ => 2020年07月のカレンダーを表示

このようなURLはブログなどによくある作り方です。

この形の場合、Laravelではルーティングの設定を変更する必要があります。例えば次のような記述となります。

Route::get('/calendar/{year}/{month}', 'CalendarController@show')
	->where(['year' => '[0-9]+', 'month' => '[0-9]+']);

ルーティングファイルが直感的でないこともあり、今回は採用しませんでした。


「どの月を表示するのか」という情報をセッションに保存する作り方もあります。

この場合はURLは常に同じですが、前後の月へ移動するリンクが「セッションに月を書き換えて遷移する」という処理になるため少しわかりにくくなるため、今回は採用しませんでした。

このタイプの処理は古いWebアプリケーションにはよくありますが、最近ではURLに状態を含んだ形の方が望ましいという考え方が主流となるため、セッションに入れる形で作ることは避けた方が安全です。

などなど、機能一つURL一つとっても考えるべきことはいくつかあります。

比較検討した結果、今回はクエリー方式を採用していきます。


# dateパラメーターの受け取り

カレンダー用のコントローラー「CalendarController」を修正し、クエリーのパラメーターを受け取って描画できるようにしていきます。


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(Request $request){

		//クエリーのdateを受け取る
		$date = $request->input("date");

		//dateがYYYY-MMの形式かどうか判定する
		if($date && preg_match("/^[0-9]{4}-[0-9]{2}$/", $date)){
			$date = strtotime($date . "-01");
		}else{
			$date = null;
		}
		
		//取得出来ない時は現在(=今月)を指定する
		if(!$date)$date = time();

		//カレンダーに渡す
		$calendar = new CalendarOutputView($date);
		return view('calendar', [
			"calendar" => $calendar
		]);
	}
	
}
public function show(Request $request){ ... }

クエリーを受け取るためshow()関数の引数にRequestを追加します。

//クエリーのdateを受け取る
$date = $request->input("date");

Request#input()関数を使うことでクエリーdateを受け取ります。

if($date && strlen($date) == 7){ ... }

dateが「2020-07」の形式であることを確認しています。簡易で7文字であることをチェックしていますが、正規表現を使って次のように書くこともできます。

if($date && preg_match("/^[0-9]{4}-[0-9]{2}$/", $date)){ ... }

正規表現はWebアプリケーションの作成では非常に重要な技術ですが、初学者の段階では難しいため応用として紹介するにとどめておきます。

$date = strtotime($date . "-01");

文字列で書かれた日付をPHPの内部の日時データに変換しています。

strtotime()関数はその名の通り「str=文字列」を「time=日時データ」に変換する関数です。

strtotime()が正しく動作するように「2020-07」という日付情報を「-01」を付与して「2020-07-01」の形に変換してから渡しています。

if(!$date)$date = time();

クエリーにdateが無い場合は現在の時間を渡せば今月を表示することができます。

$calendar = new CalendarOutputView(time());

$calendar = new CalendarOutputView($date);

CalendarOutputViewクラスはどの月を描画するかを引数で受け取る形で作成されているため、取得した日時情報$dateを渡すことで指定した月を描画することができます。


ブラウザで次のようなURLを入力して2020年12月のカレンダーを表示してみましょう。

http://127.0.0.1:8000/?date=2020-12

スクリーンショット 2020-07-31 23.46.42

祝日を休みに設定していますが、休みがありません。去年まであった天皇誕生日がなくなってしまったので、2020年12月は祝日の無い月となりました…。


# 前後の月を作成する

前後の月へのナビゲーションを作成するためには前後の月を取得する必要があります。

CalendarViewを修正し、前後の月を取得するためのgetPreviousMonth()関数、getNextMonth()関数を作成します。

app/Calendar/CalendarView.php

class CalendarView {
	
	....
	
	/**
	 * 次の月
	 */
	public function getNextMonth(){
		return $this->carbon->copy()->addMonthsNoOverflow()->format('Y-m');
	}
	/**
	 * 前の月
	 */
	public function getPreviousMonth(){
		return $this->carbon->copy()->subMonthsNoOverflow()->format('Y-m');
	}
	
	...
	
}

$this->carbon->copy()->addMonthsNoOverflow()->format('Y-m')

Carbonを使って次の月を作るときは「addMonthsNoOverflow()」関数を使います。addMonth()という関数もあるのですが、この処理では2月のような30日に満たない場合処理がおかしくなるという仕様があるため注意ください。

取得した翌月の情報をformat()関数を使って「2020-07」の形式に変換しています。

$this->carbon->copy()->subMonthsNoOverflow()->format('Y-m');

前の月を作り処理も同様に「subMonth()」ではなく「subMonthsNoOverflow()」を使います。


# 前後の月のナビゲーションを作成

テンプレートを編集し、カレンダーの画面に前後の月のナビゲーションを作成します。

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 text-center">
*					<a class="btn btn-outline-secondary float-left" href="{{ url('/?date=' . $calendar->getPreviousMonth()) }}">前の月</a>
*					
*					<span>{{ $calendar->getTitle() }}</span>
*					
*					<a class="btn btn-outline-secondary float-right" href="{{ url('/?date=' . $calendar->getNextMonth()) }}">次の月</a>
*				</div>
				<div class="card-body">
					{!! $calendar->render() !!}
               </div>
           </div>
       </div>
   </div>
</div>
@endsection

{{ url('/?date=' . $calendar->getPreviousMonth()) }}
{{ url('/?date=' . $calendar->getNextMonth()) }}

前後の月の処理をCalendarViewに作成したのでテンプレートに要素が増えていますがシンプルに書けています。「?date=2020-07」の形式になるようにリンクを設定しています。


ブラウザでカレンダーを表示して前後の月のナビゲーションが動作するか確認してみましょう。

スクリーンショット 2020-08-01 0.12.59


# 臨時営業日設定機能の修正1

臨時営業日設定機能にも同様のナビゲーションを追加していきます。

CalendarViewに前後の月を取得する機能を追加したので、それを拡張したCalendarFormViewにも自動的に同じ機能を使うことが出来ます。

まずはテンプレートを次のように変更しましょう。

resources/views/calendar/extra_holiday_setting_form.blade.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 text-center">
*					<a class="btn btn-outline-secondary float-left" href="{{ url('/extra_holiday_setting?date=' . $calendar->getPreviousMonth()) }}">前の月</a>
*					
*					<span>{{ $calendar->getTitle() }}の臨時営業日設定</span>
*				
*					<a class="btn btn-outline-secondary float-right" href="{{ url('/extra_holiday_setting?date=' . $calendar->getNextMonth()) }}">次の月</a>
*				</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

ブラウザで確認して表示が崩れていないことを確認します。

スクリーンショット 2020-08-01 0.18.39


続いてクエリーdateを受け取るようにExtraHolidaySettingControllerを修正します。dateの受け取りの処理はCalendarControllerと同じものを使うことが出来ます。

app/Http/Controllers/Calendar/ExtraHolidaySettingController.php

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

		//クエリーのdateを受け取る
		$date = $request->input("date");

		//dateがYYYY-MMの形式かどうか判定する
		if($date && strlen($date) == 7){
			$date = strtotime($date . "-01");
		}else{
			$date = null;
		}
		
		//取得出来ない時は現在(=今月)を指定する
		if(!$date)$date = time();
		
		//フォームを表示
		$calendar = new CalendarFormView($date);

		return view('calendar/extra_holiday_setting_form', [
			"calendar" => $calendar
		]);
	}
}

ブラウザで前後の月に移動できるか確認しましょう。


# 臨時営業日設定機能の修正2

臨時営業日設定機能は現在、次のように書かれているため指定した月の臨時営業日を更新する正しい処理になっていません。

$input = $request->get("extra_holiday");
ExtraHoliday::updateExtraHolidayWithMonth(date("Ym"), $input);

date("Ym")」と記述した部分を指定した月に変更する必要があります。

GETからPOSTに値を渡す時、アプローチとしては2種類あります。

1. クエリパラメーターを引き継ぐ

テンプレートでは臨時営業日のフォームは次のように書かれています。

<form method="post" action="{{ route('update_extra_holiday_setting') }}">

こちらを次のように書き換えることでクエリーパラメーターを引き継ぐことができます。

<form method="post" action="{{ route('update_extra_holiday_setting', ['date' => '2020-07']) }}">

update()関数は次のような書き方になります。クエリーパラメーターの取得方法は$request->query()を利用します。

public function update(Request $request){
		$input = $request->get("extra_holiday");
		
		$date = $request->query("date");
		$date = strtotime($date . "-01");
		ExtraHoliday::updateExtraHolidayWithMonth(date("Ym", $date), $input);
		
		...
}

2. <input type="hidden" />で値を引き継ぐ

hidden値は画面表示させずに値を埋め込む処理です。

<form method="post" action="{{ route('update_extra_holiday_setting') }}">
	@csrf
	<input type="hidden" name="ym" value="202007" />
	...
</form>

このようにhidden値で値を引き継ぐ形です。


この場合、update()関数は次のような書き方になります。

public function update(Request $request){
		$input = $request->get("extra_holiday");
		$ym = $request->input("ym");
		
		ExtraHoliday::updateExtraHolidayWithMonth($ym, $input);
		
		...
}

どちらの書き方でも実現可能ですが、今回は書き換え箇所の少ない「2. <input type="hidden" />で値を引き継ぐ」のパターンを使ってみます。


まずCalendarFormViewを修正し、<input type="hidden" />が自動的に付与されるようにします。

app/Calendar/Form/CalendarFormView.php

class CalendarFormView extends CalendarView {
	function render(){
		return parent::render() . 
			 "<input type='hidden' name='ym' value='".$this->carbon->format("Ym")."' />" .
			 "<input type='hidden' name='date' value='".$this->carbon->format("Y-m")."' />";
	}

}

parnet::render()で元々のカレンダー部分を取得して、<input type="hidden" />を後ろに埋め込んでいます。

<input type="hidden" />を埋め込む部分はもちろんテンプレートに書く形でも実装できます。

テンプレートに書く場合はCalendarViewに現在の月を取得する関数を追加する必要があるため、この書き方が一番書き換え箇所が少ない実装となります。

更新処理をシンプルに書くため、ymとdate両方をhiddenで渡しています。


update()関数は次のようになります。

app/Http/Controllers/Calendar/ExtraHolidaySettingController.php

class ExtraHolidaySettingController extends Controller
{
	public function update(Request $request){
		$input = $request->get("extra_holiday");
		$ym = $request->input("ym");
		$date = $request->input("date");
		
		ExtraHoliday::updateExtraHolidayWithMonth($ym, $input);
		return redirect()
			->action("Calendar\ExtraHolidaySettingController@form", ["date" => $date])
			->withStatus("保存しました");
	}
}

action("Calendar\ExtraHolidaySettingController@form", ["date" => $date])

更新後に「/extra_holiday_setting?date=2020-07」に遷移できるように第2引数で引き継ぐように設定しています。


指定した月の臨時営業日が設定できるか、ブラウザで動作を確認してみましょう。

スクリーンショット 2020-08-01 0.53.02


# まとめ

今回は機能としては簡単な前後の月へのナビゲーションの開発方法について解説しました。

一つの機能でも実装方法のパターンがいくつもあることを紹介しました。

今回採用しなかったパターンについては学習課題としてチャレンジしてみてください。

・URLをクエリーではなくパスを使うパターン(/calendar/2020/07)
・月の指定をURLではなくセッションに使うパターン
・臨時営業日設定で月指定をクエリパラメーターを引き継ぐパターン


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