Laravelでtry~catchとthrowを実験.Exception処理の動きを理解

Laravelでの例外処理について解説していきます。

Q. try ~ catch 処理とは

下記の記法です。

try {
    // 例外が発生する可能性のあるコード
} catch (Exception $e) {
    // 例外が発生した場合に行う処理
}

例外が発生する可能性がある場合に、
例外が発生すると処理が中断したり・停止することを防ぐために
try{ } で判断して → catch{ } で対処します。

Q. try ~ catch 例外処理を実施してみる

public function testExcetion() {
    try {
        // 例外が発生する可能性のあるコード
        $error = 5/0; // errorが発生する処理を実行
throwする必要有り。
    } catch (Exception $e) {
        // 例外が発生した場合に行う処理
    }
}

この処理を実施した場合、何も発生しません。
本来であれば、$error = 5/0; が実施されると Division by zero error のエラーが発生します。しかし、上記のtry ~ catch 処理の場合は何も表示されません。

それは、tryで例外が発生したものをキャッチして、catch内で処理させています。catch内では、受け取った例外に対して何も扱っていないので、変化無しです。

Q. try ~ catch 例外処理を受け止め

public function testExcetion() {
    try {
        // 例外が発生する可能性のあるコード
        $error = 5/0;
throwする必要有り。
    } catch (Exception $e) {
        // 例外が発生した場合に行う処理
        echo $e->getMessage(); // error messageを取得 (echo で出力)
    }
}

Division by zero error が表示されます。

Q. try は何をエラーと判断しているのか

public function testExcetion() {
    try {
        // 例外が発生する可能性のあるコード
        throw new Exception("error発生です!!!!");
throwする必要有り。
    } catch (Exception $e) {
        // 例外が発生した場合に行う処理
        echo $e->getMessage(); // error messageを取得 (echoで出力)
    }
}

error発生です!!!! が出力されます。

try{}内では、Exception を throwされることをきっかけに検知して、catchへ処理を移します。 (Exceptionは様々なClassあり)

Request後にFormRequestのバリデーションデータを操作する

標題の通りですが、どのような機会に必要でしょうか。

私が遭遇した場面ですと、POST Requestにはuser_idを含めない but バリデーションrulesの中で、user_idを利用したい場合です。
例えば、category_idとuser_idの掛け合わせた場合のUnique処理などです。

validationData()を利用.

FormRequestのクラス内でvalidationDataを使うとバリデーションデータを変更可能 ※ただし、Requestデータが変更される訳ではないのでご注意を。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;

class CommentRequest extends FormRequest
{
    //
    //省略
    //
    protected function validationData()
    {
        $data = $this->all();
        $data['user_id'] = Auth::id();

        // dd($data); // $dataを確認して見て下さい。

        return $data;
    }

    public function rules()
    {
        $user_id = $this->validationData()['user_id']; //validationData()を呼び出しています。

        return [
            'comment' => 'required',
            'category_id' => 
                ['required',Rule::unique('comments', 'channel_id')->where('user_id', $user_id)],            
          ];
    }    

    //
    //省略
    //

}

他のメソッドも合わせて紹介されている下記、ご参考に!
※ メソッドをpublic指定して下さいとErrorが出たので、ご注意を。

FormRequestでバリデーションを行う前に値を操作する

Docker環境構築 / Vue.jsをさくっと起動させるまで.

ディレクトリ構成

/Vue-Test
 L Dockerfile
 L docker-compose.yml

①:Dockerfile/docker-compose ファイルを準備

①-1:Dockerfile
FROM node:17-alpine3.14

WORKDIR /work

RUN apk update && \
    npm install -g npm && \
    npm install -g @vue/cli

EXPOSE 8080
ENV HOST 0.0.0.0
①-2:docker-compose.yml
version: '3'
services:
  vue:
    container_name: vue
    build: 
      context: .    
      dockerfile: ./Dockerfile
    ports:
      - "8080:8080"
    volumes:
      - ./server:/work:delegated      
    tty: true

②:docker コンテナの起動

②-1:ディレクトリに移動

cd Vue-Test

②-2:コンテナ起動

docker-compose up -d

③:コンテナに入り、Vueプロジェクトを作成

③-1:コンテナへ移動

docker-compose vue exec vue sh

③-2:Vueプロジェクトを作成

first-app 名称でプロジェクトを作成

vue create first-app

③-3:Vueのバージョンを選択

Default ([Vue 3] babel, eslint) 
❯ Default ([Vue 2] babel, eslint) 

③-4:package manageを選択

Use Yarn 
❯ Use NPM

これでインストールが開始します。

④:プロジェクトの起動

④-1:プロジェクトへ移動

cd first-app

④-2:serve

npm run serve

⑤:ブラウザで確認

下記へアクセスして、確認して下さい。

http://localhost:8080/

パラメータの使わけ出来ていますか? ルート/クエリ/リクエスト /Laravel

パラメータの使い分け出来ていますでしょうか。
私は恥ずかしながら、感覚で使っている部分が多かったです。 Getの場合はとりあえず、ルートパラメータかな?? POSTの追加処理にもクエリパラメータを利用したり….

それぞれの違い・役割と、Requestの処理の中身を見ていきたいと思います。

①:ルートパラメータ

文字通り、URLに含めるパラメータです。
ccccのようにパラメータを設定して、ルートの出し訳をします。

domain.com/user/{cccc}/show

②:クエリパラメータ

Queryにパラメータを追加したパラメータです。

domain.com/user/show?post_id=50

これは、ControllerからGET/POSTする際に設定する機会が多いかと思います。

<form method="POST" action="{{ route('users.post', ['user_id' => $user_id]) }}">
        <input type="text" name="comment">
</form>

[‘user_id’ => $user_id] こちらが、クエリパラメータとして送信されます。
<input type=”text” name=”comment”> こちらは、リクエストパラメータとして送信されます。

②-1:Requestの中身からクエリパラメータを確認

dd($request)で確認頂くと。

 +query: Symfony\Component\HttpFoundation\InputBag {#1297 ▼
    #parameters: array:4 [
      "_token" => "XXXXXXXXXXXXXX"
      "user_id" => "1"
    ]
  }

  +request: Symfony\Component\HttpFoundation\InputBag {#1298 ▼
    #parameters: array:4 [
      "_token" => "XXXXXXXXXXXXXX"
      "comment" => "こんにちは"
    ]
  }

query >> parameters・request >> parameters にそれぞれ含まれます。

③:リクエストパラメータ

リクエストのBodyタグの中身に含めるパラメータです。

<form method="POST" action="{{ route('users.post') }}">
        <input type="text" name="comment">
        <input type="hidden" name="user_id" value="{{$user->id}}">
</form>

下記がリクエストパラメータとして送信されます。

    <input type="text" name="comment">
    <input type="hidden" name="user_id" value="{{$user->id}}">

③-1:Requestの中身からクエリパラメータを確認

dd($request)で確認頂くと。

  +request: Symfony\Component\HttpFoundation\InputBag {#1298 ▼
    #parameters: array:4 [
      "_token" => "XXXXXXXXXXXXXX"
      "user_id" => "1"
      "comment" => "こんにちは"
    ]
  }

+request >> parametersに含まれています。

私よりも何倍も詳細に、解説されてる記事を見つけたのでご参考にしてください。

[RESTful API]パスパラメータ、クエリパラメータ、リクエストボディの違いと設計

リレーション先に制約を設けた状態でCount / Laravel withCountにクエリ制約

リレーション先に制約を設けた状態でCountする方法を解説していきます。

withCountを使っていきますが、withによるリレーション先のデータに制約を設けたデータをcountしていきます。

School Modelを準備します。 Student ModelとhasManyでリレーションしていることとします。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class School extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
    ];

    public function student()
    {
        return $this->hasMany(Studnet::class, 'id', 'school_id');
    }

}

任意のControllerにて、制約を設けたwithCountをします。
リレーション先( student )の sexカラム = woman であるデータを限定して、countしてくれます。

$school_with_student_count = 
School::withCount(['student' => function ($query) {
    $query->where('sex', 'woman');
}])
->get();

これで、School modelに
student_count というカラム、countデータが追加されています。

checkboxのPOST処理/validation / Laravel

LaravelでcheckboxのPOST処理と、そのValidation方法を解説していきます。

①:checkboxのPOST処理

name、valueをcheckboxに応じた書き方で処理していきます。

<section class="">
    @foreach($tags as $tag)
        <label class="inline-flex items-center mt-3">
            <input type="checkbox" name="tag_id[]" value="{{$tag->id}}" class="form-checkbox"><span>{{$タグ名称}}</span>
        </label>
    @endforeach
</section>

checkboxで使われる処理、name=”tag_id[]”

tagを3つ選択して、POSTした場合下記のようなデータがrequestに含まれます。

value=”{{$tag->id}}”で指定したtag_idが配列に含まれます。

    #parameters: array:2 [▼
    "_token" => "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "tag_id" => array:3 [▼
      0 => "14"
      1 => "15"
      2 => "16"
    ]

②:配列で受けたPOSTに対してValidation

適用したいルール
・データが空ではない (tag_id がデータ有り)
・tag_idが数字である (value=”{{$tag->id}}”)

下記のようなvalidationメソッドの記述をします。

        $validated = $request->validate([
            'tag_id' => 'required',
            'tag_id.*' => 'required|integer',            
        ]);

ポイントはこちらです。

'tag_id.*' => 'required|integer',            

“tag_id” => array:3 [▼ に含まれる 配列の中身に対してvalidationを適用しています。

Laravel Modelを””複数条件””で検索を色々/whereIn/orWhere/orWhereHas/function($query)

Modelを複数条件で抽出際の方法解説します。
複数条件といっても様々な複数条件が存在するかと思います。
使用用途別に解説していけたらと思います。

①:複数のカラムで、’どれか’のカラムの検索条件に合致する場合

orWhereを利用します。

sex, age, 複数カラムで検索をしています。

Human::
    where('sex', 'men')
    ->orWhere('age', '>=', 40)
    ->get();

②:特定のカラムで、複数の検索条件の’どれか’に該当

whereInを利用します。

Human::
    whereIn('age', [30,31,32,33,40,41,42,43])
    ->get();

ageカラムの中で、該当するものを抽出します。

③:複数のカラムで、複数の検索条件に該当する場合

whereInをorWhereで接続して利用します。 

$nationality = ['japan', 'china', 'korean'];

Human::
    whereIn('age', [30,31,32,33,40,41,42,43])
    ->orWhere(function($query) use($nationality) {
            $query->whereIn('nationality',$nationality);
        })
    ->get();

④:複数テーブルで、’どれか’の検索条件に該当

下記記事にて記述しています。

複数テーブルと書いていますが、厳密には1つのModelからリレーション先のデータを他のテーブルデータとして捉えています。

⑤:複数テーブルで、複数カラムの検索条件に該当

EducationalBackground
 の国が$countries含まれる OR 専攻が$magersに含まれる
OR
Job
 の専門性が$specialtiesに含まれる

Human::
    whereHas('EducationalBackground', function ($query) use($countries, $magers) {

        return $query->whereIn('country',$countries);
                    ->orWhere(function($query) use($magers) {
                        $query->whereIn('mager',$magers);
                    });
    })
    ->orWhereHas('Job', function ($query) use($specialties) {
        return $query->whereIn('specialty',$specialties);
    })
    ->get();

AjaxのPOST処理に認可処理を追加/Policy/Laravel

LaravelでAjax POST処理のPolicyを作っていきます。今回は、Ajaxの詳細には触れていきません。

Userが、AjaxでCommentの編集をすることを想定した流れとなっています。

①:Policy作成の流れ

①-1:Policyの作成

$ php artisan make:policy CommentPolicy

App\Policiesの配下にCommentPolicy.php が作成される。

①-2:Policyを登録

App\Providers\AuthServiceProvider

protected $policies = []に 作成したPolicyを登録

    protected $policies = [
        Comment::class => CommentPolicy::class,
    ];

①-3:Policyのメソッド記述 (True OR Falseを返す)

実際のPolicyの処理を書いていきます。
①-1:Policyの作成 で作成した CommentPolicy.php に処理を記述していきます。

App\Policies\CommentPolicy.php
<?php

namespace App\Policies;

use App\Models\User;
use App\Models\Comment;
use Illuminate\Auth\Access\HandlesAuthorization;

class CommentPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function update(User $user, Comment $comment)
    {
        return $user->id === $comment->user_id;
    }
}

メソッドの内容としては、非常にシンプルかと思います。

Userのid と commentのuser_idが
・同じ場合はTrueを返し、
・違う場合はFalseを返します。

①-4:Policyの呼び出し

任意のコントローラーにて、①-3:Policyのメソッド記述 で作成したupdateメソッドを呼び出します。
詳細は 「②:AjaxでPolicyを呼び出し/認可」 以降にて記述していきます。

②:AjaxでPolicyを呼び出し/認可

commentの編集をするメソッドに、認可の処理を追加していきます。

CommentController の edit メソッド で実行していきます。

前提として
・ $requestにcomment_idを含めている
・ $requestにinput_textでコメントが含まれている
・ JSに $commentで返す

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

use App\Models\Comment;
use App\Models\User;

class CommentController extends Controller
{

    public function edit(Request $request)
    {

        // comment_idから編集予定のComment modelを取得
        $comment = Comment::where('id', $request->comment_id)->get()->first();

        // 編集処理をするUserの取得    
        $user = Auth::user();

        // 編集User と 編集予定のCommentを作成したUserが 同一なのかを認可
        if($user->can('update',$comment)){

            $comment->comment = $request->input_text;
            $comment->save();

            // JSに$commentを返す。  
            return $comment;
        } else {
            abort(403);
        }
    }

}

③:Ajaxの処理で躓いたところ

正直、上記の処理は、Ajax関係なくない??と思うかと思います。
確かに、上記の処理であれば、Ajax関係ない処理として通用します。

私は、authorize() メソッドを利用した際に躓きました。
authorizeメソッドでは、 Userのインスタンスを渡す必要が無く、自動的に読み込んでくれています。

ここが、AjaxだとUserインスタンスが自動で読み込まれません。

孫リレーション先のカラムを合計する方法/Laravel/N+1問題

孫リレーション先のカラムを合計する方法を解説してきます。

今回は詳しく解説していきませんが、この処理をすることでN+1問題を解決できる機会も多くあるかと思います。

子リレーションの場合は、withSum(‘{{子リレーション}}’, ‘{{対象カラム}}’)を利用して合計していけると思います。

同様に、withSum(‘{{子リレーション.孫リレーション}}’, ‘{{孫の対象カラム}}’)でいけるかな?と思っていたらNGでした。

■解決方法

①:親→孫リレーションを作成
②:親→孫リレーションにて、withSumで集計

という2ステップで実施します。

親子孫のモーチーフとしては、
学校毎のテスト点数の合計を集計する方法を考えていきます。
School → Student → Score とつながっていきます。

①:親→孫リレーションを作成

まずは、親から孫を繋ぐためのリレーションを作成します。

Modelを親、子、孫 2つ用意します。

子:Student Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Student extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'school_id',
    ];

    public function score()
    {        
        return $this->hasMany(Score::class, 'student_id', 'id');
    }

}

孫:Score Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Score extends Model
{
    use HasFactory;

    protected $fillable = [
        'score',
        'student_id',
    ];

    public function student()
    {    
        return $this->belongsTo(Student::class, 'id', 'student_id');
    }
}

親:School Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class School extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
    ];

    // このリレーションが今回の本題です。 親→孫へのリレーションを作成
    public function studentscore()
    {
        return $this->hasManyThrough(
            Score::class, // 孫 Model
            Student::class, // 子 Model
            'school_id', // 子テーブルの親ID
            'student_id', // 孫モデルの子ID
            'id',    // 親テーブルのローカルキー
            'id'    // 子テーブルのローカルキー
        )
    }

}

ポイントは、School Model の hasManyThroughを利用したリレーションメソッドです。
(親→子、子→孫へのリレーションもお忘れなく作成しておいてください。)

上記にて、親→孫リレーションを作成が出来ました。

②:親→孫リレーションにて、withSumで集計

②-1:孫からデータを合計

学校毎の学生の点数を合計する処理を書いていきます。
任意のControllerにて、

$school_score = School::
    withSum('student_score', 'score')
    ->get();

こちらで、学校毎に学生の点数合計が取得出来ます。

②-2:合計データはどこに?

合計データはどこに含まれるのでしょうか。

A. 親データに新しいカラムが追加されます。 カラム名は、

{{親孫のリレーション名}}_sum_{{合計したカラム名}}

$school_scoreの中身に、下記のカラムが追加さています。

studentscore_sum_score

ddでご確認を!