はじめに
私はプログラミング歴1年の初心者です。
実務でWebサイトのコーディングを1年間行ってきました。
そろそろシステム開発もできるようになりたいということで
LaravelやReactをこれから勉強していこうと思っております。
この記事の目的
Laravelのチュートリアルを進めて行く中で
Laravelの主要な仕組みを理解することです。
この記事は前回の記事があるので、もしご覧になっていない方がいればこちらからどうぞ。
参考記事(チュートリアル記事)
こちらのTodoアプリを作成するチュートリアル記事を参考にさせていただきました。
すごく丁寧に説明してくださっているのですごく勉強になりました。
目次
(1)開発環境
(2)アプリケーション設計
(3)プロジェクト作成
(4)一覧機能
(5)作成機能
(6)編集機能
↓今回はここから(認証機能の後半から)↓
(7)認証機能
(8)エラーハンドリング
(9)デプロイ
認証機能(後半戦!)
ヘッダーの出し分け
ポイントは
○Auth::check()はログインしていればtrue、してなければfalseを返すこと。
※Auth::guest()という逆の働きをするメソッドもある。
○Auth::user()でログインしているユーザーを取得できる。
○今回のログアウトはでやりたかったので、JSを使って実装。
// headerの中身を変更する
変更前
<nav class="my-navbar">
<a class="my-navbar-brand" href="/">ToDo App</a>
</nav>
↓↓↓↓↓↓↓↓↓
変更後
<nav class="my-navbar">
<a class="my-navbar-brand" href="/">ToDo App</a>
<div class="my-navbar-control">
@if(Auth::check())
<span class="my-navbar-item">ようこそ, {{ Auth::user()->name }}さん</span>
|
<a href="#" id="logout" class="my-navbar-item">ログアウト</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
@else
<a class="my-navbar-item" href="{{ route('login') }}">ログイン</a>
|
<a class="my-navbar-item" href="{{ route('register') }}">会員登録</a>
@endif
</div>
</nav>
// main下を変更する
変更前
<main>
@yield('content')
</main>
@yield('scripts')
</body>
</html>
↓↓↓↓↓↓↓↓↓
変更後
<main>
@yield('content')
</main>
@if(Auth::check())
<script>
document.getElementById('logout').addEventListener('click', function(event) {
event.preventDefault();
document.getElementById('logout-form').submit();
});
</script>
@endif
@yield('scripts')
</body>
</html>
ミドルウェアを使ってページに認証を求める
ポイントは
○ミドルウェアはルーティングごとに移る前に実行されるプログラムのこと。
○認証状態の確認というのは、各ルートに共通した確認事項なので、こちらで実装しましょうということ。
Routingの設定
ポイントは
○Route::group()の記述でRoutingをまとめること。
○認証ミドルウェアの'auth'の記述は、Kernel.phpにある。
○'auth'というのは、ログインしていたら中のRoutingに進み、ログインしていなかったらログイン画面にリダイレクトさせるもの。
Route::group(['middleware' => 'auth'], function() {
Route::get('/', 'App\Http\Controllers\HomeController@index')->name('home');
Route::get('/folders/{id}/tasks', 'App\Http\Controllers\TaskController@index')->name('tasks.index');
Route::get('/folders/create', 'App\Http\Controllers\FolderController@showCreateForm')->name('folders.create');
Route::post('/folders/create', 'App\Http\Controllers\FolderController@create');
Route::get('/folders/{id}/tasks/create', 'App\Http\Controllers\TaskController@showCreateForm')->name('tasks.create');
Route::post('/folders/{id}/tasks/create', 'App\Http\Controllers\TaskController@create');
Route::get('/folders/{id}/tasks/{task_id}/edit', 'App\Http\Controllers\TaskController@showEditForm')->name('tasks.edit');
Route::post('/folders/{id}/tasks/{task_id}/edit', 'App\Http\Controllers\TaskController@edit');
});
Auth::routes();
ミドルウェアを編集して、ログイン前のみ閲覧出来るページを探す
ポイントは
○redirect先をRouteServiceProvider::HOMEから'/'にすること。
○認証ミドルウェアの'guest'の記述は、Kernel.phpにある。
○RedirectIfAuthenticatedはRegisterController.phpやLoginController.phpのコンストラクタで適用されている。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/'); // ★ 引数を変更
}
return $next($request);
}
コントローラーでログインユーザーに紐付いたフォルダを作成できるようにする
ポイントは
○Auth::user()でログインユーザーモデルを取得していること。
○リレーションを利用してフォルダモデルを保存している。
// ★ Authクラスをインポートする
use Illuminate\Support\Facades\Auth;
class FolderController extends Controller
{
// 中略
public function create(CreateFolder $request)
{
// フォルダモデルのインスタンスを作成する
$folder = new Folder();
// タイトルに入力値を代入する
$folder->title = $request->title;
// ★ ユーザーに紐づけて保存
Auth::user()->folders()->save($folder);
// 以下略
コントローラーでログインユーザーとしてタスクを表示できるようにする
ポイントは
○上記と同じ
// ★ Authクラスをインポートする
use Illuminate\Support\Facades\Auth;
class TaskController extends Controller
{
public function index(int $id)
{
// ★ ユーザーのフォルダを取得する
$folders = Auth::user()->folders()->get();
// 以下略
フォルダを作成済みであればログイン後にタスク一覧にリダイレクト
ポイントは
○HomeController.phpで設定すること
// 以下を追記
use Illuminate\Support\Facades\Auth;
public function index()
{
// ログインユーザーを取得する
$user = Auth::user();
// ログインユーザーに紐づくフォルダを一つ取得する
$folder = $user->folders()->first();
// まだ一つもフォルダを作っていなければホームページをレスポンスする
if (is_null($folder)) {
return view('home');
}
// フォルダがあればそのフォルダのタスク一覧にリダイレクトする
return redirect()->route('tasks.index', [
'id' => $folder->id,
]);
}
パスワード再設定機能
ポイントは
○パスワード再設定のために必要なメールをMailtrapというサービスを使って実現すること。
○.envで設定すること。
○auth/passwords/email.blade.phpの編集。
// MailtrapのLaravel対応の設定をする(Demo inboxのSMTP設定から確認できる)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=USERNAME
MAIL_PASSWORD=PASSWORD
MAIL_ENCRYPTION=ENCRYPTION
MAIL_FROM_ADDRESS=todoapp2@email.com
MAIL_FROM_NAME="todoapp2"
// パスワード再設定メール送信ページ
@extends('layout')
@section('content')
<div class="container">
<div class="row">
<div class="col col-md-offset-3 col-md-6">
<nav class="panel panel-default">
<div class="panel-heading">パスワード再発行</div>
<div class="panel-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
<form action="{{ route('password.email') }}" method="POST">
@csrf
<div class="form-group">
<label for="email">メールアドレス</label>
<input type="text" class="form-control" id="email" name="email" />
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">再発行リンクを送る</button>
</div>
</form>
</div>
</nav>
</div>
</div>
</div>
@endsection
// パスワード再設定ページ
@extends('layout')
@section('content')
<div class="container">
<div class="row">
<div class="col col-md-offset-3 col-md-6">
<nav class="panel panel-default">
<div class="panel-heading">パスワード再発行</div>
<div class="panel-body">
<form action="{{ route('password.update') }}" method="POST">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group">
<label for="email">メールアドレス</label>
<input type="text" class="form-control" id="email" name="email" required autocomplete="email" autofocus/>
</div>
<div class="form-group">
<label for="password">新しいパスワード</label>
<input type="password" class="form-control" id="password" name="password" required autocomplete="new-password"/>
</div>
<div class="form-group">
<label for="password-confirm">新しいパスワード(確認)</label>
<input type="password" class="form-control" id="password-confirm" name="password_confirmation" required autocomplete="new-password"/>
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</div>
</nav>
</div>
</div>
</div>
@endsection
protected $redirectTo = '/';
メッセージの日本語化
ポイントは
○ForgotPasswordController.phpとResetPasswordController.phpのどちらも修正する方法ではなく、validation.phpで一括で変更している点。(アプリ全体に反映される)
○lang/ja/passwords.phpを編集する。
// 以下を追記
'attributes' => [
'email' => 'メールアドレス',
'password' => 'パスワード',
'token' => 'トークン',
],
$ cp ./resources/lang/en/passwords.php ./resources/lang/ja/
// 以下を編集
'reset' => 'パスワードを再設定しました。',
'sent' => 'パスワード再設定リンクを送信しました。',
'token' => 'トークンが無効です。',
'user' => "入力されたメールアドレスのユーザーは見つかりませんでした。",
メールの内容を変更する
ポイントは
○メールの内容はresources/views/mail/password-reset.blade.phpで編集できること。
○Mailableクラスがメールの送信を司ること。
○subject()メソッドは件名を決めるメソッド。
○Userモデルにパスワード再設定メールの設定をする。
○ちなみにtokenは2014_10_12_100000_create_password_resets_table.phpを見ると、ちゃんと、tokenのカラムがあることがわかる。
詳しくは以下を参照にしてください。
// resources/views/mail/password-reset.blade.phpを作成する
$ mkdir resources/views/mail
$ touch resources/views/mail/password-reset.blade.php
<a href="{{ route('password.reset', ['token' => $token]) }}">
パスワード再設定リンク
</a>
// Mailableクラスの雛形を作成する
$ php artisan make:mail ResetPassword
変更前
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ResetPassword extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('view.name');
}
}
↓↓↓↓↓↓↓↓↓↓
変更後
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ResetPassword extends Mailable
{
use Queueable, SerializesModels;
public $token;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject('パスワード再設定')->view('mail.password-reset');
}
}
use App\Mail\ResetPassword; // ★ 追加
use Illuminate\Support\Facades\Mail; // ★ 追加
class User extends Authenticatable
{
// 略
/**
* ★ パスワード再設定メールを送信する
*/
public function sendPasswordResetNotification($token)
{
Mail::to($this)->send(new ResetPassword($token));
}
// 略
}
エラーハンドリング
ポイントは
○エラーハンドリングとは、予期しないリクエストに対するエラーに対応することです。
○レスポンスステータスコードがあるので、それぞれに適切なコードを選択するようにすること。
○abort関数を使ってエラー系のレスポンスを返却すること。(練習)
○ルートモデルバインディングを使って、一連の流れをフレームワークに任せることができるようにする。
○ルートとモデルを紐付けるのが、ルートモデルバインディング機能。
○これによって、URL中のID部分に該当するフォルダデータがコントローラーに渡されるようになるので、abortしなくても自動で404エラー画面に遷移する。
存在しないフォルダIDにアクセスで404エラー(練習)
// 存在しないフォルダIDにアクセスすると404が返される。
public function index(int $id)
{
// 略
// 選ばれたフォルダを取得する
$current_folder = Folder::find($id);
if (is_null($current_folder)) {
abort(404);
}
// 略
}
ルートモデルバインディングを使って、abortを使わずに処理する。
// {id}から{folder}に変更する
Route::get('/folders/{folder}/tasks', 'App\Http\Controllers\TaskController@index')->name('tasks.index');
// 以下のように編集する
変更前
public function index(int $id)
{
// ログインユーザーのフォルダを取得する
$folders = Auth::user()->folders()->get();
// 選ばれたフォルダを取得する
$current_folder = Folder::find($id);
if(is_null($current_folder)) {
abort(404);
}
// 選ばれたフォルダに紐づくタスクを取得する
$tasks = $current_folder->tasks()->get();
return view('tasks/index', [
'folders' => $folders,
'current_folder_id' => $current_folder->id,
'tasks' => $tasks,
]);
}
↓↓↓↓↓↓↓↓↓↓
変更後
public function index(Folder $folder)
{
// ログインユーザーのフォルダを取得する
$folders = Auth::user()->folders()->get();
// 選ばれたフォルダに紐づくタスクを取得する
$tasks = $folder->tasks()->get();
return view('tasks/index', [
'folders' => $folders,
'current_folder_id' => $folder->id,
'tasks' => $tasks,
]);
}
タスクの作成と編集もルートモデルバインディング化する
// {id}から{folder}に、{task_id}から{task}に変更する
Route::get('/folders/{folder}/tasks/create', 'App\Http\Controllers\TaskController@showCreateForm')->name('tasks.create');
Route::post('/folders/{folder}/tasks/create', 'App\Http\Controllers\TaskController@create');
Route::get('/folders/{folder}/tasks/{task}/edit', 'App\Http\Controllers\TaskController@showEditForm')->name('tasks.edit');
Route::post('/folders/{folder}/tasks/{task}/edit', 'App\Http\Controllers\TaskController@edit');
// createとedit関係を編集する
変更前
public function showCreateForm(int $id)
{
return view('tasks/create', [
'folder_id' => $id,
]);
}
public function create(int $id, CreateTask $request)
{
$current_folder = Folder::find($id);
$task = new Task();
$task->title = $request->title;
$task->due_date = $request->due_date;
$current_folder->tasks()->save($task);
return redirect()->route('tasks.index', [
'id' => $current_folder->id,
]);
}
/**
* GET /folders/{id}/tasks/{task_id}/edit
*/
public function showEditForm(int $id, int $task_id)
{
$task = Task::find($task_id);
return view('tasks/edit', [
'task' => $task,
]);
}
public function edit(int $id, int $task_id, EditTask $request)
{
// リクエストされたIDのタスクデータを取得する
$task = Task::find($task_id);
// そのタスクデータに入力された値を代入する
$task->title = $request->title;
$task->status = $request->status;
$task->due_date = $request->due_date;
$task->save();
// 編集対象のタスクが属するタスク一覧画面にリダイレクトする
return redirect()->route('tasks.index', [
'id' => $task->folder_id,
]);
}
}
↓↓↓↓↓↓↓↓↓↓
変更後
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Folder;
use App\Models\Task;
use App\Http\Requests\CreateTask;
use App\Http\Requests\EditTask;
use Illuminate\Support\Facades\Auth;
class TaskController extends Controller
{
/**
* タスク一覧
* @param Folder $folder
* @return \Illuminate\View\View
*/
public function index(Folder $folder)
{
// ログインユーザーのフォルダを取得する
$folders = Auth::user()->folders()->get();
// 選ばれたフォルダに紐づくタスクを取得する
$tasks = $folder->tasks()->get();
return view('tasks/index', [
'folders' => $folders,
'current_folder_id' => $folder->id,
'tasks' => $tasks,
]);
}
/**
* タスク作成フォーム
* @param Folder $folder
* @return \Illuminate\View\View
*/
public function showCreateForm(Folder $folder)
{
return view('tasks/create', [
'folder' => $folder->id,
]);
}
/**
* タスク作成
* @param Folder $folder
* @param CreateTask $request
* @return \Illuminate\Http\RedirectResponse
*/
public function create(Folder $folder, CreateTask $request)
{
$task = new Task();
$task->title = $request->title;
$task->due_date = $request->due_date;
$folder->tasks()->save($task);
return redirect()->route('tasks.index', [
'folder' => $folder->id,
]);
}
/**
* タスク編集フォーム
* @param Folder $folder
* @param Task $task
* @return \Illuminate\View\View
*/
public function showEditForm(Folder $folder, Task $task)
{
return view('tasks/edit', [
'task' => $task,
]);
}
/**
* タスク編集
* @param Folder $folder
* @param Task $task
* @param EditTask $request
* @return \Illuminate\Http\RedirectResponse
*/
public function edit(Folder $folder, Task $task, EditTask $request)
{
// そのタスクデータに入力された値を代入する
$task->title = $request->title;
$task->status = $request->status;
$task->due_date = $request->due_date;
$task->save();
// 編集対象のタスクが属するタスク一覧画面にリダイレクトする
return redirect()->route('tasks.index', [
'folder' => $task->folder_id,
]);
}
}
Homeのルートモデルバインディング化する
エラーがでてきたので、Homeコントローラーもやっておこう。
// idからfolderに編集する
変更前
public function index()
{
// ログインユーザーを取得する
$user = Auth::user();
// ログインユーザーに紐づくフォルダを一つ取得する
$folder = $user->folders()->first();
// まだ一つもフォルダを作っていなければホームページをレスポンスする
if (is_null($folder)) {
return view('home');
}
// フォルダがあればそのフォルダのタスク一覧にリダイレクトする
return redirect()->route('tasks.index', [
'id' => $folder->id,
]);
}
↓↓↓↓↓↓↓↓↓↓↓↓
変更後
use App\Models\Folder;
public function index(Folder $folder)
{
// ログインユーザーを取得する
$user = Auth::user();
// ログインユーザーに紐づくフォルダを一つ取得する
$folder = $user->folders()->first();
// まだ一つもフォルダを作っていなければホームページをレスポンスする
if (is_null($folder)) {
return view('home');
}
// フォルダがあればそのフォルダのタスク一覧にリダイレクトする
return redirect()->route('tasks.index', [
'folder' => $folder->id,
]);
}
テンプレート内のリンクもルートバインディング化する
エラーが出たので、テンプレートのリンクも修正しよう。
ポイントは
○おそらく最初からルートバインディングシていたほうが良さそう。
// リンク指定の中の'id'を'folder'にする
// リンク指定の中の'task_id'を'task'にする
フォルダの作成についてもルートバインディング化する
// 'id'を'folder'に変更する
権限がないコンテンツを403エラーで返す(練習)
ポイントは
○権限がないコンテンツに関しては403エラーで返すこと。
○この記述は一旦試したらけしておくこと。
// 403エラーで返す
public function index(Folder $folder)
{
if (Auth::user()->id !== $folder->user_id) {
abort(403);
}
// 以下略
}
ポリシークラスを使って、abortを使わずに403エラーを表示する
ポイントは
○ポリシークラスはLaravelでの認可処理を司ること。
○このポリシークラスでの設定したviewメソッドは「ユーザーとフォルダが紐付いているときに認可する」という意味が定義される。
○作成したポリシーはAuthServiceProviderに登録する。
○Folderモデルに対する処理への認可にはFoderPolicyポリシーを使用するということ。
○ミドルウェアを介してポリシーを使用する。
$ php artisan make:policy FolderPolicy
// 以下を追記
use App\Models\Folder;
/**
* フォルダの閲覧権限
* @param User $user
* @param Folder $folder
* @return bool
*/
public function view(User $user, Folder $folder)
{
return $user->id === $folder->user_id;
}
use App\Models\Folder; // 追加
use App\Policies\FolderPolicy; // 追加
protected $policies = [
Folder::class => FolderPolicy::class,
];
// 以下の変更を加えて、ミドルウェアを適用させてポリシーを呼び出す。
Route::group(['middleware' => 'can:view,folder'], function() {
Route::get('/folders/{folder}/tasks', 'App\Http\Controllers\TaskController@index')->name('tasks.index');
Route::get('/folders/{folder}/tasks/create', 'App\Http\Controllers\TaskController@showCreateForm')->name('tasks.create');
Route::post('/folders/{folder}/tasks/create', 'App\Http\Controllers\TaskController@create');
Route::get('/folders/{folder}/tasks/{task}/edit', 'App\Http\Controllers\TaskController@showEditForm')->name('tasks.edit');
Route::post('/folders/{folder}/tasks/{task}/edit', 'App\Http\Controllers\TaskController@edit');
});
(ちなみに全体の見た目は)
Route::group(['middleware' => 'auth'], function() {
Route::get('/', 'App\Http\Controllers\HomeController@index')->name('home');
Route::get('/folders/create', 'App\Http\Controllers\FolderController@showCreateForm')->name('folders.create');
Route::post('/folders/create', 'App\Http\Controllers\FolderController@create');
Route::group(['middleware' => 'can:view,folder'], function() {
Route::get('/folders/{folder}/tasks', 'App\Http\Controllers\TaskController@index')->name('tasks.index');
Route::get('/folders/{folder}/tasks/create', 'App\Http\Controllers\TaskController@showCreateForm')->name('tasks.create');
Route::post('/folders/{folder}/tasks/create', 'App\Http\Controllers\TaskController@create');
Route::get('/folders/{folder}/tasks/{task}/edit', 'App\Http\Controllers\TaskController@showEditForm')->name('tasks.edit');
Route::post('/folders/{folder}/tasks/{task}/edit', 'App\Http\Controllers\TaskController@edit');
});
});
Auth::routes();
フォルダとタスクの紐付けを確認してエラーを返す
ポイントは
○各自にabort(404)でもいいけど、重複を防ぎたいので、privateメソッドで、一つのメソッドにする。
public function showEditForm(Folder $folder, Task $task)
{
$this->checkRelation($folder, $task);
// 以下略
}
public function edit(Folder $folder, Task $task, EditTask $request)
{
$this->checkRelation($folder, $task);
// 以下略
}
private function checkRelation(Folder $folder, Task $task)
{
if ($folder->id !== $task->folder_id) {
abort(404);
}
}
エラー画面を作る
// 403,404,500のファイルを作成する
$ mkdir resources/views/errors
$ touch resources/views/errors/403.blade.php
$ touch resources/views/errors/404.blade.php
$ touch resources/views/errors/500.blade.php
@extends('layout')
@section('content')
<div class="container">
<div class="row">
<div class="col col-md-offset-3 col-md-6">
<div class="text-center">
<p>お探しのページにアクセスする権限がありません。</p>
<a href="{{ route('home') }}" class="btn">
ホームへ戻る
</a>
</div>
</div>
</div>
</div>
@endsection
@extends('layout')
@section('content')
<div class="container">
<div class="row">
<div class="col col-md-offset-3 col-md-6">
<div class="text-center">
<p>お探しのページは見つかりませんでした。</p>
<a href="{{ route('home') }}" class="btn">
ホームへ戻る
</a>
</div>
</div>
</div>
</div>
@endsection
@extends('layout')
@section('content')
<div class="container">
<div class="row">
<div class="col col-md-offset-3 col-md-6">
<div class="text-center">
<p>サーバー上のエラーが起きました。申し訳ありませんが、以下のリンクからホーム画面へお戻りください。</p>
<a href="{{ route('home') }}" class="btn">
ホームへ戻る
</a>
</div>
</div>
</div>
</div>
@endsection
今回はここで終了します。
いよいよ次回大詰めです。
ここまででアプリの開発自体は終了です。
ということで、次回はデプロイをする感じです。
それでは、ここまでご覧頂きありがとうございました。
次回の記事をお楽しみに。