Laravel 演習入門

偉人は名言を言った

Routing Model の基礎

問題

以下のような、名言 API を作成してください。


問題は、以下の手順で解いてください。

  1. Red:小さいテストを作成し、失敗を確認してください
  2. Green:テストを成功させてください
  3. Refactor:整理・整頓してください
  4. 必要に応じて、1から3を繰り返してください

ヒント

背景知識
便利なアサーションの例

アサーションの調べ方 も合わせてご覧ください。
今回は、以下を使うのではないかと思います。

解答例

続きを読む

実行環境:

Note

私の環境の Inspiring::quotes() では、Laozi(老子)の名言が2つあったので、これを数え、その2つが表示されているかを試すことにしました。
誰の名言をテストするかは、お使いの環境ごとに、調整してください。
また、存在しない偉人をテストするのも良いと思います。
Inspiring クラスの内容は、 Inspiring PHP クラス をご覧ください。

(前準備)老子の id 番号と名言の個数を確認する

テストを作成します。
bash:

php artisan make:test LaoziQuotesTest

tests/Feature/LaoziQuotesTest.php:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Illuminate\Foundation\Inspiring;

class LaoziQuotesTest extends TestCase
{
    public function test_老子の情報を確認(): void
    {
        $laozi_index = Inspiring::quotes()->map(fn($s)=>trim(explode('-', $s)[1]))->unique()->search('Laozi') + 1;  // 0-index を 1-index に変えるために1を足す
        var_dump('老子のid 番号: ' . $laozi_index);

        $laozi_count = Inspiring::quotes()->map(fn($s)=>trim(explode('-', $s)[1]))->countBy()->all()['Laozi'];
        var_dump('老子の名言の個数: ' . $laozi_count);

        $this->assertTrue(true);
    }
}

上記のテストを実行し、老子の ID 番号が6, 名言数は2つであることを確認しました。
Inspiring class を確認したところ、その2つとは、以下の通りでした:

テストでは、これを確認します。

Red: /quotes/{Laozi(老子)のuser_id} で、老子の名言の個数とその内容が出力されているか

テストを追加します。
tests/Feature/LaoziQuotesTest.php:

    use RefreshDatabase;
    public function test_老子の名言数と内容を確認(): void
    {
        $this->seed();
        $response = $this->get('/quotes/6');
        $response->assertStatus(200)
            ->assertJsonCount(2, 'quotes')
            ->assertSeeText('He who is contented is rich.')
            ->assertSeeText('When there is no desire, all things are at peace.');
    }

テストの失敗を確認します。

Green

ルーティングを作成します。
routes/web.php:

use App\Http\Controllers\QuoteApiController;
Route::get('/quotes/{id}', [QuoteApiController::class, 'index']);

コントローラーを作成します。
bash:

php artisan make:controller QuoteApiController

app/Http/Controllers/QuoteApiController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;

class QuoteApiController extends Controller
{
    public function index(string $id): array 
    {
        $user = User::find($id);
        if(!$user) {
            abort(404);
        }
        return ['quotes' => $user->quotes];
    }
}

Quote Model, Migration, Factory, Seeder 各ファイルを作成します。
bash:

php artisan make:model Quote -fms

マイグレーションを作成します。
database/migrations/…create_quote_table.php:

    public function up(): void
    {
        Schema::create('quotes', function (Blueprint $table) {
            $table->id();
            $table->text('quote');
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->timestamps();
        });
    }

シーダーを作成します。
database/seeders/QuoteSeeder.php:

    public function run(): void
    {
        Inspiring::quotes()->each(function (string $quote_author): void {
            [$quote, $author] = explode('-', $quote_author);
            $quote = trim($quote);
            $author = trim($author);
            $user = User::where('name', $author)->first();
            if(!$user) {
                $user = User::factory()->create(['name' => $author]);
            }
            Quote::factory()->create([
                'quote' => $quote,
                'user_id' => $user->id,
            ]);
        });
    }

database/seeders/DatabaseSeeder.php:

    public function run(): void
    {
        $this->call([
            QuoteSeeder::class,
        ]);
    }

ファクトリーを作成します。
database/factories/QuoteFactory.php:

    public function definition(): array
    {
        return [
            'quote' => '名言',
            'user_id' => 1,
        ];
    }

モデルをリレーションさせます。
app/Models/Quote.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
...
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

app/Models/User.php:

use Illuminate\Database\Eloquent\Relations\HasMany;
...
    public function quotes(): HasMany
    {
        return $this->hasMany(Quote::class);
    }

Refactor

上記の例では、 ルーティングで {id} を文字列で受け取り、 コントローラーで User::where() メソッドで探していますが、実はルーティングの時点で User を受け取ることが可能です。
routes/web.php:

Route::get('/quotes/{user}', [QuoteApiController::class, 'index']);

app/Http/Controllers/QuoteApiController.php:

    public function index(User $user): array 
    {
        return ['quotes' => $user->quotes];
    }

ルートモデルバインディングというそうです。
詳しくは、解説でまとめます。

解説

続きを読む

ルート・モデルバインディング

ルーティングに渡した変数を、コントローラーで(もしくはルーティングの第2引数に渡すコーラブルに) Model 型で渡すと、自動でインスタンスを見つけてくれる仕組みです。

Laravel 公式ドキュメントには、次のような例が載っていました:

use App\Models\User;
 
Route::get('/users/{user}', function (User $user) {
    return $user->email;
});

デフォルトでは、 id を探すようですが、ブログポストにおける slug カラムのように、別のカラムを利用することも可能のようです。
(おそらく、 unique 制約などを付けるのだと思います)

use App\Models\Post;
 
Route::get('/posts/{post:slug}', function (Post $post) {
    return $post;
});

<= 問題を読んだ・解いた・理解したなどのチェックにご利用ください。クリックすると、チェックが変化します。
問題一覧に戻る