Laravel 10 入門トップページ


目次

  1. API とプロジェクトの概要
  2. プロジェクトの作成と初期設定
  3. データベースのマイグレーション
  4. シーダによるコメントデータの登録
  5. モデルの生成
  6. リソースの生成
  7. GETメソッドを利用した個別コメント取得のAPI作成
  8. 日本語文字列の表示と日時の調整
  9. GETメソッドを利用したコメント一覧取得のAPI作成
  10. データのラップ
  11. POSTメソッドを利用したコメントの新規投稿APIの作成
  12. PUTメソッドを利用したコメント更新APIの作成
  13. DELETEメソッドを利用したコメント削除APIの作成
  14. シーダの拡張
  15. ページネーションの実装
  16. 個別コメントをコントローラで取得
  17. Postman の利用
  18. ユーザ情報を登録する
  19. Sanctum によるユーザ認証
  20. ログインとトークン
  21. コメントとユーザのリレーションシップ
  22. 新規投稿時にユーザIDを記録する
  23. コメントからユーザ名を表示するリレーションシップ
  24. ユーザからコメント一覧を取得するリレーションシップ
  25. 更新と削除の権限設定
  26. 発行済みトークンの取得
  27. トークンの有効期限
  28. レート制限
  29. 閲覧権限の緩和

Laravel で API を開発する

ページネーションの実装

現時点では高々100件のデータですが,数万件,数百万件,数億件といったようにデータ件数が多くなったときに全てのデータをまとめて取得することは現実的ではありません.ここではページネーションを利用して一部のデータだけを取得するようにします.まず,次のとおりルートを変更します.具体的には,これまでルートの定義の中でコメントを一覧で取得していましたがこれをコメントアウト(または削除)して,コントローラの index 関数でコメントの一覧を取得するようにします.

routes/api.php (抜粋)Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::get('/comments/{id}', function (string $id) {
    return new CommentResource(Comment::findOrFail($id));
});

// Route::get('/comments', function () {
//     return new CommentCollection(Comment::get());
// });

Route::get('/comments', [CommentController::class, 'index']) -> name('comments.index');
Route::post('/comments', [CommentController::class, 'store']) -> name('comments.store');
Route::put('/comments/{comment_id}', [CommentController::class, 'update']) -> name('comments.update');
Route::delete('/comments/{comment_id}', [CommentController::class, 'destroy']) -> name('comments.destroy');

次に,コントローラに index 関数を作成します.一覧取得の際には最終更新日時を降順でソートして結果から指定されたページの3件のみを取得するようにしています.

app/Http/Controllers/CommentController.php<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Models\Comment;
use App\Http\Resources\CommentResource;
use App\Http\Resources\CommentCollection;

class CommentController extends Controller
{
    public function index()
    {
        $comments = Comment::orderBy('updated_at','DESC')
                    ->paginate(3);
        return new CommentCollection($comments);
    }

    public function store(Request $request)
    {
        $comment = new Comment();
        $comment->title = $request->title;
        $comment->body = $request->body;
        $comment->save();
        return new CommentResource($comment);
    }

    public function update(Request $request, $comment_id)
    {
        $comment = Comment::findOrFail($comment_id);
        $comment->title = $request->title;
        $comment->body = $request->body;
        $comment->save();
        return new CommentResource($comment);
    }

    public function destroy(Request $request, $comment_id)
    {
        $comment = Comment::findOrFail($comment_id);
        $comment->delete();
        return response()->json();
    }
}

さらに CommentCollection リソースを次のように変更します.

app/Http/Resources/CommentCollection.php<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class CommentCollection extends ResourceCollection
{
    /**
     * 適用する「データ」ラッパー
     *
     * @var string|null
     */
    public static $wrap = 'comments';
    /**
     * Transform the resource collection into an array.
     *
     * @return array<int|string, mixed>
     */
    public function toArray(Request $request): array
    {
       return [
            'comments' => $this->collection,
         ];
        // return parent::toArray($request);
    }

    /**
     * Customize the outgoing response for the resource.
     *
     * @param  \Illuminate\Http\Request
     * @param  \Illuminate\Http\Response
     * @return void
     */
    public function withResponse($request, $response)
    {
        $response->header('Charset', 'utf-8');
        $response->setEncodingOptions(JSON_UNESCAPED_UNICODE);
    }
}

実際に curl コマンドを用いてコメントの一覧を取得します.ページを指定しなければ更新日時の最も新しい3件が取得されます.同時に,他のページを取得するためのURIも数多く取得できていることも確認できます.その関係でかなり見辛くなっています.

C:\Users\Rinsaka>curl http://192.168.56.101:8000/api/comments/ ⏎
{"comments":[{"id":56,"title":"山口 七夏","body":"5878352  愛媛県廣川市西 区田中町笹田6-8-6 コーポ三宅108号 \/ kumiko.kudo@example.net","updated_at":"2023-11-30T19:57:49.000000Z"},{"id":99,"title":"斉藤 舞","body":"7186306  千葉県宮沢市南区桐山町鈴木9-1-5 \/ yoshida.nanami@example.org","updated_at":"2023-11-30T02:25:44.000000Z"},{"id":8,"title":"高橋 洋介","body":"1981576  秋田県小林市北区山口町野村4-4-1 \/ kudo.akira@example.net","updated_at":"2023-11-29T19:54:07.000000Z"}],"links":{"first":"http:\/\/192.168.56.101:8000\/api\/comments?page=1","last":"http:\/\/192.168.56.101:8000\/api\/comments?page=34","prev":null,"next":"http:\/\/192.168.56.101:8000\/api\/comments?page=2"},"meta":{"current_page":1,"from":1,"last_page":34,"links":[{"url":null,"label":"« Previous","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=1","label":"1","active":true},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=2","label":"2","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=3","label":"3","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=4","label":"4","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=5","label":"5","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=6","label":"6","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=7","label":"7","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=8","label":"8","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=9","label":"9","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=10","label":"10","active":false},{"url":null,"label":"...","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=33","label":"33","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=34","label":"34","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=2","label":"Next »","active":false}],"path":"http:\/\/192.168.56.101:8000\/api\/comments","per_page":3,"to":3,"total":100}}
C:\Users\Rinsaka>

上で出力された JSON 形式のデータを整形すると次のようになります.これを見ると,"comments" というキーに最新の3件のコメントが記録され,それ以外にも "links""meta" をキーに様々な情報が記録されていることがわかります.

整形した1ページ目の結果{
    "comments": [
        {
            "id": 56,
            "title": "山口 七夏",
            "body": "5878352  愛媛県廣川市西 区田中町笹田6-8-6 コーポ三宅108号 / kumiko.kudo@example.net",
            "updated_at": "2023-11-30T19:57:49.000000Z"
        },
        {
            "id": 99,
            "title": "斉藤 舞",
            "body": "7186306  千葉県宮沢市南区桐山町鈴木9-1-5 / yoshida.nanami@example.org",
            "updated_at": "2023-11-30T02:25:44.000000Z"
        },
        {
            "id": 8,
            "title": "高橋 洋介",
            "body": "1981576  秋田県小林市北区山口町野村4-4-1 / kudo.akira@example.net",
            "updated_at": "2023-11-29T19:54:07.000000Z"
        }
    ],
    "links": {
        "first": "http://192.168.56.101:8000/api/comments?page=1",
        "last": "http://192.168.56.101:8000/api/comments?page=34",
        "prev": null,
        "next": "http://192.168.56.101:8000/api/comments?page=2"
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 34,
        "links": [
            {
                "url": null,
                "label": "« Previous",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=1",
                "label": "1",
                "active": true
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=2",
                "label": "2",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=3",
                "label": "3",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=4",
                "label": "4",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=5",
                "label": "5",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=6",
                "label": "6",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=7",
                "label": "7",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=8",
                "label": "8",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=9",
                "label": "9",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=10",
                "label": "10",
                "active": false
            },
            {
                "url": null,
                "label": "...",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=33",
                "label": "33",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=34",
                "label": "34",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=2",
                "label": "Next »",
                "active": false
            }
        ],
        "path": "http://192.168.56.101:8000/api/comments",
        "per_page": 3,
        "to": 3,
        "total": 100
    }
}

次にリクエストのURIにパラメータ ?page=4 を付与して4ページ目の3件を取得します.

C:\Users\Rinsaka>curl http://192.168.56.101:8000/api/comments/?page=4 ⏎
{"comments":[{"id":63,"title":"杉山 英樹","body":"7291705  福岡県工藤市東 区中津川町山本2-6-10 \/ tsubasa.sato@example.net","updated_at":"2023-11-28T02:33:10.000000Z"},{"id":24,"title":"三宅 直樹","body":"9878173  北海道渡辺市北区加藤町笹田4-7-2 \/ qkondo@example.org","updated_at":"2023-11-28T02:21:52.000000Z"},{"id":82,"title":"山田 治","body":"9102099  奈良県高橋市 南区松本町山田9-6-7 コーポ田中103号 \/ mikako.tsuda@example.com","updated_at":"2023-11-27T22:21:58.000000Z"}],"links":{"first":"http:\/\/192.168.56.101:8000\/api\/comments?page=1","last":"http:\/\/192.168.56.101:8000\/api\/comments?page=34","prev":"http:\/\/192.168.56.101:8000\/api\/comments?page=3","next":"http:\/\/192.168.56.101:8000\/api\/comments?page=5"},"meta":{"current_page":4,"from":10,"last_page":34,"links":[{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=3","label":"« Previous","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=1","label":"1","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=2","label":"2","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=3","label":"3","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=4","label":"4","active":true},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=5","label":"5","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=6","label":"6","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=7","label":"7","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=8","label":"8","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=9","label":"9","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=10","label":"10","active":false},{"url":null,"label":"...","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=33","label":"33","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=34","label":"34","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=5","label":"Next »","active":false}],"path":"http:\/\/192.168.56.101:8000\/api\/comments","per_page":3,"to":12,"total":100}}
C:\Users\Rinsaka>

最後のページは34ページ目です.パラメータ ?page=34 を付与して一覧で取得します.

C:\Users\Rinsaka>curl http://192.168.56.101:8000/api/comments/?page=34 ⏎
{"comments":[{"id":1,"title":"最初のコメント","body":"最初のコメントです!","updated_at":"2023-10-02T10:10:10.000000Z"}],"links":{"first":"http:\/\/192.168.56.101:8000\/api\/comments?page=1","last":"http:\/\/192.168.56.101:8000\/api\/comments?page=34","prev":"http:\/\/192.168.56.101:8000\/api\/comments?page=33","next":null},"meta":{"current_page":34,"from":100,"last_page":34,"links":[{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=33","label":"« Previous","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=1","label":"1","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=2","label":"2","active":false},{"url":null,"label":"...","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=25","label":"25","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=26","label":"26","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=27","label":"27","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=28","label":"28","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=29","label":"29","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=30","label":"30","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=31","label":"31","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=32","label":"32","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=33","label":"33","active":false},{"url":"http:\/\/192.168.56.101:8000\/api\/comments?page=34","label":"34","active":true},{"url":null,"label":"Next »","active":false}],"path":"http:\/\/192.168.56.101:8000\/api\/comments","per_page":3,"to":100,"total":100}}
C:\Users\Rinsaka>

4ページ目と最終ページの結果も整形したものを示しておきます.

整形した4ページ目の結果{
    "comments": [
        {
            "id": 63,
            "title": "杉山 英樹",
            "body": "7291705  福岡県工藤市東 区中津川町山本2-6-10 / tsubasa.sato@example.net",
            "updated_at": "2023-11-28T02:33:10.000000Z"
        },
        {
            "id": 24,
            "title": "三宅 直樹",
            "body": "9878173  北海道渡辺市北区加藤町笹田4-7-2 / qkondo@example.org",
            "updated_at": "2023-11-28T02:21:52.000000Z"
        },
        {
            "id": 82,
            "title": "山田 治",
            "body": "9102099  奈良県高橋市 南区松本町山田9-6-7 コーポ田中103号 / mikako.tsuda@example.com",
            "updated_at": "2023-11-27T22:21:58.000000Z"
        }
    ],
    "links": {
        "first": "http://192.168.56.101:8000/api/comments?page=1",
        "last": "http://192.168.56.101:8000/api/comments?page=34",
        "prev": "http://192.168.56.101:8000/api/comments?page=3",
        "next": "http://192.168.56.101:8000/api/comments?page=5"
    },
    "meta": {
        "current_page": 4,
        "from": 10,
        "last_page": 34,
        "links": [
            {
                "url": "http://192.168.56.101:8000/api/comments?page=3",
                "label": "« Previous",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=1",
                "label": "1",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=2",
                "label": "2",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=3",
                "label": "3",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=4",
                "label": "4",
                "active": true
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=5",
                "label": "5",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=6",
                "label": "6",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=7",
                "label": "7",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=8",
                "label": "8",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=9",
                "label": "9",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=10",
                "label": "10",
                "active": false
            },
            {
                "url": null,
                "label": "...",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=33",
                "label": "33",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=34",
                "label": "34",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=5",
                "label": "Next »",
                "active": false
            }
        ],
        "path": "http://192.168.56.101:8000/api/comments",
        "per_page": 3,
        "to": 12,
        "total": 100
    }
}
整形した34ページ目の結果{
    "comments": [
        {
            "id": 1,
            "title": "最初のコメント",
            "body": "最初のコメントです!",
            "updated_at": "2023-10-02T10:10:10.000000Z"
        }
    ],
    "links": {
        "first": "http://192.168.56.101:8000/api/comments?page=1",
        "last": "http://192.168.56.101:8000/api/comments?page=34",
        "prev": "http://192.168.56.101:8000/api/comments?page=33",
        "next": null
    },
    "meta": {
        "current_page": 34,
        "from": 100,
        "last_page": 34,
        "links": [
            {
                "url": "http://192.168.56.101:8000/api/comments?page=33",
                "label": "« Previous",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=1",
                "label": "1",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=2",
                "label": "2",
                "active": false
            },
            {
                "url": null,
                "label": "...",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=25",
                "label": "25",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=26",
                "label": "26",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=27",
                "label": "27",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=28",
                "label": "28",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=29",
                "label": "29",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=30",
                "label": "30",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=31",
                "label": "31",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=32",
                "label": "32",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=33",
                "label": "33",
                "active": false
            },
            {
                "url": "http://192.168.56.101:8000/api/comments?page=34",
                "label": "34",
                "active": true
            },
            {
                "url": null,
                "label": "Next »",
                "active": false
            }
        ],
        "path": "http://192.168.56.101:8000/api/comments",
        "per_page": 3,
        "to": 100,
        "total": 100
    }
}

目次に戻る