4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【初心者用】Laravelのお勉強

Last updated at Posted at 2022-03-12

これは何?

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種類のミドルウェアがある

  1. グローバルなミドルウェア、すべてに適用される
  2. webとapiのルートのみに適用される、kernel.phpで設定
  3. 特定のルートグループのみに適用される、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フォルダ配下にある

Laravelがどの設定を使うか決める流れ
Laravel-Environment-Variable@2x (1).png

githubなどを使う場合、.envはセキュリティの観点から、コミットしてはいけない
コミットする場合は、.env.exampleに変数のみを記載してコミットすること

例えば、データベースに関する設定はdatabase.phpに記述がある

デフォルトで使うRDMSの設定

'default' => env('DB_CONNECTION', 'mysql'),

第1引数は.envファイルの変数名を記載
第2引数は、.envファイルに記述がない場合のデフォルト値

データベースとMigration

データベースへのアクセスの種類
Databases@2x (3).png

マイグレーション(データベース作成)ファイルの概要
Laravel+Migrations@2x (1).png

マイグレーションファイルの例

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)は別物、使えるメソッドが違うので注意

参照:【Laravel】Eloquentを理解する

保存する

$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フィールドだけですが、ブラウザをちょっといじれば入力項目を追加できます。これが複数代入です。大抵、他の項目も更新出来てしまう問題を回避するために、複数の項目に代入出来ない様にしています。

参照:「複数代入」ってなに?-猿でも分かるlaravel解説-

割り当てを適用する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)

Laravel+Mix@2x (1).png

使っているツール

ツール名 説明
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つに大別される。
Laravel+Bootstrap+Overview@2x (1).png

名前 内容
レイアウト レスポンシブデザインを作るのに役立つ。コンテンツを行と列に配置する。
コンポーネント ボタン、ドロップダウン、フォームなど一般的に使用される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オブジェクトのインスタンスが返るようになっている

参照:https://carbon.nesbot.com/

<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.phpTestCaseクラスを継承する必要がある
単体テストのファイルはtests/Unitディレクトリ配下、機能テストのファイルはtests/Featureディレクトリ配下。ただし、tests/Unitディレクトリ配下のテストは、アプリケーションを起動しないため、データベースやその他のフレームワークにアクセスができない。
またテストクラスのメソッド名はtestから始まる必要がある

リクエスト関連のテストではassert...のメソッドが大量に用意されている。
参照:(Laravel公式)HTTPテスト

テストの実行

テストをターミナルで実行(簡易的な結果)

./vendor/bin/phpunit 

テストをターミナルで実行(詳細な結果)

php artisan test

典型的なテストの形

  1. Arrangeパート(テストをするためのオブジェクトの初期化)
  2. Actパート(オブジェクトに紐づくメソッドの実行)
  3. 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\Authenticateauthenticateで認証チェック

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.phpusernameメソッドで定義している

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.phpattemptLoginメソッド内で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.phpvendor/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になる

参照:(Laravel公式)artisanコンソール

作成する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されたときには、deletingdeletedのイベントがディスパッチされる。
上記イベントをディスパッチするために、モデルに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();

withTrashedonlyTrashedはクエリビルダのインスタンスを返す

取得したモデルがソフトデリートされているかをチェックする

$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::deniesupdate-postが認可されていない場合を判定
abortはヘルパ関数、第2引数で表示するメッセージを定義

認可されていない判定を簡単に記述する

// Controllerに記述
public function edit($id)
{
    $post = BlogPost::findOrFail($id);

    $this->authorize('update-post', $post);

    return view('posts.edit', ['post' => $post]);
}

Controllerの場合、$thisauthorizeヘルパ関数が使える
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が呼ばれる

参照:(Laravel公式)リソースコントローラの認可

リソースポリシーとリソースコントローラを使っている場合は、コントローラのコンストラクターで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.phpx-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でネストしたcommentsuserへアクセスできる

複数のリレーションへのアクセス

複数のリレーションは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>

formenctype="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_idimageable_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()だけでアクセスができる

ライフサイクル

  1. autoloadを読み込んで、requireしなくても名前空間を指定するだけで良くなる
  2. Applicationインスタンスを作成(サービスコンテナ)、サービスプロバイダも読み込まれる
  3. HttpKernelインスタンスを作成
  4. Requestインスタンスを作成
  5. HttpKernelがリクエストを処理してResponseを取得
  6. レスポンスを送信
  7. terminate()でリクエストとレスポンスを削除

サービスコンテナ

依存関係を自動で解決してくれる。
依存関係のさらに先の依存関係まで全部解決してくれる。
app()はサービスコンテナを呼び出すヘルパ関数。

コンテナへの登録

app()->bind()

// 1度しか依存関係を解決しなくて良い場合
app()->singleton()

登録されているものを使う

app()->make()

// 他の書き方
app('登録したメソッド名');
app()->resolve();
App::make();

サービスプロバイダ(提供者)

サービスコンテナにサービスを登録する。

サービスプロバイダの作成

php artisan make:provider サービスプロバイダ名アッパーキャメルケース

登録

register()

登録後に実行したい処理

boot()
4
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?