はじめに
Laravel7学習の際に「これ絶対忘れそうだな〜」とか「どっかで使ったけどどこに書いてたっけ?」とか、対象となりそうなところに絞って内容をまとめていきます。
自分の備忘録であり、忘れそうなところをまとめたインデックスです。
Laravel7では、ルーティング、様々な重要処理、Viewの継承に分けてまとめていきます。
対象読者
「routeのRoute::resource、Auth::Routes()の動きを忘れそうな人」
「ミドルウェア・アクセサの使い方がいまいちわからない人」
「laravelの簡易ログイン作成方法を知りたい人」
「フォームが一つでそれぞれ別のテーブルに記録したい人(実際はフォームが一つはあまり良くないらしい。。。)」
「viewの継承の使い方を知りたい人」
というプログラミング初心者です。
プログラミングはじめて八ヶ月程度でフレームワークを使い始めたのも最近なので、温かい目で見て頂けたらと。。
ご質問、ご指摘ありましたら遠慮なくご連絡下さい。
では、書いていきます。
参考文献(個人的な物も含む)
入門Laravelチュートリアル (1) イントロダクションと環境構築
https://www.hypertextcandy.com/laravel-tutorial-introduction/
Laravel入門 - 使い方チュートリアル -
https://qiita.com/sano1202/items/6021856b70e4f8d3dc3d
Laravel(EC2)CircleCIによるデプロイ(deploy)の自動化
https://noumenon-th.net/programming/2020/05/02/circleci-laravel-ec2-deploy/
作成に関係ありませんが、個人的にaws、circleciの構築に大変役に立ったので残しておきます。
Laravel 7.x で Auth を利用する
https://qiita.com/kapibarasensei/items/c6b40366505f94103c35
Dockerでphpコンテナとかにnpmをインストールするときのメモ
https://tsyama.hatenablog.com/entry/docker-not-found-npm
上記2つはlaravel7のnpmをdockerfileに追加してAuthを利用可能にするまで
ルーティング
ルートの命名
Route::get('/record', 'RecordController@index')->name('record');
Route::post('/record', 'RecordController@post');
{{ route('record') }}
今回ディレクトリの指定は/recordとしています。
Route::get()->name();とすることで
getでもpostでも{{ route('') }}で呼び出すことができます。
名前をつけてあとで呼び出せるのは URL だけなので、同じ URL で HTTP メソッド違い(delete,putなど)のルートがいくつかある場合はどれか一つに名前をつければ OK です。
Auth::routes();
make:authで下記のようなRouteが生成されます。
Auth::routes(); /これだけで下記のroute取得/
php artisan route:list
+--------+----------+-------------------------+------+-----------------------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+-------------------------+------+-----------------------------------------------------------------+------------+
| | GET|HEAD | / | | Closure | |
| | GET|HEAD | home | | App\Http\Controllers\HomeController@index | web,auth |
| | GET|HEAD | login | | App\Http\Controllers\Auth\AuthController@showLoginForm | web,guest |
| | POST | login | | App\Http\Controllers\Auth\AuthController@login | web,guest |
| | GET|HEAD | logout | | App\Http\Controllers\Auth\AuthController@logout | web |
| | POST | password/email | | App\Http\Controllers\Auth\PasswordController@sendResetLinkEmail | web,guest |
| | POST | password/reset | | App\Http\Controllers\Auth\PasswordController@reset | web,guest |
| | GET|HEAD | password/reset/{token?} | | App\Http\Controllers\Auth\PasswordController@showResetForm | web,guest |
| | GET|HEAD | register | | App\Http\Controllers\Auth\AuthController@showRegistrationForm | web,guest |
| | POST | register | | App\Http\Controllers\Auth\AuthController@register | web,guest |
+--------+----------+-------------------------+------+-----------------------------------------------------------------+------------+
php artisan route:listで現在のrouteリストが確認できます。
ちょっとわかりづらいのでget,postでまとめてControllerを省略します。
//トップページ
Route::get('/home', 'HomeController@index');
//登録
Route::get('/register', 'AuthController@showRegistrationForm');
Route::post('/register ', 'AuthController@register');
//ログイン
Route::get('/login', 'AuthController@showLoginForm');
Route::post('/login ', 'AuthController@login');
//ログアウト
Route::get('/logout', 'AuthController@logout');
//パスワードメール送信
Route::post('password/email', 'PasswordController@sendResetLinkEmail');
//パスワードリセット
Route::get('password/reset/{token?}', 'PasswordController@showResetForm');
Route::post('password/reset', 'PasswordController@reset');
順番を入れ替えて、それぞれ分けて書いてみました。
登録・ログイン・リセットのgetメソッドがshow{}Formになっていたのは新しい発見。
HomeController以外、App\Http\Controllers\Auth以下に存在します。
namespace App\Http\Controllers\Auth;
上記の記述がControllerに書かれています。
軽くググったらnamespaceは「名前空間:ファイルの居場所を示す」だそうです。なるほど。
{{ route('register') }}
{{ route('login') }}
{{ route('logout') }}
{{ route('password.email') }}
{{ route('password.update') }}
上記でget,postともに呼び出せます。('password/reset/{token?}'は例外)
パスワードリセットpost('password/reset')なのに{{ route('password.update') }}なのわかりにくい。
Restfulリソースコントローラ
Route::resource('book','BookController');
下記のRestfulなルーティングを行うことができます。
Route::get('/book', 'BookController@index'); //一覧画面の表示
Route::get('/book/{book}', 'BookController@show'); //詳細画面の表示
Route::get('/book/create', 'BookController@create'); //登録画面の表示
Route::post('/book', 'BookController@store'); //登録処理
Route::get('/book/{book}/edit', 'BookController@edit'); //編集画面の表示
Route::put('/book/{book}', 'BookController@update'); //編集処理
Route::delete('/book/{book}', 'BookController@destroy'); //削除処理
Route::resource();にルート命名はできないので'/book~'で呼び出します。
<a href="/book"></a>
この場合、getでindexメソッドを呼び出し、一覧画面の表示をします。
様々な重要処理
ミドルウェア
認証を求めるミドルウェアはデフォルトで用意されていますので、routes/web.php でルートにミドルウェアを適用します。
Route::group(['middleware' => 'auth'], function() {
// いままで定義してきたルート
});
これで認証ユーザーしか「いままで定義してきたルート」内にアクセスできません。
ですが、その中で「指定したユーザーしかアクセスできないようにする」ためには別でポリシークラスを設定する必要があります。
コマンドラインから以下のコマンドでポリシークラスを作成できます。
php artisan make:policy FolderPolicy
app/Policies/FolderPolicy.php を以下の内容で編集します。
<?php
namespace App\Policies;
use App\Folder;
use App\User;
class FolderPolicy
{
/**
* フォルダの閲覧権限
* @param User $user
* @param Folder $folder
* @return bool
*/
public function view(User $user, Folder $folder)
{
return $user->id === $folder->user_id;
}
}
これで「ユーザーidとフォルダーuser_idが結びついてるときのみ許可する」という認証ルールが作成されました。
フォルダーを作成する時にユーザーid=>フォルダーuser_idとするので、「作成したフォルダーを保有しているのはuser_idである」とテーブルに必ず記録しておきます。
作成したポリシーは AuthServiceProvider に登録します。
<?php
namespace App\Providers;
use App\Folder; // ★ 追加
use App\Policies\FolderPolicy; // ★ 追加
protected $policies = [
Folder::class => FolderPolicy::class,
];
Folder モデルに対する処理への認可には FolderPolicy ポリシーを使用する、という意味です。
簡単に言うと、Folderを取り出すときには「FolderPolicyを通りますよー」ということです。
では作成したポリシーを使用します。
いくつかの方法がありますが、今回はミドルウェアから呼び出して使用する方法を紹介します。
Route::group(['middleware' => 'can:view,folder'], function() {
Route::get('/folders/{folder}/tasks', 'TaskController@index')->name('tasks.index');
});
(view,folder)はカンマ区切りになっていて、カンマの左側が認可処理の種類、右側がポリシーに渡すルートパラメーター(URL の変数部分)を示します。
canが適切な認可処理を判定して、trueなら後続処理、falseならコード403でレスポンスを返します。
リレーションが存在しない
次にリレーションが存在しない(テーブル同士の関係性がない)パターンについて考えます。
おそらくポリシークラスでも同様に作成できますが、コード403(権限がない)で返すため、そのフォルダー(今回でいえばタスク)は存在すると知られてしまうので、コード404(存在しない)で返すようにします。
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);
}
}
チェックの処理をメソッドに切り出します。
これもポリシークラスと同様に「checkRelationを通りますよー」という形です。
条件も同様で、フォルダーidがタスクfolder_idと異なっているならコード404を返す処理になっています。
メソッドを切り出したら、$this->checkRelation(,);で使用できます。
エラー画面404(403、500)作成
@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
これで作成した画面がエラー画面として表示されます。
アクセサ
アクセサとは、モデルクラスが本来持つデータを加工した値を、さもモデルクラスのプロパティであるかのように参照できる Laravel の機能です。
簡単に言うと、「本来持つプロパティのデータを加工して、新しいデータを変更・追加したりした後もともとプロパティに存在するように振る舞う」という感じですかね。
人からノート見せて貰って写したけど、ちょっと内容変えて自分の名前で提出したみたいなw。
今回は独自カラムを作成したので、その方法を紹介します。
protected $appends = array();によって、「もともとプロパティに存在するように振る舞う」ことができるようになります。
今回作成したのがこちらです。
/**
* 状態定義
*/
const RECORD = [
'flg' => '出勤',
'class' => 'text-primary'
];
// 独自カラム
protected $appends = array('go_flg','go_class');
// 独自カラムのアクセサ
public function getGoFlgAttribute()
{
return self::RECORD['flg'];
}
// 独自カラムのアクセサ
public function getGoClassAttribute()
{
return self::RECORD['class'];
}
これで'go_flg','go_class'を呼び出せば、RECORD['flg']、RECORD['class']を取得することができます。
このメリットは、テーブルに余計なプロパティを追加する必要がなくなるので、テーブルを汚さずに追加したい要素を作成できます。
コントローラー側で下記を追記することでカラムを追加できます。
// 独自カラムを追加する
$gorecords->append('go_flg','go_class')->toArray();
これでプロパティを呼び出せば、画面に表示されます。
簡易ログイン
laravel簡易登録・簡易ログインと調べても出てこなかったので、ここで書いておこうと思います。
時間かかったわりに簡単でしたw。
// 認証処理をパスしてログイン
$user = User::find($user_id);
Auth::login($user,true);
//こっちは一行で書ける
Auth::loginUsingId(1, true);
Auth::login();で認証処理をパスしてログインできます。
ユーザーidの指定、第2引数にtrueをつけることでログイン保持ができます。
Auth::loginUsingId();とすれば一行で記述できます。(今回管理者を指定したいので第一引数に1を指定しています)
1つのフォームでテーブルを分けて保存する
Controller側で分岐処理を入れる形です。送信ボタンにname属性を持たせます。
今回であればbladeから送信するnameはgo、leaveです。
public function post(Request $request){
if ($request->input('go')){
if($request->name == null){
$message = ['ユーザー名を選択して下さい'];
return redirect()->route('record')->withInput($message);
}
$message = ['出勤しました'];
// Gorecordモデルのインスタンスを作成する
$gorecord = new Gorecord();
// 送信されたユーザー情報を代入する
$gorecord->user_name = $request->name;
$gorecord->record_date = Carbon::now();
$gorecord->record_time = Carbon::now();
// インスタンスの状態をデータベースに書き込む
$gorecord->save();
return redirect()->route('record')->withInput($message);
}elseif ($request->input('leave')){
if($request->name == null){
$message = ['ユーザー名を選択して下さい'];
return redirect()->route('record')->withInput($message);
}
$message = ['退勤しました'];
// Leaverecordモデルのインスタンスを作成する
$Leaverecord = new Leaverecord();
// 送信されたユーザー情報を代入する
$Leaverecord->user_name = $request->name;
$Leaverecord->record_date = Carbon::now();
$Leaverecord->record_time = Carbon::now();
// インスタンスの状態をデータベースに書き込む
$Leaverecord->save();
return redirect()->route('record')->withInput($message);
}
}
$requestにinput('go')であればそのまま処理、input('leave')であればelseif下の処理が実行されます。
補足ですが、格納した変数を->withInput();でリダイレクトに送ることができます。
リダイレクト側は $request->old(); で変数を受け取れます。
Viewの継承
@yield('')
まず親ビューとなるtemplate.blade.phpを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<!-- viewport meta -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<省略>
</head>
<body>
<!-- ヘッダー -->
<header>
<nav class="navbar navbar-light bg-lightblue fixed-top font-small font-weight-bold" id="header">
<div class="container-fluid">
@guest
<a class="header_title " href="/">
@else
<a class="header_title " href="/user">
@endguest
<i class="fas fa-door-open"></i>
{{ config('app.name', 'Attendance') }}
</a>
<!-- ハンバーガーメニュー -->
<div id="js-nav-toggle">
<div class="nav-toggle">
<span></span>
<span></span>
<span></span>
</div>
</div>
<nav class="header_nav">
<ul id="nav-toggle-2">
@guest
<省略>
@else
<省略>
@endguest
</ul>
</nav>
</div>
</nav>
</header>
@yield('content')
<!-- フッター -->
<footer>
<div class="container-fluid bg-lightblue font-small" id="footer">
<div class="row text-center pt-2">
<!-- 各種操作一覧 -->
@guest
<省略>
@else
<省略>
@endguest
</div>
</div>
<!-- Copyright -->
<div class="container-fluid bg-brown text-white" id="copyright">
<div class="row text-center">
<div class="col-12">
Copyright(C) Attendance.All Rights Reasearved.
</div>
</div>
</div>
</footer>
</body>
親ビューの@yield('content')に小ビューの@section('content')~@endsectionで囲った部分が挿入されます。
親の持っている機能を子が使うというイメージですね。
なお、@guestは認証ユーザーでなければそのまま処理、そうでなければ@else後続の処理が実行されます。
逆にしたい場合は @if(Auth::check())~@else~@endifを使用します。
@extends('layouts.template')
@section('content')
<!-- メインメニュー -->
<h1 id="js-show-msg" class="msg-slide text-center font-weight-bold" style="display: none;">
{{ $message[0] }}
</h1>
<div class="container-fluid padding" id="main">
<省略>
@endsection
今回ファイルの場所がviews/layouts/templateなので@extends('layouts.template');とすることで親ビューを継承できます。
@include('', ['target' => ''])
@include('', ['target' => ''])を使用すると他のファイルを簡単に取得できます。
targetは下記のように記述します。
@if($target == 'store')
<form action="/book" method="post">
@elseif($target == 'update')
<form action="/book/{{ $book->id }}" method="post">
<input type="hidden" name="_method" value="PUT">
@endif
@elseif($target == '')のtargetを増やせば、いくつでも表示の変更が可能です。
(増えすぎるとコードが見にくくなるので、2,3つだけにするべき)
CSRF対策
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<中略>
<form action="/record" method="post">
@csrf
最後に
書いてたらこれも忘れそう、あれも必要かもと色々脱線してしまいました。。
色々書きたい記事は出てきたので、また忘れそうなことをアウトプットしていきます!
長い記事(わたしのインデックス)を読んでくださりありがとうございました!