これは何?
Laravelについて勉強した備忘録です。
何かの参考になれば幸いです。
全体のファイル構造
フォルダ名 or ファイル名 | 内容 |
---|---|
app | ビジネスロジック |
config | アプリケーションの設定 |
database | データベース作成やデフォルトデータ作成 |
public | アクセスした時に最初に参照されるエントリーポイント(index.php) |
resources | html、cssなど見た目に関するファイル群 |
route | ルーティング設定 |
vendor | サードパーティのライブラリ群 |
.env | (重要) 認証情報など環境固有の設定 |
composer.json | phpファイルの依存関係解決 |
package.json | javaScript、フロントエンドの依存関係解決 |
Artisan Command
Laravelが用意しているコマンドラインインターフェイス
使えるコマンドを確認する
php artisan list
自分のコマンドを作成する
php artisan make:command コマンド名
作成されるクラスの中身
プロパティ or メソッド | 内容 |
---|---|
signature | コマンドの名前 |
description | コマンドの説明 |
handle | 実際に実行されるコマンドの中身 |
Tinker
PsySHパッケージを搭載したLaravelフレームワークのREPL
対話的にデータベースなどを参照できる
実行コマンド
php artisan tinker
終了するときはexitかctrl+c
デフォルトだと色がつかないので、色をつける場合の対処法は下記の2通り
-
php artisan tinker --ansi
を実行する -
vendor/psy/psysh/src/configuration.php
の下記をdisabled
からauto
にする
const COLOR_MODE_DISABLED = 'disabled';
ルーティング
静的なものとパラメータなどを持つ動的なものがある。
web.php と api.php の違い
web.php と api.php に分かれているのは、要件が違うから。
api はjson形式のフォーマットのみを返す。
また、一般的なwebサイトはcookieやsessionに依存するが、apiはRateLimit(回数制限?)が必要な場合がある。
ルーティングの例
Route::get('/recent-posts/{days_ago?}', function ($daysAgo = 20) {
return 'Post from ' . $daysAgo . 'days ago';
})->name('posts.recent.index ')->middleware('auth');
?をつけないと必須パラメータになり、その場合、パラメータがないと404が返る
$daysAgo = 20 のように、パラメータにデフォルト値を設定できる
nameをつけると他のファイルでその名前を使える、名前をつけるの推奨
ルーティングの例2
Route::get('/post/{id}', function ($id) use($posts) {
return view('posts.show', ['post' => $posts[$id]]);
})->where([
'id' => '[0-9]+'
])->name('posts.show');
whereでパラメータに制約を付けることができる
whereには配列を渡す
制約には正規表現を使うことができる
ルーティングの例3
Route::prefix('/fun')->name('fun.')->group(function() use($posts) {
Route::get('responses', function() use($posts) {
group でルートのグルーピングができる
ルーティイングの例4
Route::get('/', [HomeController::class, 'home'])->name('home.index')
controllerを指定する場合は[]で囲う
第1引数にコントローラのクラス名、第2引数にコントローラのメソッド名
RouteServiceProviderで共通の制約をつける
RouteServiceProviderのbootメソッド内に記述
Route::pattern('id', '[0-9]+');
Route::patternメソッドで、共通の制約を付けることができる
個別のrouteメソッドにいちいち記載しなくて良いので便利
上記の場合は、idだったら常に1つ以上の数字でないと404になる
ビュー
ルーティングファイルで、下記のように記述することでviewに遷移できる
return view('home.index',[]);
第2引数には配列でviewに渡すパラメータを入れられる
ルーティングファイルでviewに変数を渡す
// $postsは$idをキーに持った配列
Route::get('/post/{id}', function ($id) use($posts) {
abort_if(!isset($posts[$id]), 404);
return view('posts.show', ['post' => $posts[$id]]);
})
上記の場合、postがviewで使える変数の名前になる
abort_ifは条件に当てはまったらエラーを返すヘルパ関数、よく使う
Bladeテンプレートで変数を表示
<h1>{{ $post['title'] }}</h1>
ダブルプレース構文(ヒゲ)に通された変数は、エスケープ処理のためのphpのhtmlspecialchars関数を通る
ルーティングファイルで変数を持たないviewに遷移
Route::view('/contact', 'home.contact')->name('home.contact');
Bladeテンプレート
Blade directive(指令)を使うことで、効率的にviewを作成できる
directiveの例
resources/views/layouts/app.blade.php
を継承
@extends('layouts.app')
継承したテンプレート(親ファイル)の title に Home page を表示
@section('title', 'Home page')
セクションディレクティブの中身が長い場合は、下の書き方
@section('content')
<h1>Welcome to Laravel</h1>
<div>This is the content of the main page</div>
@endsection
親ファイルに記述
継承先のテンプレート(子ファイル)からsectionディレクティブで指定された title をもらう
@yield('title')
phpの if, elseif , else のように使える
@if($post['is_new'])
<div>A new blog post! Using if</div>
@else
<div>Blog post is old! Using elseif/else</div>
@endif
〜でないとき、ifの逆
@unless($post['is_new'])
<div>It is an old post... using unless</div>
@endunless
値がセットされているかどうかの判定
@isset($post['has_comments'])
<div>The post has some comments...</div>
@endisset
ループ処理
@if(count($posts))
@foreach($posts as $key => $post)
<div>{{ $key }}. {{ $post['title'] }}</div>
@endforeach
@else
No posts found!
@endif
foreachループ処理の中では$loop
変数が最初から使える
使えるプロパティについてはLaravel公式を参照
if + foreachの簡略化
@forelse ($posts as $key => $post)
@include('posts.partials.post')
@empty
No posts found!
@endforelse
スキップ(continue)と終了(break)
@foreach ($users as $user)
@continue($user->type == 1)
<li>{{ $user->name }}</li>
@break($user->number == 5)
@endforeach
phpのforと一緒
@for ($i = 0; $i < 10; $i++)
現在の値は、{{ $i }}
@endfor
phpのwhileと一緒
@while (true)
<p>無限ループ中です。</p>
@endwhile
phpの記述をする
@php
$isActive = false;
$hasError = true;
@endphp
部分的に他のファイルを表示する
@include('posts.partials.post')
postsフォルダ → partialsフォルダ → post.blade.php の中身を表示
変数は全てinclude先のファイルに継承される
追加データを渡すこともできる
@include('posts.partials.post', ['status' => 'complete'])
@include + loop を同時にする
@each('posts.partials.post', $posts, 'post')
第1引数は表示するビュー
第2引数は反復対象のコレクション、もしくは配列
第3引数はビュー内で表示する反復要素の変数名
追加データを渡すことはできない
$loop
変数も使えない
ビューコンポーザ(view composer)
ビューをレンダするときに呼び出すコールバックまたはクラスメソッドのこと。
ビューをレンダする際に常に同じデータを渡す場合は、ビューコンポーザでロジックを集約できる。
ビューコンポーザの作成(ディレクトリはどこでもOK)
use Illuminate\View\View;
class ActiveComposer
{
public function compose(View $view)
{
// 省略
$view->with('most_commented', $most_commented);
$view->with('most_active', $most_active);
$view->with('most_active_last_month', $most_active_last_month);
}
}
レンダされる際に実行されるcompose
メソッドを定義する
$view->with()
でviewをレンダする時に常にデータを渡すようにする
AppServiceProvider.php
でViewComposerを記述
public function boot()
{
view()->composer(['posts.index', 'posts.show'], ActiveComposer::class);
// view()->composer('*', ActiveComposer::class);
}
boot
メソッド内でview
ヘルパ関数、composer
メソッドを呼ぶ
第1引数には、対象のviewファイル、すべてを対象にする場合はアスタリスクを記述
第2引数には、定義したview composerクラス
リクエストとレスポンス
レスポンス
ヘルパ関数でレスポンスオブジェクトの作成
response($posts, 201)
第1引数に内容、第2引数にhttpステータス
responseオブジェクトはviewメソッド、headerメソッド、cookieメソッドが使える
response($posts, 201)
->view('/contact', 'home.contact')
->header('Content-Type', 'application/json')
->cookie('MY-COOKIE', 'HOGE HOGE', 3600);
header の第2引数には html か json
cookie の第3引数は有効期限、3600なら1時間
リダイレクトしたい場合は、redirectメソッド
return redirect('/contact')
リダイレクト時にURIではなく、ルートを指定することもできる
return redirect()->route('posts.show', ['id' => 1])
自分のサイト以外にリダイレクトしたい場合
return redirect()->away('https://google.com')
直前のページに戻したい場合
return back()
htmlではなくjson形式のデータを返したい場合
return response()->json($posts);
ファイルをダウンロードさせたい場合
return response()->download(public_path('/daniel.jpg', 'face.jpg'))
public フォルダへのアクセスは public_path メソッド
第3引数にヘッダー情報を配列で渡す
リクエスト
リクエストへのアクセスは2通り
request()
use Illuminate\Http\Request;
function (Request $request)
リクエストの中身すべてへアクセス
request()->all()
配列が返ってくる
リクエストボディとクエリ文字列(URLの末尾に付けたす変数)も表示される
クエリ文字列は文字列で取得されるので数字などはcastする必要がある
リクエストの一部へアクセス
request()->input('page', 1)
第1引数にアクセスしたいキーを指定
第2引数に入力値が存在しない場合のデフォルト値を指定できる
リクエストのクエリ文字列へアクセス
request()->query('page', 1)
チェックボックスなどの真偽値を受け取る場合
request()->boolean('archived');
1、"1"、true、"true"、"on"、"yes"にtrueを返す
受け取りたいパラメータを複数指定する
request()->only('username', 'password');
受け取りたくないパラメータを指定する
request()->except('credit_card')
リクエストに指定された値がすべて存在するか確認する
request()->has(['name', 'email'])
リクエストに値が存在した場合の実行処理
request()->whenHas('name', function ($input) {
// "name"が存在する場合の処理…
}, function () {
// "name"が存在しない場合の処理…
});
リクエストに指定された値のいずれかが存在するか確認する
request()->hasAny(['name', 'email'])
リクエストに値が存在し、空でないことを確認する
request()->filled('name')
リクエストに値が存在し、空でない場合の実行処理
request()->whenFilled('name', function ($input) {
// "name"の値が空でない場合の処理…
}, function () {
// "name"の値が空の場合の処理…
});
ミドルウェア
controllerに入る前と入った後の処理
ミドルウェアにはhandleメソッドのみ存在する
3種類のミドルウェアがある
- グローバルなミドルウェア、すべてに適用される
- webとapiのルートのみに適用される、
kernel.php
で設定 - 特定のルートグループのみに適用される、controllerで設定
kernel.php
での記述
// すべてに適用されるミドルウェア
protected $middleware = [
//
];
// webとapiに適用されるミドルウェア
protected $middlewareGroups = [
'web' => [
//
],
'api' => [
//
],
];
// 特定のルートに適用されるミドルウェア
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
];
controllerで特定のルートにミドルウェアを適用
Route::get('/', [HomeController::class, 'home'])
->name('home.index')
->middleware('auth');
コントローラ
ルートからビューに渡す前に何か処理をしたいときに記述する
コントローラの作成
php artisan make:controller 作成したいコントローラ名 --resource
resourceオプションを指定するとCRUD(create, read, update, delete)のメソッドを自動で作ってくれる
リソースコントローラのルート設定
Route::resource('posts', PostsController::class);
CRUDの一部だけを使いたい場合
Route::resource('posts', PostsController::class)->only([
'index', 'show'
]);
Route::resource('photos', PhotoController::class)->except([
'create', 'store', 'update', 'destroy'
]);
メソッドが一つだけのシングルコントローラ
class AboutController extends Controller
{
public function __invoke()
{
return 'Single';
}
}
__invokeメソッドを使う
設定(config と .env)
Laravelの設定ファイルはconfigフォルダ配下にある
githubなどを使う場合、.envはセキュリティの観点から、コミットしてはいけない
コミットする場合は、.env.exampleに変数のみを記載してコミットすること
例えば、データベースに関する設定はdatabase.php
に記述がある
デフォルトで使うRDMSの設定
'default' => env('DB_CONNECTION', 'mysql'),
第1引数は.envファイルの変数名を記載
第2引数は、.envファイルに記述がない場合のデフォルト値
データベースとMigration
マイグレーションファイルの例
class CreateBlogPostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('blog_posts', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('title')->default('');
$table->text('content');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('blog_posts');
}
}
主キー(id)は必ず必要
timestampsでcreated_atとupdated_atのカラムを作成する
マイグレーションファイルで使えるカラムタイプ一覧
(Laravel公式)マイグレーション 利用可能なカラムタイプ
モデルとマイグレーションファイルを一緒に作成する
php artisan make:model BlogPost -m
ORM(eloquent)とモデル(Model)
クエリをphpライクに記述することができる方法
modelなどにも簡単にアクセスができる
(重要)
返り値はEloquentモデルオブジェクト(Illuminate\Database\Eloquent\Model
)だが、複数返ってくる場合はEloquentコレクション
Eloquentコレクション(Illuminate\Database\Eloquent\Collection
)と基本コレクション(Illuminate\Support\Collection
)は別物、使えるメソッドが違うので注意
保存する
$posts = new BlogPost();
$posts->title = 'I am title';
$posts->content = 'I am content';
$posts->save();
idで探す
$posts = BlogPost::find(1);
該当のIDが存在しない場合はnullを返す
複数のidを探す場合は配列を渡す
$posts = BlogPost::find([1, 2, 3]);
見つかったidのみ返す、エラーは出さない
idで探して、idが存在しない場合exceptionを返す
$posts = BlogPost::findOrFail(1000);
すべてのデータを取得する
$posts = BlogPost::all();
戻り値はeloquentコレクション
取得する値はデフォルトではidの古い順になっている(他のメソッドも)
コレクションから特定の項目だけ取得する
$posts = BlogPost::all()->pluck('id');
pluckはeloquentコレクションではなく基本コレクションのインスタンスを返す
コレクションへのアクセス方法は配列と同じ
$posts[0]
コレクションの最初の値を取得する
$posts->first();
コレクションの数をカウントする
$posts->count();
クエリビルダ(QueryBuilder)
sqlに近い形でクエリを記述できるようにしたもの
クエリビルダの実態はIlluminate\Database\Query\Builder
モデルクラスにはクエリビルダが組み込まれている
SQLのキーワードを下記のような形で使える
Model::where(...)
Model::select(...)
Model::join(...)
クエリビルダの例
User::where('id', '>=', 2)->orderBy('id', 'desc')->take(5)->get();
クエリビルダは静的に呼びだし、ビルダクラスの新しいインスタンスが生成される
(重要)返り値はコレクションオブジェクト(Illuminate\Support\Collection
)
takeメソッドはsqlのlimit句と一緒
takeをgetの後に追加した場合は取得したコレクションに対して実行されるORMの扱い
フォーム
ユーザーが入力した情報を送信する
ポストした時のルーティングを指定
<form action="{{ route('posts.store') }}" method="POST">
CSRF(クロスサイトリクエストフォージェリ)から守る
@csrf
hidden でユニークなトークンをセッションに保存する
フォーム送信処理のルートで送信されたトークンと生成されたトークンが一致するか調べる
app/Http/Middleware/VerifyCsrfToken.php
で確認
クロスサイトリクエストフォージェリとは…
悪意のあるウェブサイトのURLをクリックすると、クリックした時点で、正規のサイトに登録されているパスワードなどを更新する。本来であれば、正規のサイト側で上書きを拒否する仕組みが必要。
ハッキング回避のため、データ送信の method には GET ではなく POST を指定する
バリデーション・検証
送信されるデータについて検証をかけることができる
バリデーションの例
$request->validate([
'title' => 'bail|required|min:5|max:100',
'content' => 'required|min:10',
]);
bailはバリデーションに引っかかった場合、残りのバリデーションルールをチェックしない
上記の場合、titleのmin:5で失敗すると、max:100は検証されない
バリデーションルールで失敗すると、以下の2通りのいずれかになる
- フォームから送信された場合、最後のページにリダイレクトされる(同時にエラーの内容をセッションに保存する)
- Ajaxで呼び出された場合、エラーとともにレスポンスでアドレスを返す
エラーメッセージ
ミドルウェアのShareErrorsFromSession
クラスのおかげで、bladeテンプレート内なら、$errors
変数を使ってエラー内容にいつでもアクセスできる
app/Http/Kernel.php
に記述がある
protected $middlewareGroups = [
'web' => [
// 省略
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
// 省略
],
Blade内ですべてのエラーをまとめて表示する
@if($errors->any())
<div class="mb-3">
<ul class="list-group">
@foreach ($errors->all() as $error)
<li class="list-group-item list-group-item-danger">{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
$errorsを定義していなくてもアクセスできる
Blade内で個別の項目にエラーを表示する
@error('title')
<div class="alert alert-danger">{{ $message }}</div>
@enderror
@errorディレクティブ内でメッセージ内容が格納されている$messageにアクセスできる
エラー内容を各項目ごとに表示する例
<div class="form-group">
<label>Password</label>
<input name="password" required type="password"
class="form-control {{ $errors->has('password') ? 'is-invalid' : '' }}"/>
@if ($errors->has('password'))
<span class="invalid-feedback">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
html の type = password型で入力時にアスタリスクなどに変換してくれる
errors->has('password')でパスワードの項目にエラーがあるかをチェック
errors->first('password)でエラーの最初の内容を取得
class="invalid-feedback"はbootstrapの書き方でエラー表示に使う
バリデーションルールをカプセル化する
フォームリクエストクラスの作成
php artisan make:request StorePostRequest(クラス名)
authorize
メソッドでフォームを送信できるユーザーかどうかをチェック
バリデーションルールはrules
メソッドに記載
public function rules()
{
return [
'title' => 'bail|required|min:5|max:100',
'content' => 'required|min:10',
];
}
controllerでバリデーション後の値を取得
public function store(StorePostRequest $request)
{
$validated = $request->validated();
//
}
フォームリクエストクラスをタイプヒントし、validated
メソッドを呼び出せばOK
フォームリクエストはコントローラメソッドが呼び出される前にバリデーションを行う
バリデーションが適用された後の値へアクセス
$post->title = $validated['title'];
フラッシュメッセージ
セッションに一度だけ使用するメッセージを保存できる
$request->session()->flash('status', 'The blog post was created!');
第1引数には使いやすい変数の名前を書く(なんでもよい)
Blade内でフラッシュメッセージを表示
@if(session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
直前に入力された値を消さずに画面に表示する
Blade内でoldメソッドを使用すると、バリデーションで失敗しても入力した値が消えない
<input type="text" name="username" value="{{ old('title', optional($post ?? null)->title) }}">
第2引数にデフォルト値を設定できる
optionalメソッドを使うと、指定したオブジェクトがnullだった場合、nullを返す
複数代入(MassAssignment)対策
複数代入の脆弱性対策として、$fillable
または$guarded
をモデルに設定する
複数代入可能な$fillable
の設定
class BlogPost extends Model
{
/**
* 複数代入可能な属性
*/
protected $fillable = [
'title',
'content'
];
}
複数代入不可能な$guarded
の設定
class BlogPost extends Model
{
/**
* 複数代入不可能な属性
*/
protected $guarded = [
'password',
];
}
複数代入(MassAssignment)とは…
アプリケーションの意図しない複数のカラムにデータを入れること
例えば、passwordだけ更新出来る画面が有ったとして、ブラウザに表示されるのはpasswordフィールドだけですが、ブラウザをちょっといじれば入力項目を追加できます。これが複数代入です。大抵、他の項目も更新出来てしまう問題を回避するために、複数の項目に代入出来ない様にしています。
割り当てを適用する3つの方法
1 create
$validated = $request->validated();
BlogPost::create($validated);
新しいモデルインスタンスを作成し、全てのプロパティに入力値を入れ、DBにすぐに保存する
すべてのプロパティに入力値が取得できる場合に使用
引数にはphpのarrayをとる
2 make
$post = BlogPost::make([
'title' => 'BlogPost2'
]);
$post->save();
新しいモデルインスタンスを作成し、プロパティに入力値を入れるが、DBに保存しない
DBに保存するにはsaveメソッドを呼ぶ必要がある
makeを呼んだ時点では取得できていない関連モデルやプロパティがある場合に使用
saveの引数にはEloquendモデルインスタンスをとる
3 fill
(create, makeと違い、静的メソッドではない)
$post->fill([
'title' => 'BlogPost3'
]);
すでにインスタンスが存在する場合や、既存のモデルを修正したい場合に使用
update と delete
update
updateする場合はputするがhtmlではgetとpostしかできないため、methodディレクティブを使用する
<form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST">
@csrf
@method('PUT')
//
</form>
delete
deleteもupdateと同じでmethodディレクティブを使用する
<form class="d-inline" action="{{ route('posts.destroy', ['post' => $post->id]) }}" method="POST">
@csrf
@method('DELETE')
<input type="submit" value="Delete!" class="btn btn-primary">
</form>
controller側でdeleteの設定をする
public function destroy($id)
{
$post = BlogPost::findOrFail($id);
$post->delete();
session()->flash('status', 'Blog post was deleted!');
return redirect()->route('posts.index');
}
sessionヘルパ関数を使うことで、sessionにアクセスできる
Requestクラスをタイプヒントすることでsessionにアクセスする方法もある
Assets & Styles(JavaScript, CSS, Laravel MIX, BootStrap)
使っているツール
ツール名 | 説明 |
---|---|
Node.js | CSSやJavaScriptの管理をするツール、サーバサイドのJavaScript実行環境やクライアントサイドJavaScriptの開発環境として利用される |
NPM | Node.jsのパッケージマネージャー、PHPのcomposerのようなもの、フロントエンドのライブラリを取得し管理する |
WebPack | JavaScriptのモジュールバンドラー(module bundler) 、複数のモジュールの依存関係を解決して1つにまとめる、CSSプリプロセッサのコンパイルやminifyも行う、Node.jsで作られたパッケージの1つ |
Laravel Mix | webpackビルド手順を定義する流暢なAPIを提供するもの |
参照:(Laravel公式)Laravel 9.x JavaScriptとCSSスカフォールド
フロントエンドライブラリ
使用するフロントエンドライブラリはpackage.jsonに記載がある。
PHPのcomposer.jsonのようなもの。
フロントエンドライブラリのインストール
npm install
インストールするとnode_modulesファイルとpackage-lock.jsonファイルができる
コンパイルの実行
npm run dev
publicフォルダ配下にapp.cssファイルとapp.jsファイルが作られる
package.jsonにはnpmで使えるコマンドが記述されている
"scripts": {
// 開発環境用のコンパイル。minifyしないのでデバッグがしやすい。
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js",
// devと一緒だが、コマンド実行後、変更を監視し、変更があったら自動で再コンパイルしてくれる。
"watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
// 本番環境用のコマンド。minifyするので早いが、デバッグには向かない。
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --config=node_modules/laravel-mix/setup/webpack.config.js"
},
Blade内でassetsの読み込み
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<script src="{{ asset('js/app.js') }}" defer></script>
asset関数は、現在のリクエストのスキーマ(HTTPかHTTPS)を使い、アセットへのURLを生成
deferをつけるとページがレンダリングされてから、javaScriptが読み込まれる
バージョニング/キャッシュの破棄
コンパイルしたアセット(cssファイル、jsファイル)にタイムスタンプまたは一意のトークンの接尾辞を付けて、提供してきたコードの古くなったコピー(キャッシュ)の代わりに、ブラウザに新しいアセットをロードするように強制することができる。
Laravelではwebpack.mix.jsで簡単にバージョニングができる。
コンパイルしたすべてのファイルのファイル名に一意のハッシュを追加する
mix.js('resources/js/app.js', 'public/js');
if (mix.inProduction()) {
mix.version();
}
version
メソッドで、コンパイルしたすべてのファイル名に一意のハッシュを追加
inProduction
メソッドはnpm run prod
の実行中のみtrueを返す
バージョン付きファイルは開発では不要なため、本番環境でのみ適用されるようにしておく
Bladeのasset
ヘルパ関数をmix
ヘルパ関数にする
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<script src="{{ mix('js/app.js') }}" defer></script>
mix
関数は、ハッシュされた(suffixがついた)ファイルの現在の名前を自動的に判別
参照:(Laravel公式)Laravel 9.x アセットのコンパイル(Mix)
Bootstrap
UIコンポーネントにはプリセットという概念があり、その1つがbootstrap。
Bootstrapの機能はレイアウト、コンポーネント、ユーティリティの3つに大別される。
名前 | 内容 |
---|---|
レイアウト | レスポンシブデザインを作るのに役立つ。コンテンツを行と列に配置する。 |
コンポーネント | ボタン、ドロップダウン、フォームなど一般的に使用されるWeb要素の構成。 |
ユーティリティ | フレックスボックスや色の適用。CSSをカスタムしなくてもすぐに使える。 |
laravel/uiパッケージをインストールする
composer require laravel/ui 3.0.0
バージョン指定がなくてもOK
基本的なスカフォールドを生成
php artisan ui bootstrap
スカフォールド(scaffold)とはアプリケーションの足場(雛形)を自動生成して簡単に作ることができる機能
(bootstrapとは直接関係ないけど)認証用コントローラの作成
php artisan ui:controllers
Carbon
LaravelはCarbon(PHPのDateTimeクラスをオーバーラップした日付操作ライブラリ)を使用しているため、すべての日付はcarbonオブジェクトのインスタンスが返るようになっている
<p>Added {{ $post->created_at->diffForHumans() }}</p>
@if(now()->diffInMinutes($post->created_at) < 5)
<div class="alert alert-info">New!</div>
@endif
日付を人が理解しやすい形に変換
now
は現時点を表す新しいIlluminate\Support\Carbonインスタンス
を生成
テスト
単体テスト(UnitTest)と機能テスト(FeatureTest)
テストは単体テストと機能テストの2つに大別される。
名前 | 説明 |
---|---|
単体テスト | 1つのクラスや1つのメソッドを機能するか確認するテスト |
機能テスト | ブログの記事を投稿したら保存される、というような機能単位のテスト。実行中のアプリケーションにリクエストを送ることが多い |
テストに関連するファイルはtestsディレクトリ配下にある。
すべてのテストファイルはtests/TestCase.php
のTestCaseクラスを継承する必要がある。
単体テストのファイルはtests/Unit
ディレクトリ配下、機能テストのファイルはtests/Feature
ディレクトリ配下。ただし、tests/Unit
ディレクトリ配下のテストは、アプリケーションを起動しないため、データベースやその他のフレームワークにアクセスができない。
またテストクラスのメソッド名はtestから始まる必要がある。
リクエスト関連のテストではassert...
のメソッドが大量に用意されている。
参照:(Laravel公式)HTTPテスト
テストの実行
テストをターミナルで実行(簡易的な結果)
./vendor/bin/phpunit
テストをターミナルで実行(詳細な結果)
php artisan test
典型的なテストの形
- Arrangeパート(テストをするためのオブジェクトの初期化)
- Actパート(オブジェクトに紐づくメソッドの実行)
- Assertパート(操作の結果が期待通りかどうかの検証)
Arrangeパートは常にあるわけではないが、ActとAssertは常に必要
テストに関する設定
phpunit.xml
ファイルに設定の記述がある。テストはローカル環境とは別の環境で実行される。
また使用しているデータベースに直接影響がないように設定できる。
データベースをsqliteのインメモリ機能を使う
// phpunit.xmlに記述
<env name="DB_CONNECTION" value="sqlite_testing"/>
// database.phpに記述
'connections' => [
//
'sqlite_testing' => [
'driver' => 'sqlite',
'database' => ':memory:'
],
//
]
phpunit.xml
に<env name="DB_DATABASE" value=":memory:"/>
でもOK
:memory:
はメモリ上にデータベースを作成するインメモリの指定
参照:(Qiita記事)【Laravel7】PHPUnitの設定をしてテストを実行してみる
dockerを使っている場合はforceを付け加える必要あり
テスト前にphp artisan cache:clear
でキャッシュをクリアすること
参照:(Qiita記事)【Laravel】.env.testingの使用方法と注意点
参照:(Laravel公式)設定キャッシュ
テストファイルの作成
php artisan make:test HomeTest
HomeTestの部分は作成するファイル名
テスト例1(トップページにアクセスした時に表示されるテキストの確認)
class HomeTest extends TestCase
{
public function testHomePageIsWorkingCorrectly()
{
$response = $this->get('/');
$response->assertSeeText('Welcome to Laravel!');
}
}
メソッド名は長くても良いので、何をテストしているのかわかりやすい名前をつける
テスト例2(データベース関連のテスト)
class PostTest extends TestCase
{
use RefreshDatabase;
public function test1BlogPostWhenThereIs1withNoComments()
{
// Arrange
$post = $this->createDummyBlogPost();
// Act
$response = $this->get('/posts');
// Assert
$response->assertSeeText('New title');
$response->assertSeeText('No comments yet');
$this->assertDatabaseHas('blog_posts', [
'title' => 'New title',
]);
}
}
RefreshDatabase
を使うことで、テストで使ったデータが保存されたままにならない
assertDatabaseHas
メソッドで、DBに値を保持しているかチェック
テスト3(フォームからポストした時のテスト)
public function testStoreValid()
{
// Arrange
$params = [
'title' => 'Valid title',
'content' => 'At least 10 characters'
];
// Act & Assert
$this->post('/posts', $params)
->assertStatus(302)
->assertSessionHas('status');
// Assert
$this->assertEquals(session('status'), 'The blog post was created!');
}
assertStatus(302)
でリダイレクトが正しく動作するかの確認
テスト例4(updateした時のテスト)
public function testUpdateValid()
{
// Arrange
$post = $this->createDummyBlogPost();
$this->assertDatabaseHas('blog_posts', $post->toArray());
$params = [
'title' => 'A new named title',
'content' => 'Content was changed',
];
// Act & Assert
$this->put("/posts/{$post->id}", $params)
->assertStatus(302)
->assertSessionHas('status');
// Assert
$this->assertEquals(session('status'), 'Blog post was updated!');
$this->assertDatabaseMissing('blog_posts', $post->toArray());
$this->assertDatabaseHas('blog_posts', [
'title' => 'A new named title',
]);
}
put
を使っているが、php artisan route:list
で何を使うか調べること
リレーション(1対1, 1対多, Eloquent)
モデルとマイグレーションファイルの作成
php artisan make:model Author --migration
1対1のリレーションのモデル
Author(親モデル)がprofile(子モデル)を持つ場合
class Author extends Model
{
use HasFactory;
public function profile()
{
return $this->hasOne('App\Models\Profile');
}
}
class Profile extends Model
{
use HasFactory;
public function author()
{
return $this->belongsTo('App\Models\Author');
}
}
author側にhasoneメソッド、profile側にbelongsToメソッドを使用する
profile(親モデル)をauthor(子モデル)に割り当てる方法
$author = new Author();
$profile = new Profile();
$author->profile()->save($profile);
save
メソッドはモデルインスタンスを引数にとる
create
メソッドは配列を引数にとる
author(子モデル)をprofile(親モデル)に割り当てる方法1
$profile = new Profile();
$author = Author::create();
$profile->author()->associate($author)->save();
author(子モデル)をprofile(親モデル)に割り当てる方法2
$profile = new Profile();
$author = Author::create();
$profile->author_id = 4;
$profile->save();
デフォルトではリレーションは明示的に呼び出さない限りは読み込まれない
// この時点ではprofileは読み込まれてない
$author = Author::find(2);
// profileにアクセスすることで読み込まれる
$author->$profile
1度に全てのリレーションを読み込まないことをlazy loadingと言う
最初からリレーションを読み込む場合
$author = Author::with(['profile', 'comment'])->whereKey(1)->first();
1度にリレーションまで読み込むことをeager loadingを言う
1対多のリレーションモデル
BlogPostが複数のCommentを持つ場合
class BlogPost extends Model
{
public function comments()
{
return $this->hasMany('App\Models\Comment');
}
}
class Comment extends Model
{
public function blogPost()
{
// 基本の形
// return $this->belongsTo('App\Models\BlogPost');
// キー名が違う場合
return $this->belongsTo('App\Models\BlogPost', 'post_id', 'blog_post_id');
}
}
メソッド名の末尾にidをつけたものを外部キー名とする(上の例だとblog_post_id)
外部キーが違う場合、第2引数で外部キー名を指定できる
親モデルの主キーを参照しない場合、第3引数で親モデルの別カラムを指定できる
CommentをBlogPostに割り当てる方法
$bp = BlogPost::find(2);
$comment = new Comment();
$comment->content = 'A first comment.';
$bp->comment()->save($comment);
BlogPostをCommentに割り当てる方法
$bp = BlogPost::find(2);
$comment = new Comment();
$comment->content = 'A second comment.';
$comment->blogpost()->associate($bp)->save();
BlogPostをCommentに割り当てるもう1つの方法
$comment = new Comment();
$comment->content = 'A third comment.';
$comment->blog_post_id = 2;
$comment->save();
複数のCommentをBlogPostに1回で割り当てる方法
$bp = new BlogPost();
$comment = new Comment();
$comment->content = 'a';
$comment2 = new Comment();
$comment2->content = 'b';
$bp->comments()->saveMany([$comment, $comment2]);
CommentなしでBlogPostだけを取得する
$post = BlogPost::all();
BlogPostに紐づいているCommentもまとめて取得する
$post = BlogPost::with('comments')->get();
CommentからBlogPostをlazy loading
$comment = Comment::find(1);
$comment->blogPost;
blogPostはCommentモデルに記述したメソッド名
Lazy Loading と Eager Loading
実行されるクエリログを確認するには、ファサードクラスのIlluminate\Database\Connection
のメソッドを使用する
実行クエリを確認する
// ログの有効化
DB::connection()->enableQueryLog();
// クエリ実行
$post = BlogPost::all();
// 実行クエリをログに出力
dd(DB::getQueryLog());
Lazy Loading
$post = BlogPost::all();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->content;
}
}
上記の場合、すべてのブログポストを取得した後、各ブログポストに紐づくコメントを1つずつ取得する。そのため、コメントを取得するクエリはコメントの数だけ実行される。N+1問題
Eager Loading
$post = BlogPost::with('comments')->get();
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->content;
}
}
withを使った場合、コメントテーブルのデータもまとめて取得するので、ブログポストを取得するクエリ + コメントを取得するクエリの2回で済む
Commentを持っているBlogPostだけを取得する
BlogPost::has('comments')->get();
リレーション関連のメソッド(has
)はEloquent
Eloquentリレーションはクエリビルダとしても機能する
Commentを2つ以上持っているBlogPostだけを取得する
BlogPost::has('comments', '>=', 2)->get();
より複雑なクエリを記述する場合はwhereHasメソッドで$query
変数を使用する
BlogPost::whereHas('comments', function($query) {
$query->where('content', 'like', '%abc%');
})->get();
CommentがないBlogPostの取得
BlogPost::doesntHave('comments')->get();
abcを含むCommentをもつBlogPostは取得しない
BlogPost::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', '%abc%');
})->get();
持っているCommentの数をカウントする
BlogPost::withCount('comments')->get();
新しくcomments_countという属性がBlogPostに自動的に追加される
別名をつけてカウントするクエリ
BlogPost::withCount(['comments', 'comments as new_comments' => function($query) {
$query->where('created_at', '>=', '2022-03-24 09:00');
}])->get();
comments_countとnew_commentsの数が集計され追加される
Model Factory
データベースのテストで使用する。モデルに紐づくテーブルにダミーのデータを生成する。
Faker
Faker(ライブラリ)で生成されるランダムなデータを使って、特定のモデルインスタンスをいくつか生成することができる。
参照:(github)Faker(Fakerで生成できるデータの種類が載ってる)
参照:(Laravel公式)モデルファクトリ
composer.jsonの記述
"require-dev": {
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.4.2",
"nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.3.3"
},
composer.json
でfakerをインストールしている
Model Factory
Factoryの作成
php artisan make:factory CommentFactory --model=Comments
--model=Comments
でどのモデルのファクトリなのかを指定
database
ディレクトリ配下にファイルが生成される
comments
のダミーデータを作成するファクトリファイル
class CommentFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Comment::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'content' => $this->faker->realText(50, 5)
];
}
}
definition
メソッドのreturn
にfakerで生成するデータを定義する
protected $model = Comment::class;
で対応するモデルを定義
factoryファイルを呼び出す
Comment::factory()->count(4)->create(['blog_post_id' => 2]);
factory
メソッドでどこでもfactory
を呼び出せる
count
メソッドで作成したい個数を指定、上記だと4つのレコードを作成
create
メソッド内の配列でオプションのパラメータを指定
特定の文字数以上のダミーデータをFactoryで作成
public function definition()
{
return [
'title' => $this->faker->sentence(10),
'content' => $this->faker->paragraphs(5, true)
];
}
sentence(10)
で10文字分の文字列を作成
paragraphs(5, true)
で文章を5つ作成。第2引数をfalseにすると配列が返る
Factory の State
状態操作するメソッドを定義することで、ダミーではなく任意のデータに変更できる
public function newTitle()
{
return $this->state(function() {
return [
'title' => 'New title',
];
});
}
メソッド名はわかりやすい名前にする
$this->state()
内のreturn
+ 配列で属性に任意のデータを定義する
stateメソッドで定義した内容のデータを作成する
BlogPost::factory()->count(5)->newTitle()->create();
stateで定義されていないものはdefinition
で定義したランダムデータが作成される
state
はあくまでモデルファクトリで使用できるもので、実際のモデルには関係がない
afterCreating と afterMaking
モデルの作成中、または、作成後に追加のタスクを実行できるようにする。
author(親モデル)を生成しDBに保存した後にprofile(子モデル)を保存する
public function createDummyProfile()
{
return $this->afterCreating(function(Author $author) {
$author->profile()->save(Profile::factory()->make());
});
}
認証
認証に関するファイルを追加
php artisan make:auth
既存のファイルを上書きする場合があるので注意
web.php
でルーティングが記述される
Auth::routes();
Auth::routes(); だけで認証に関するすべてのルーティングが設定される
登録されているルーティングを確認するには、php artisan route:list
認証に関するコードを追ってみる
app/Http/Controllers/HomeController.php
で認証に関するミドルウェアを設定している
public function __construct()
{
$this->middleware('auth');
}
ミドルウェアは app/Http/Kernel.php
で設定されてる
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
App\Http\Middleware\Authenticate
で、認証が失敗したときのリダイレクト先を定義
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
APIは弾きたいので、jsonフォーマットかどうかをチェック
Illuminate\Auth\Middleware\Authenticate
を継承している
Illuminate\Auth\Middleware\Authenticate
のauthenticate
で認証チェック
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
アカウント登録に関するコードを追ってみる
app/Http/Controllers/Auth/RegisterController.php
に記述がある
最初の時点では認証済みではないため、guestを使用
public function __construct()
{
$this->middleware('guest');
}
バリデーション(制約)の設定
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
}
emailではusersテーブルでユニークかどうかをチェック
パスワード強度を上げるなど、独自のルールを追加することも可能
confirmedは、フィールドが、{field}_confirmationフィールドと一致する必要がある。たとえば、バリデーション中のフィールドが「password」の場合、「password_confirmation」フィールドが入力(html側)に存在し一致している必要がある。
データをDBに保存する処理
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
パスワードは必ずハッシュ化して保存する
登録全体の処理はtraitを使用している
use RegistersUsers;
vendor/laravel/ui/auth-backend/RegistersUsers.php
が使用しているtrait
以下、RegistersUsers.php
(trait)の処理
登録画面の呼び出し(/registerがgetされた時)
public function showRegistrationForm()
{
return view('auth.register');
}
登録処理(/registerがpostされた時)
public function register(Request $request)
{
$this->validator($request->all())->validate();
event(new Registered($user = $this->create($request->all())));
$this->guard()->login($user);
if ($response = $this->registered($request, $user)) {
return $response;
}
return $request->wantsJson()
? new JsonResponse([], 201)
: redirect($this->redirectPath());
}
$request->all() でフォームから送信されたすべての値を取得
validate() でバリデーションの検証
new Registeredでユーザーの新しいインスタンスを保持
redirect(this->redirectPath())でリダイレクト
リダイレクト先はtraitに記述
use RedirectsUsers;
vendor/laravel/ui/auth-backend/RedirectsUsers.php
を使用している
以下、RedirectUsers.php
の処理
リダイレクト先の指定
public function redirectPath()
{
if (method_exists($this, 'redirectTo')) {
return $this->redirectTo();
}
return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
}
redirectToというメソッドを定義すれば、そのメソッドが呼び出される
メソッドがない場合、redirectToプロパティで指定される
ログインに関するコードを追ってみる
app/Http/Controllers/Auth/LoginController.php
に記述がある
use AuthenticatesUsers;
protected $redirectTo = RouteServiceProvider::HOME;
public function __construct()
{
$this->middleware('guest')->except('logout');
}
trait vendor/laravel/ui/auth-backend/AuthenticatesUsers.php
を使用
redirectToでログインした時のリダイレクト先を定義
exceptで認証が必要ないものを除外
(上記だとlogout以外はguestの認証でOK (logoutはauthの認証が必要のため))
以下、AuthenticatesUsers.php
の処理
リダイレクト先をtraitで記述
use RedirectsUsers
ログインフロー
public function login(Request $request)
{
$this->validateLogin($request);
if (method_exists($this, 'hasTooManyLoginAttempts') &&
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
return $this->sendFailedLoginResponse($request);
}
validateLoginでバリデーションのチェック
$this->attemptLoginで何回ログインを試したかをカウント
何回ログインをしたかのカウント
protected function attemptLogin(Request $request)
{
return $this->guard()->attempt(
$this->credentials($request), $request->filled('remember')
);
}
protected function credentials(Request $request)
{
return $request->only($this->username(), 'password');
}
guard()->attemptで与えられた認証情報でログインをしようとしている
credentialsメソッドでユーザー名とパスワードを取得
login
メソッド内のsendLoginResponse
メソッドでレスポンスを送信
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
if ($response = $this->authenticated($request, $this->guard()->user())) {
return $response;
}
return $request->wantsJson()
? new JsonResponse([], 204)
: redirect()->intended($this->redirectPath());
}
session()->regenerateでセッションを再生成
clearLoginAttemptsでログインに失敗した回数を消去
ガードコンポーネントの解説
認証するとき、どのようにユーザーの情報を取ってくるかの設定
auth.php
に記述がある
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
コンポーネントが分かれていることで、複数の認証方法を使ったコンポーネントを作成できる
guard の driver は session か token から選ぶ(apiはtoken)
providers の driver は eloquent か database から選ぶ
passwordsでパスワードのリセットを設定
tableでリセットにどのテーブルを使うかを指定
expireで有効期限を60分に指定
throttleでログイン失敗の回数を60回に指定
Remember Me(ログイン保持)
アカウント名に何を使うかはvendor/laravel/ui/auth-backend/AuthenticatesUsers.php
のusername
メソッドで定義している
protected function validateLogin(Request $request)
{
$request->validate([
$this->username() => 'required|string',
'password' => 'required|string',
]);
}
public function username()
{
return 'email';
}
上記ではemailだが別のクラスでnameなどにオーバーライドすることができる
htmlのnameにrememberを記述
<input class="form-check-input" type="checkbox" name="remember"
value="{{ old('remember') ? 'checked' : '' }}"/>
vendor/laravel/ui/auth-backend/AuthenticatesUsers.php
のattemptLogin
メソッド内でrememberのチェックしている
protected function attemptLogin(Request $request)
{
return $this->guard()->attempt(
$this->credentials($request), $request->filled('remember')
);
}
attempt
メソッドの第2引数にbool値を渡せる
trueの場合は、手動でログアウトするまで認証が継続される
認証済みと未認証の挙動を分ける
Blade内で出しわけ
@auth('admin')
// ユーザーは認証済み…
@endauth
@guest('admin')
// ユーザーは認証されていない…
@endguest
登録ルートがある場合のみ登録画面へのリンクを表示する
@if (Route::has('register'))
<a class="p-2 text-dark" href="{{ route('register') }}">Register</a>
@endif
ログアウトのルートはpostしか受け付けないためpostでの実装をする
<a class="p-2 text-dark" href="{{ route('logout') }}"
onclick="event.preventDefault();document.getElementById('logout-form').submit();">
Logout ({{ Auth::user()->name }})
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
event.preventDefault();はjavaScriptの記述でデフォルトの挙動を妨害する
上記の場合、route('logout')へのリンク遷移を妨害している
csrfトークンの処理を追ってみる
app/Http/Middleware/VerifyCsrfToken.php
でcsrf対策の適用を除外するURIを設定できる
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
// 適用を除外するURI
];
}
app/Http/Middleware/VerifyCsrfToken.php
はvendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php
を継承しており、handle
メソッド内でcsrf対策の処理をしている
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return tap($next($request), function ($response) use ($request) {
if ($this->shouldAddXsrfTokenCookie()) {
$this->addCookieToResponse($request, $response);
}
});
}
throw new TokenMismatchException('CSRF token mismatch.');
}
tokensMatch
メソッドでcsrfトークンのチェック
protected function tokensMatch($request)
{
$token = $this->getTokenFromRequest($request);
return is_string($request->session()->token()) &&
is_string($token) &&
hash_equals($request->session()->token(), $token);
}
getTokenFromRequest
メソッドでcsrfトークンを取得
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
try {
$token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
} catch (DecryptException $e) {
$token = '';
}
}
return $token;
}
inputかヘッダーにcsrfトークンがあるか確認している
csrfトークンでエラーが出た場合、419が返ってくる
どこでエラーが起きているか確認する以外はvendor配下のファイルは変更しない方が良い
セッションの書き込みエラーの場合は、storage/framework/sessions
ディレクトリ配下のファイルに書き込み権限があるかどうかを確認する
認証されたユーザー情報の取得
Auth
ファサードを使用する方法
use Illuminate\Support\Facades\Auth;
// 現在認証しているユーザーのidを取得
Auth::id();
// 現在認証しているユーザーモデルを取得
Auth::user();
// ユーザーが認証済みかチェックする
Auth::check();
Auth
ファサードクラスを見ると使用可能なメソッド一覧が確認できる
認証済みユーザーでリクエストを送る場合$ruquestからも取得できる
public function store(StorePost $request)
{
$validated = $request->validated();
$validated['user_id'] = $request->user()->id;
// 省略
}
$request->user()
で認証ユーザーの情報を取得
特定のルートを認証済みユーザーしかアクセスできないようにする
route側で認証済みユーザーしかアクセスできないようにする場合
Route::get('/recent-posts/{days_ago?}', function ($daysAgo = 20) {
return 'Post from ' . $daysAgo . 'days ago';
})->name('posts.recent.index ')->middleware('auth');
controller側で認証済みユーザーしかアクセスできないようにする場合
class PostsController extends Controller
{
public function __construct() {
$this->middleware('auth')
->only(['create', 'store', 'edit', 'update', 'destroy']);
}
//
}
テストで認証されたユーザーを使用する場合
tests/TestCase.php
クラスにuser
メソッドを追加
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function user()
{
return User::factory()->create();
}
}
テストする箇所でuser
メソッドを呼び出す
$this->actingAs($this->user())
->post('/posts', $params)
->assertStatus(302)
->assertSessionHas('status');
actingAs
ヘルパ関数で指定したユーザーを現在のユーザーとして認証
Database Seeding
外部キーの追加
既存のデータベースに外部キーを追加する場合、外部キーを追加するだけのmigrationファイルを実行すると、既存レコードの外部キーに何を入れればよいかLaravelが判断できないため、エラーが発生する。
エラーが出るmigrationファイル
public function up()
{
Schema::table('blog_posts', function (Blueprint $table) {
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
});
}
上の簡易的な書き方
public function up()
{
Schema::table('blog_posts', function (Blueprint $table) {
$table->foreignId('user_id')->constrained('users');
});
}
カラムタイプは実際のテーブルの外部キーと一致すること。上記の場合、users.idがunsignedBigIntegerなので、そのように設定している。
解決方法1
nullを許容する
$table->unsignedInteger('user_id')->nullable();
解決方法2
開発環境ならmigrate:refreshを実行する
php artisan migrate:refresh
すべてのデータベースが1度ロールバックされた後、再度テーブルが作成される
本番環境では使用しない
seeder
データベースにデータを一括で登録できるもの。
seederの実行
php artisan db:seed
database/seeders/DatabaseSeeder.php
に記述された内容が実行される
class DatabaseSeeder extends Seeder
{
public function run()
{
DB::table('users')->insert([
'name' => 'yuta saito',
'email' => 'saito@laravel.test',
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
]);
}
}
run
メソッド内が実行される、上記の例だとusersテーブルにレコードを1つ追加している
もう1度seederを実行すると重複したレコードを登録できないためエラーになる
その場合はphp artisan migrate:fresh --seed
を実行するとエラーにならない
seeder内でfactoryを使う
class DatabaseSeeder extends Seeder
{
public function run()
{
$users = User::factory(10)->create();
// $usersが10こ生成されるか確認
dd($users->count());
}
}
コレクションはphpの配列に関する関数(countとか)をそのまま使える
外部キーをもつmodelのseeder
public function run()
{
$takauji = User::factory()->takaujiAshikaga()->create();
$else = User::factory(20)->create();
$users = $else->concat([$takauji]);
$blog_posts = BlogPost::factory(50)->make()->each(function ($blog_post) use ($users) {
$blog_post->user_id = $users->random()->id;
$blog_post->save();
});
Comment::factory(150)->make()->each(function ($comment) use ($blog_posts) {
$comment->blog_post_id = $blog_posts->random()->id;
$comment->save();
});
}
make
を使っている点に注意
以下、コレクションで使えるメソッド
concat
配列またはコレクションを別のコレクションに加える
each
反復処理し、各アイテムをクロージャに渡す
random
コレクションの中からランダムに1つ返す
個別のseederファイルを作成する
php artisan make:seeder UsersTableSeeder(ファイル名)
作成したseederファイルが実行されるようにする
// database/seeders/DatabaseSeeder.phpに記述
public function run()
{
$this->call([
UsersTableSeeder::class,
BlogPostsTableSeeder::class,
CommentsTableSeeder::class,
]);
}
新しいseederファイルを作成したら必ず下記コマンドを実行すること
composer dump-autoload
作成した個別のseederを実行する
php artisan db:seed --class= UsersTableSeeder
seeder実行時のオプションを設定する
php artisan db:seed
を実行した際にdatabaseを作り直すか確認するように設定
if ($this->command->confirm('Do you want to refresh the database?', true)) {
$this->command->call('migrate:refresh');
$this->command->info('Database was refreshed');
}
継承元のvendor/laravel/framework/src/Illuminate/Database/Seeder.php
に$command
プロパティが設定されている
$command
のメソッドとしてaritsanコンソール関連のメソッドを使用できる
confirm
メソッドでyes, no で回答する質問が表示される、第2引数がtrueの場合はデフォルト値がyesになる
作成するuser数のデフォルト値を設定する
class UsersTableSeeder extends Seeder
{
public function run()
{
$user_count = max($this->command->ask('How many users would you like?', 20), 1);
User::factory($user_count)->create();
}
}
max
で与えられた引数の中で最も大きな数を返す、入力値が0だった場合「0, 1」になり1が返る
ask
メソッドの第2引数はデフォルト値
-n
オプションを付けるとデフォルト値ですべて進めてくれる
php artisan db:seed -n
物理削除と論理削除(ソフトデリート)
デフォルトだと関連テーブルは自動で削除してくれないので、設定する必要がある。
関連テーブルの削除方法1
Eloquentモデルはイベントをディスパッチする。
delete
されたときには、deleting
と deleted
のイベントがディスパッチされる。
上記イベントをディスパッチするために、モデルにbooted
メソッドを定義する。
// BlogPostを削除したとき、紐づいたCommentsも一緒に削除されるよう設定
public static function booted()
{
static::deleting(function (BlogPost $blogPost) {
$blogPost->comments()->delete();
});
}
deletingの場合、削除される直前に処理が走る
commentsではなくcomments()としているのは、特定のコメントではなくすべてのコメントを取得するため
関連テーブルの削除方法2
migrationファイルでテーブルを作成するときに、関連テーブルも削除されるように設定する
public function up()
{
Schema::table('comments', function (Blueprint $table) {
// 外部キーのblog_post_idが削除されたとき、commentsも削除する
$table->dropForeign(['blog_post_id']);
$table->foreign('blog_post_id')
->references('id')
->on('blog_posts')
->onDelete('cascade');
});
}
dropForeign
で既存の外部キーを削除、カラム名を指定するときは配列を渡す
dropForeign('comments_blog_post_id_foreign')
のように外部キーの名前を渡す場合は、配列ではなく文字列で渡す
onDelete('cascade')
で外部キーのレコードが削除されたときに紐づくレコードが削除される
ソフトデリート
ソフトデリート(論理削除)するにはIlluminate\Database\Eloquent\SoftDeletes
を使う
use Illuminate\Database\Eloquent\SoftDeletes;
class BlogPost extends Model
{
use SoftDeletes;
//
}
migrationファイルでテーブル作成時にdeleted_atカラムを追加する
public function up()
{
Schema::table('blog_posts', function (Blueprint $table) {
$table->softDeletes();
});
}
public function down()
{
Schema::table('blog_posts', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
関連テーブルも論理削除したい場合は、関連テーブルにもSoftDeletesを使う
ソフトデリートされたレコードを取得する
$all = BlogPost::withTrashed()->get()->pluck('id');
ソフトデリートされたものはall()など通常のクエリでは取得対象でなくなる
ソフトデリートされたレコードだけを取得する
$trashed = BlogPost::onlyTrashed()->where('id', '>', 3)->get();
withTrashed
やonlyTrashed
はクエリビルダのインスタンスを返す
取得したモデルがソフトデリートされているかをチェックする
$post = BlogPost::withTrashed()->find(3);
// bool値が返ってくる
$post->trashed();
ソフトデリートしたものを復元する
$post = BlogPost::withTrashed()->find(3);
$post->restore();
関連テーブルも一緒に復元させる場合はbooted
メソッド内にrestoring
メソッドをあらかじめ記述
static::restoring(function (BlogPost $blogPost) {
$blogPost->comments()->restore();
});
論理削除(ソフトデリート)が有効な場合に、レコードを物理削除する方法
$post = BlogPost::has('comments')->get()->first();
$post->forceDelete();
認可 (Gate, Policy)
あるユーザーが特定の行動をできるように許可することを認可という。
Laravelでの認可の方法は、GateとPolicyの2つがある。
Gate
Gateとは、ユーザーが特定のアクションを実行することを許可されているかどうかを判断するクロージャのこと。Illuminate\Support\Facades\Gate
を使用する。
App\Providers\AuthServiceProvider
でGateを使用することで、認可の定義ができる。
自分で投稿したBlogPostだけ編集できるようにする
use App\Models\BlogPost;
use App\Models\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', function (User $user, BlogPost $post) {
return $user->id === $post->user_id;
});
}
}
boot
メソッド内にGateを定義する
define
の第1引数でわかりやすいアクション名をつける
クロージャの第1引数は必ず$user
編集するときに認可されているかを判定
// Controllerに記述
public function edit($id)
{
$post = BlogPost::findOrFail($id);
if (Gate::denies('update-post', $post)) {
abort(403, "You can't edit this blog post.");
}
return view('posts.edit', ['post' => $post]);
}
Gate::denies
でupdate-post
が認可されていない場合を判定
abort
はヘルパ関数、第2引数で表示するメッセージを定義
認可されていない判定を簡単に記述する
// Controllerに記述
public function edit($id)
{
$post = BlogPost::findOrFail($id);
$this->authorize('update-post', $post);
return view('posts.edit', ['post' => $post]);
}
Controllerの場合、$this
でauthorize
ヘルパ関数が使える
authorize
でアクションが認可判定し、認可されていない場合は403を返す
認証済みユーザー以外のユーザーが特定の行動を認可されているか確認する
$user = User::find(21);
// 認可されている場合、trueが返る
Gate::forUser($user)->allows('update-post', $post);
// 認可されていない場合、trueが返る
Gate::forUser($user)->denies('update-post', $post);
adminのようにすべての権限を認可する場合
Gate::before(function ($user, $ability) {
if ($user->is_admin && in_array($ability, ['update-post'])) {
return true;
}
});
before
メソッドで他の認可チェックが入る前のクロージャを定義できる
第2引数の$ability
は他の認可チェックで定義しているアクション名
null以外が返った場合、認可チェックの結果と見なされる
上記の場合、userがadminでupdate-postのアクションをする場合は認可される
同じようなものにafter
メソッドがある
Policy
特定のモデルまたはリソースに関する認可ロジックを集めたクラスのこと。
認可対象が増えてきたときにGateですべて対処しようと思うと、複雑になるのでPolicyを使用する。
特に特定のモデルやリソースに紐づく場合に使用するとベター。
Policyの作成
php artisan make:policy BlogPostPolicy --model=BlogPost
オプションでモデルを指定することでCRUDのメソッドを自動で生成してくれる
Policyファイルに認可条件を定義
public function update(User $user, BlogPost $blog_post)
{
return $user->id === $blog_post->user_id;
}
AuthServiceProviderで定義したPolicyを使用する
Gate::define('update-post', [BlogPostPolicy::class, 'update']);
モデルを紐づけた場合はresourceメソッドを使うこともできる
Gate::resource('posts', BlogPostPolicy::class);
view、create、update、deleteを定義しているのと一緒
第1引数の名前はなんでも良いのでわかりやすい名前をつける
モデルとポリシーの対応を定義することでGateを定義しなくても認可が反映される
// AuthServiceProviderに記述
protected $policies = [
BlogPost::class => BlogPostPolicy::class,
];
リソースコントローラでの認可
モデルを紐づけてPolicyを作成しており、コントローラもモデルを紐づけて作成されている場合、アクション名なしでPolicyを反映できる
public function update(StorePost $request, $id)
{
$post = BlogPost::findOrFail($id);
$this->authorize($post);
// 省略
}
$this->authorize($post)
だけでPolicyのupdateが呼ばれる
リソースポリシーとリソースコントローラを使っている場合は、コントローラのコンストラクターでauthorizeResource
メソッドを使用できる
// Controllerに記述
public function __construct()
{
$this->middleware('auth');
$this->authorizeResource(User::class, 'user');
}
authorizeResource
の第1引数にモデルのクラス名、第2引数にモデルのIDを含むルート/リクエストパラーメーターの名前を入れることで、自動的に下記のポリシーメソッドが適用されるようになる。
コントローラメソッド | ポリシーメソッド |
---|---|
index | viewAny |
show | view |
create | create |
store | create |
edit | update |
update | update |
destroy | delete |
Bladeでの認可
Bladeで認可されているかどうかを判定する
@can('update', $post)
<a href="{{ route('posts.edit', ['post' => $post->id]) }}" class="btn btn-primary">Edit</a>
@endcan
can
ディレクティブで認可の設定ができる
コントローラと同じようにアクション名と変数を渡す
Bladeで認可されていない時のアクションを設定する
@cannot('delete', $post)
<p>You cannot delete this post.</p>
@endcannot
cannnot
ディレクティブで認可されていない時の設定ができる
ミドルウェアでの認可
// AuthServiceProviderに記述
Gate::define('home.secret', function (User $user) {
return $user->is_admin;
});
// Routeに記述
Route::get('/secret', [HomeController::class, 'secret'])
->name('home.secret')
->middleware('can:home.secret');
middleware
の中でcan
を使うことで認可の設定ができる
クエリスコープ(Query Scopes)
クエリの制約をつけることができる。
グローバルスコープとローカルスコープがある。
グローバルスコープ
特定のモデルのすべてのクエリに制約を追加できる。
スコープ専用の特定ディレクトリはデフォルトでないため、自分で作成する。
グローバルスコープの作成
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class LatestScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->orderby($model::CREATED_AT, 'desc');
}
}
$model::CREATED_AT
でモデルの作成日を指定
グローバルスコープの適用
// app/Models/BlogPost.phpに記述
public static function booted()
{
static::addGlobalScope(new LatestScope);
}
addGlobalScope
の引数にはインスタンスだけをとる
元々Laravelの機能でlatest
メソッドがあるので上の例はあくまで練習
Softdeleteを特定のクエリで適用されないようにする
class DeletedAdminScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
if (Auth::check() && Auth::user()->is_admin) {
$builder->withoutGlobalScope(SoftDeletingScope::class);
}
}
}
// app/Models/BlogPost.phpに記述
public static function boot()
{
static::addGlobalScope(new DeletedAdminScope);
parent::boot();
}
Auth::check
でユーザーが認証済みかチェック
$builder->withoutGlobalScope()
で適用されるはずのScopeを削除
ローカルスコープ
特定の場合にのみ再利用できる共通のクエリ制約を設定できる。
タイトルで並び替える
// app/Models/BlogPost.phpに記述
public function scopeTitle(Builder $query)
{
return $query->orderby('title', 'asc');
}
ローカルスコープのメソッド名は必ずscope
から始める
適用
// app/Http/Controllers/PostsController.phpに記述
return view(
'posts.index',
['posts' => BlogPost::title()->withCount('comments')->get()]
);
設定したローカルスコープのscopeの文字を消したものがメソッド名として使用できる
複雑な場合はクロージャを使うケースもある
// app/Models/Comment.phpに記述
public function scopeContent(Builder $query)
{
return $query->orderby('content', 'asc');
}
// app/Http/Controllers/PostsController.phpに記述
return view('posts.show', [
'post' => BlogPost::with(['comments' => function ($query) {
return $query->content();
}])->findOrFail($id),
]);
上記はBlogPostを取得するとき、コメントの内容をアイウエオ順に並び替えて取得
BladeComponents
componentはBladeの要素をまとめて再利用しやすくするもの。
クラスベースのcomponent
componentの作成
php artisan make:component Alert
App\View\Components
の下にクラスのファイルが作成される
resources/views/components
ディレクトリの下にビュー用のファイルが作成される
コンポーネントを使用する
// resources/views/components/alert.blade.phpに記述
<div class="alert alert-{{ $type }}">
{{ $message }}
</div>
// app/view/components/Alert.phpに記述
class Alert extends Component
{
public $type;
public $message;
public function __construct($type, $message)
{
$this->type = $type;
$this->message = $message;
}
public function render()
{
return view('components.alert');
}
}
// 表示したいBladeに記述
<x-alert type="primary" :message="$message"/>
変数を渡すときは:
をつける
x-
でコンポーネントを使える
クラスには使いたい変数をすべて定義する必要がある
スロット
コンポーネントにプレーンテキストを渡すときは$slot
を使用する
// resources/views/components/alert.blade.phpに記述
<span class="alert-title">{{ $title }}</span>
<div class="alert alert-danger">
{{ $slot }}
</div>
<x-alert>
<x-slot:title>
Server Error
</x-slot>
<strong>Whoops!</strong> Something went wrong!
</x-alert>
$slot
に<strong>Whoops!</strong> Something went wrong!
が渡される
x-slot:title
で$title
に値を渡す
匿名コンポーネント
関連するクラスのない単一のコンポーネントファイル。
resources/views/components
の下にファイルを作成するだけでOK。
基本的な使い方はクラスベースコンポーネントと一緒。
ただしcomponents
ディレクトリの下にファイルを置かない場合は、AppServiceProvider.php
でエイリアスを設定してあげる必要がある。
public function boot()
{
Blade::component('posts.components.badge', 'badge');
}
上記だとresources/views/posts/components/badge.blade.php
をx-badge
で呼び出せるようにしている
キャッシュ
Laravelのキャッシュとは、一時的にメモリにデータを保存することです。
メモリからデータを読み込むことで、ディスクへアクセスすることなく高速にデータを取得できます。
キャッシュするデータは通常、MemcachedやRedisなどの非常に高速なデータストアに保存します。
設定
キャッシュの設定はconfig/cache.php
、.env
のファイルに記述されている。
config/cache.php
で使用するキャッシュの種類を指定
'default' => env('CACHE_DRIVER', 'file'),
memcachedやredisについても設定されている
redisの詳細はconfig/database.php
にも設定がある
キャッシュのメソッド
Illuminate\Support\Facades\Cache
を使えばキャッシュに関するメソッドを使用できる。
キャッシュを設定
Cache::put('data', 'Hello World!', 5);
第1引数がキー名、第2引数が値、第3引数が保存する時間(秒)
第3引数がない場合は永遠に保存された状態になる
キャッシュの値を取得
Cache::get('data');
キャッシュが設定されているか確認
Cache::get('data');
ある場合はtrue、ない場合はfalseが返る
キャッシュ内の値をインクリメント
Cache::increment('xxx', $data);
第2引数で増減させる値を設定できる、$dataが2なら'xxx'を2増やす
キャッシュがない場合だけキャッシュを設定
$most_commented = Cache::remember('blog-post-commented', 60, function () {
return BlogPost::mostCommented()->take(5)->get();
});
第1引数はキー名、第2引数はキャッシュの保存時間(秒)、第3引数はクロージャでキーがない場合に取得する値
第2引数はcarbonなどでも指定できる、またない場合は永遠に保存される
キャッシュを削除する
Cache::forget("blog-post-{$blogPost->id}")
キャッシュタグ
キャッシュタグはMemcachedやRedisなどで使用できる。fileやmysqlでは使用できない。
キャッシュ内の関連アイテムにタグを付けてから、特定のタグが割り当てられているすべてのキャッシュ値を削除できる。
参照:[ Docker ] Redis をDocker化する & LaravelからSessionを保存する
参照:DockerでRedisのコンテナを作成しLaravelと接続する
キャッシュタグをつける
Cache::tags(['people', 'artists'])->put('John', 'Hello Im John', 60);
Cache::tags(['people', 'authors'])->put('Ann', 'Hello Im Ann', 60);
キャッシュタグへのアクセス
Cache::tags(['people', 'artists'])->get('John');
// "Hello Im John"が表示される
タグ付きキャッシュアイテムの削除
Cache::tags(['people'])->flush();
['people']の場合は、'people'のタグがついたすべてのキャッシュが削除される
['artists']の場合は、'artists'のタグがついたもの('John'だけ)が削除される
リレーション(多対多、Eloquent)
blog_postsテーブル、tagsテーブル、中間テーブルのblog_post_tagsテーブルを定義する
modelで関係性を定義
// app/Models/BlogPost.phpで記述
public function tags()
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
// app/Models/Tag.phpで記述
public function blogPosts()
{
return $this->belongsToMany(BlogPost::class)->withTimestamps();
}
withTimestamps
メソッドをつけることで中間テーブルのcreated_atと
updated_atに値が入るようになる
関連付けをする
$blog_post = BlogPost::find(1);
$blog_post->tags()->attach([$tag1->id, $tag2->id]);
attach
メソッドで紐付けをする、中間テーブル(pivotテーブル)にレコードが生成される
配列で複数の値を渡すことができる
attach
メソッドは重複を気にせず関連付けしてしまうので注意
関連付けを解除する
$blog_post->tags()->detach($tag1);
$blog_post->tags()->detach([$tag1->id, $tag2->id]);
$tag1に紐づくレコードが中間テーブルから削除される
複数渡す場合は、配列で渡してあげる
引数を指定しない場合は、すべての紐付けが解除される(レコードすべて削除)
すべての関連付けを指定した値に同期させる
$blog_post->tags()->sync([$tag1->id, $tag2->id]);
sync
メソッドで指定されていない値は関連付けが解除される
上記の場合、$tag3などの関連付けがあれば解除され、レコードが削除される
重複は発生しない
関連付けを解除せずに指定した値に同期させる
$blog_post->tags()->syncWithoutDetaching([$tag1->id ,$tag2->id]);
syncWithoutDetaching
メソッドでは指定した値以外のものも、そのまま残る
上記の場合、$tag3などの関連付けがあってもそのまま残り、レコードも削除されない
重複は発生しない
中間テーブルのデータにアクセスする
$tags = App\Models\Tag::find(1);
$post = $tags->blogPosts->first();
$post->pivot->created_at;
中間テーブルにはpivot
でアクセスできる
中間テーブルの属性名に別名をつける
// app/Models/Tag.phpに記述
public function blogPosts()
{
return $this->belongsToMany(BlogPost::class)
->as('tagged')
->withTimestamps();
}
as
メソッドで別名をつけることでpivot以外の名前にすることができる
ただし、両方のmodelで定義しない場合、定義した側からしかその名前でアクセスできない
ネストしたリレーションへのアクセス
BlogPost::with(['comments', 'tags', 'user', 'comments.user'])->findOrFail($id);
comments.user
でネストしたcomments
のuser
へアクセスできる
複数のリレーションへのアクセス
複数のリレーションはwithの引数に配列で渡してあげればOK
BlogPost::with(['comments', 'tags', 'user', 'comments.user'])->findOrFail($id);
ファイルストレージ
ファイルをどこに保存するか、どこからダウンロードするかの設定。
ファイルストレージの設定はfilesystem.php
に記述がある。
storage_path
メソッドでどこのディレクトリに保存するかを指定している。
アクセスする場合は、どの設定でもIlluminate\Support\Facades\Storage
を使う。
'default' => env('FILESYSTEM_DRIVER', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
],
.envに設定がない場合のデフォルトの設定はlocal
になってる
ファイルのアップロード
アップロードボタンの作成
<form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="form-group">
<label for="thumbnail">Thumbnail</label>
<input id="thumbnail" type="file" name="thumbnail" class="form-control-file">
</div>
<div>
<input type="submit" value="Create" class="btn btn-primary btn-block">
</div>
</form>
form
にenctype="multipart/form-data"
を指定する
リクエストにファイルがあるか確認して、あったら保存する
$has_file = $request->hasFile('thumbnail');
if ($has_file) {
$file = $request->file('thumbnail');
$file->store('thumbnails');
}
hasFile
メソッドでリクエストに対象のファイルがあるか確認
$request->file('thumbnail')
でファイルにアクセス
$file->store('thumbnails')
でファイルを保存、thumbnailsというディレクトリ配下にファイルが作成される
ファイル名の確認
$file->getClientMimeType();
// もしくは
$file->hashName();
ファイル拡張子の確認
$file->getClientOriginalExtension();
// もしくは
$file->extension();
悪意のあるユーザーによりファイル名や拡張子が改竄される可能性があるため、getClientMimeType
やgetClientOriginalExtension
は使わない方が良い
Storageファサードを使う書き方
Storage::disk('s3')->put('thumbnails', $file);
// もしくは
Storage::putFile('thumbnails', $file)
disk
メソッドを使用すると特定のディスクを指定できる
putFile
メソッドで一意のファイル名を生成する、第2引数にはIlluminate\Http\File
またはIlluminate\Http\UploadedFile
を引数にとる
ファイル名を指定する場合
$file->storeAs('thumbnails', $post->id . $file->extension());
// もしくは
Storage::putFileAs('thumbnails', $file, $post->id . $file->extension());
storeAs
メソッドの第2引数として、ファイル名を指定
putFileAs
メソッドの第2引数で保存するファイル、第3引数でファイル名を指定
アップロード先のURLを取得する
$name1 = $file->storeAs('thumbnails', $post->id . '.' . $file->guessExtension());
$name2 = Storage::disk('local')->putFileAs('thumbnails', $file, $post->id . '.' . $file->extension());
Storage::url($name1);
// 結果:http://localhost/storage/thumbnails/62.jpg
Storage::disk('local')->url($name2);
// 結果:/storage/thumbnails/62.jpg
Storage::url()
でURLを取得する
publicフォルダ配下のものはhttpからURLが表示される
diskでlocalを指定している場合は、storage/app配下に格納されるため、httpからのURLではない
publicアップロードした先のファイルにアクセスできるようにする
php artisan storage:link
publicディレクトリにapp/storage/public配下のファイルのシンボリックリンクを作成する
これをしないとアクセスができないので注意
画面上にファイルを表示する
// app/Models/Image.phpに記述
public function url()
{
return Storage::url($this->path);
}
// Bladeに記述
<img src="{{ $post->image->url() }}"/>
ファイルを削除する
delete
メソッドを使う
Storage::delete($post->image->path);
ファイルにバリデーションをかける
リクエストクラスに記述
public function rules()
{
return [
'title' => 'bail|required|min:5|max:100',
'content' => 'required|min:10',
'thumbnail' => 'image|mimes:jpg,jpeg,png,gif,svg|max:1024|dimensions:min_height=500',
];
}
imageで画像ファイルのみを指定
mimesで拡張子を指定
maxでファイルサイズを指定
dimensionsでパラメータで高さなどを指定
ポリモーフィックリレーション(Polymorphic relation)
ポリモーフィック(多相型)な関係は、一つのモデルが複数のモデルに紐づくものをいう。
例えば、画像(image)がブログ記事(Post)にもユーザー(User)にも紐づく場合、ポリモーフィックな関係と言える。
ポリモーフィックなテーブルを作成するmigrationファイル
public function up()
{
Schema::table('images', function (Blueprint $table) {
// $table->unsignedBigInteger('imageable_id');
// $table->string('imageable_type');
// 上記2つを`morphs`メソッド1つで代替できる
$table->morphs('imageable');
});
}
imageable_id
とimageable_type
でPostとUserのどちらの画像なのかを判定する
morphs
メソッドで、{column}_id
と{column}_type
を生成できる
public function down()
{
Schema::table('images', function (Blueprint $table) {
$table->dropMorphs('imageable');
});
}
dropMorphs
メソッドでmorphs
メソッドで作成したカラムの削除
作成されるカラムのデフォルト文字数をあらかじめAppServiceProvider
で定義しておく
public function boot()
{
Schema::defaultStringLength(191);
}
defaultStringLength
メソッドでmigrationで作ったstringのデフォルト文字数を定義
1対1のポリモーフィックな関係の定義
// app/Models/Image.phpに記述
public function imageable()
{
return $this->morphTo();
}
// app/Models/BlogPost.php と app/Models/User.phpに記述
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
imageを保存
$post->image()->save(
Image::make(['path' => $path])
);
$post->image()
だけでアクセスができる
ライフサイクル
- autoloadを読み込んで、requireしなくても名前空間を指定するだけで良くなる
- Applicationインスタンスを作成(サービスコンテナ)、サービスプロバイダも読み込まれる
- HttpKernelインスタンスを作成
- Requestインスタンスを作成
- HttpKernelがリクエストを処理してResponseを取得
- レスポンスを送信
- terminate()でリクエストとレスポンスを削除
サービスコンテナ
依存関係を自動で解決してくれる。
依存関係のさらに先の依存関係まで全部解決してくれる。
app()はサービスコンテナを呼び出すヘルパ関数。
コンテナへの登録
app()->bind()
// 1度しか依存関係を解決しなくて良い場合
app()->singleton()
登録されているものを使う
app()->make()
// 他の書き方
app('登録したメソッド名');
app()->resolve();
App::make();
サービスプロバイダ(提供者)
サービスコンテナにサービスを登録する。
サービスプロバイダの作成
php artisan make:provider サービスプロバイダ名(アッパーキャメルケース)
登録
register()
登録後に実行したい処理
boot()