Laravel 7.xの新ルート機能(モデル・バインディング)実例

こんにちは!フリーランス・エンジニアの 九保すこひ です。

さてさて、前回記事「新コンポーネント機能!実例」では、Laravel 7.xの新機能の中からコンポーネントについてをご紹介しました。

そして、今回はもうひとつ「ちょっと地味だけど前から欲しかった」機能について実例をまとめてみました。

その内容とは・・・

ルートの新しいバインディング方法

です。

例えば、Laravelではルートを以下のようにしてhttp://your-site.test/user/1にアクセスすると、自動的にID1のデータをUserモデルから取得してくれるようになります。

use \App\User;

Route::get('user/{user}', function(User $user) { // DBからデータを自動取得

    //

});

これは「モデル・バインディング」と呼ばれるものでコード量が少なくて済むLaravelの強力な機能になっています。

ただ、この機能に以前から「あればいいのにな・・・」というものがありました。

それは、

ID以外のデータを使ってバインディングをしたい

というものです。

実は今回のアップデートでこの要望が実装されましたので使い方をご紹介したいと思います。(また、これに関連した便利機能が追加になっているので後で合わせてご紹介します)

ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: Laravel 7.x

DBテーブルの準備

ここからご紹介する内容で必要なDBテーブルは以下の2つです。

  • users ・・・ サイトのユーザー
  • posts ・・・ ユーザーが投稿した内容

そして、テーブルの内容はこのようになっています。

なお、postsテーブルのslugは他のデータと重複「しない」IDのようなものとして利用します。またuser_idusersテーブルに外部キーを通して連携をしています。

そのため、マイグレーションは以下のようになります。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->string('title');
            $table->text('content');
            $table->string('slug'); // 重複しない文字列
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
            $table->unique('slug');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

では、DBテーブルの準備ができたら実際にID以外でモデル・バインディングする方法をみていきましょう!

ID以外でモデル・バインディングする方法

一番シンプルな方法

では、一番シンプルにpostsslugを使ってバインディングする方法です。
実際の例はこうなります。

Route::get('post/{post:slug}', function(\App\Post $post) {

    //

});

そして、このルートにアクセスするためには、例えば、「http://your-site.test/post/post-5」などのようなURLになり、その場合slugpost-5のものが自動的にバインディングされることになります。

モデルの中で指定する

ルートで直接指定する方法が一番お手軽ですが、もし指定するルートが多い場合:以降のフィールド指定を忘れてしまったり、何度も書くのがめんどうになることもあると思います。

その場合は、モデル内でバインディングのターゲットの初期値を指定することもできます。

例えば、先ほどのPostテーブルではこうなります。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function getRouteKeyName()
    {
        return 'slug'; // モデル・バインディングには slug を使う
    }
}

ただし、この状態であってもし以下のようにルートでidを使うよう指定をするとそちらが優先されます。

Route::get('post/{post:id}', function(\App\Post $post) {

    //

});

データの整合性を考慮したバインディング

例えば、以下のようなバインディングが2つ存在するルートがあるとします。

use \App\User;
use \App\Post;

Route::get('user/{user}/post/{post}', function(User $user, Post $post) {

    //

});

この場合、実際にアクセスされるURLは「http://your-site.test/user/1/post/5」のようになり、その場合取得されるのは以下のとおりです。

  • $user ・・・ 「id」が「1」のもの
  • $post ・・・ 「id」が「5」のもの

ただし、この2つのデータに整合性はありません。

つまり、$user->idと、$post->user_idが一致していない場合でもデータがバインディングされて処理が実行されてしまうわけです。

しかし、今回のアップデートで:を使ってバインディングするターゲットを指定すると整合性をとる処理をしてくれるようになりました。

では、実際の例を見てみましょう!

なお、この機能を使うにはリレーションシップが必要なので先にUserモデルにhasManyをセットしておきます。

<?php

// 省略

class User extends Authenticatable
{
    // 省略

    public function posts() {

        return $this->hasMany('App\Post');

    }
}

そして、ルートはこんな形です。

Route::get('user/{user}/post/{post:slug}', function(User $user, Post $post) {

    //

});

この場合、実際にアクセスされるURLは「http://your-site.test/user/1/post/post-5」のようになりますが、もし$user->id$post->user_idが一致しない場合は以下のように404を返すようになります。

つまり、今回私の環境で用意した以下のテストデータの中でいうと、ユーザーが1の場合はpost-9URLでいうと「http://your-site.test/user/1/post/post-9」であれば通常のアクセスができるということになります。

なお、slugを使わないけど整合性もキープしたいという場合は以下のように:idを指定するとOKです。

Route::get('user/{user}/post/{post:id}', function(User $user, Post $post) {

    //

});

おわりに

ということで、今回は新しいモデル・バインディングの方法についてご紹介しました。

この機能が役に立つのは、商品コードを使ったシステムや(今も効果があるのかは疑問ですが)SEO対策としてURLに文字列を入れておきたい場合などが考えられると思います。

また、コード量をこれまでより減らすことができると思うので積極的に活用していきたいですね。

ぜひ皆さんも使ってみてくださいね。

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
このエントリーをはてなブックマークに追加       follow us in feedly