エンジニアHubproduced by エン

若手Webエンジニアのための情報メディア

Laravel実践入門! シンプルなREST APIを実装して学ぶ、多機能なPHPフレームワークの使い方

LaravelはPHPで実装されたオープンソースのWebアプリケーションフレームワークです。自由度が高く、幅広いニーズをサポートし、開発支援も充実し、多くのユーザに使われています。ここではサンプルアプリケーションとしてREST APIの実装を通して、Laravelを利用した開発を体験します。

はじめまして。Webアプリケーション開発や技術サポートを行っている新原@shin1x1です。本記事では、これからLaravelを使ってみたい方を対象に、シンプルなREST APIの実装を通じて、Laravelを利用した開発のイメージを紹介します。フレームワークの詳細には触れていないので、実際の開発で必要な情報は公式マニュアルや書籍、ブログなどを参照してください。

Laravelの概要と特徴

Laravelは、PHPで実装されているオープンソースのWebアプリケーションフレームワークです。Taylor Otwell@taylorotwell氏を中心に、コミュニティによって開発されています。MITライセンスで公開されており、個人の非商用アプリケーションから企業による商用アプリケーションまで幅広く利用できます。

フルスタックフレームワークに分類されており、下記のようなWebアプリケーションで必要となる機能を豊富に持ちます。

  • ルーティング
  • HTTPリクエストハンドリング
  • バリデーション
  • ビューやJSONによるHTTPレスポンス生成
  • データストア(データベース、KVS、ファイルなど)アクセス
  • メール送信
  • データベースマイグレーション
  • テスト支援
  • 通知
  • キャッシュ
  • メッセージキュー連携
  • 外部API連携

PHPには、Laravel以外にも多くのフレームワークがあります。筆者もその中のいくつかを利用してきましたが、現在はLaravelをメインに利用しています。Laravelを利用していく中で筆者が感じる特徴には、下記のようなものがあります。

特徴1. 自由度が高い

Laravelは、アプリケーションに求める制約が少ないフレームワークです。アプリケーションコードディレクトリや名前空間は基本的には自由に変更できます。

また、他のフレームワークではコントローラにフレームワークのクラスを継承することが前提になっているものもありますが、LaravelではPOPO(Plain Old PHP Object)やクロージャで実装できます(Eloquentのように継承が前提のものもあります)

フレームワークのコンポーネントについてもサービスコンテナ(DIコンテナ)で構成されているので、必要があればユーザが任意のコンポーネントを実装して差し替えることも可能です。

こうした自由度の高さにより、ユーザが好むアーキテクチャを採用できるのは大きなメリットです。一般的なMVC(MVC2)だけでなく、レイヤードアーキテクチャやクリーンアーキテクチャといったいろいろな構造でLaravelを利用できます。

一方で、このような自由度の高さがゆえにプロジェクトによって構成が異なっていたり、そもそもどのような構成を取るのがよいかという判断をユーザに委ねられているという面があります。

これは、フレームワークに何を求めるかというスタンスの問題です。デフォルトの構成は用意されているので、アーキテクチャなどにこだわりがなければ、それをそのまま利用すればよいでしょう。ただ構造の変更や拡張が容易な分、独自な構成となっている場合があるということは覚えておきましょう。

特徴2. 幅広いニーズをサポート

PHPでWebアプリケーションを開発するユーザは、実に多様です。単にデータベースから値を取得して、HTMLを組み立てることだけを実現できればよい人もいるでしょう。OOPなどを駆使して、複雑なビジネスアプリケーションやWebサービスを開発している人もいます。また、同じユーザであっても、プロジェクトによって求められる内容は変わるでしょう。

Laravelでは、こうした多様なニーズを満たすようになっています。同じ機能でも、ファサードと呼ばれるクラスメソッド呼び出し、ヘルパー関数による簡便な呼び出し方法、そしてDIでコンポーネントをインジェクトして利用する方法が用意されています。内部的にはファサードやヘルパー関数も同じコンポーネントを利用するようになっているので、どちらでも実現できることは同じです。

簡潔なコードで実現したいときは簡単に、必要なコンポーネントを明確にしてフレームワークへの依存をコントロールしたい時はそのようにできるようになっており、ユーザやプロジェクトによって求められる方法でコンポーネントを利用できます。

一方、これには上述した自由度の高さと似通った面があり、同じプロジェクト内でもコードを書く人によってファサードを使ったり、DIを使ったりが分かれてしまう場合があります。もちろん現場ではコードレビューなどで方法を統一したりするなどを行うのですが、制御できない場合は実現方法が混在して混乱してしまうということがあります。

特徴3. 開発支援の充実

Laravelには、冒頭で記述したWebアプリケーションを実行するための機能だけでなく、開発支援の機能が充実しています。コード生成機能、データベースマイグレーションによるテーブルスキーマ管理やテスト(とくにファンクショナルテスト)のサポート、.envや環境変数による設定情報管理といったものは日常的な開発、運用において強力なサポートとなるでしょう。

他にもCLIアプリケーションを実装する機能を持っており、バッチ処理やワーカー処理、自動化ツールなどをこうした機能を用いて実装できます。

こうした開発支援の機能は、Laravel以外にオープンソースで公開されているものもあるので、そちらを利用することで同様のことは実現できます。ただ、フレームワークにはじめから同梱されているおかげで、ユーザはパッケージを探したり、連携方法を探ったりすることなく、すぐにこうした機能を利用できます。

多様な機能がオールインワンでインテグレートされているのも1つの特徴でしょう。

特徴4. ユーザが多い

これはLaravel自身の特徴というより、それを取り巻く環境の話ですが、ユーザが多いというのも1つの特徴です。GitHubのスター数は60,000を超えており、これはサーバーサイドのWebアプリケーションフレームワークとしては最多です(2020年9月時点)

多くのユーザがいるおかげで、多くのノウハウがインターネットや書籍で公開されています。また、Laravelで活用することを前提としたパッケージも多く存在するので、それらを利用することで効率的な開発が可能です。

Laravelのバージョンと選び方

現在(2020年9月時点)、Laravelは6、7、8の3つのバージョンをサポートしています。それぞれのリリースで、初期バージョンがリリースされた日から一定期間、バグフィックスやセキュリティフィックスがサポートされます。

Laravel 6はLTS(Long-term Support)と呼ばれるリリースで、長期間サポートされます。Laravel 8が最新のバージョンですが、これは通常のリリースです。それぞれのサポート期限は下表のとおりです。

バージョン リリース日 バグフィックス
サポート期限
セキュリティフィックス
サポート期限
6 (LTS) 2019年9月3日 2021年10月5日 2022年9月3日
7 2020年3月3日 2020年10月6日 2021年3月3日
8 2020年9月8日 2021年4月6日 2021年9月8日

これからLaravelを利用するのであれば、どのバージョンを使えばよいでしょうか?

現在(2020年9月時点)の状況であれば、6もしくは8のいずれかを選択することになります。どちらを選ぶかはアプリケーションの要件次第です。長期間安定したバージョンを利用したいのであれば6を、最新機能を利用していきたければ8を選ぶことになります。

この選択はさらに、セキュリティフィックス期限が切れた後にも影響します。例えば6を選択した場合、おそらく長期間利用することになるので、次にバージョンを上げる際は最新バージョンとの差異が大きくなり、アップグレードに手間がかかる可能性があります。

一方、8を選択して最新バージョンに適宜アップグレードしていけば、頻度は増えますが、都度の手間は小さくなります。ご自身やチームの開発状況や方針などを鑑みて、どちらを選ぶか検討してください。

なお、Laravelは6以降、セマンティックバージョニングを採用しているので、メジャーバージョンが変わらない限り、後方互換性が壊れることはありません。マイナーバージョンやパッチバージョンは頻繁に上がりますが、それについては基本的には最新バージョンを利用するとよいでしょう。

サンプル開発に必要な情報と環境の構築

ここから、サンプルアプリケーションの実装を通じて、Laravelを利用した開発を体験してみましょう。

近年のWebアプリケーション開発では、SPA(Single Page Application)やモバイルアプリケーションのように、ユーザが利用するUIはJavaScriptやスマートフォンアプリで実装して、PHPが担うサーバ側はAPIのみを提供するというケースが多くあります。本記事でも、WebのUIは実装せず、REST APIのみをLaravelで実装します。

なお、本記事で実装するソースコードは下記のリポジトリにあります。実装の手順を確認したり、動作イメージを知りたい場合などに活用してください。

https://github.com/shin1x1/eh-laravel-sample

必要なシステム構成

本記事のサンプルアプリケーションを動作させるには、下記のシステム構成を必要とします。

  • PHP:バージョン7.3以降
  • Webサーバ:nginx
  • データベースサーバ:PostgreSQL

ここでは、この構成をDockerを利用して構築します。Dockerはコンテナ技術を利用して、ホストマシン(PC)の中に仮想的な実行環境を構築できます。本記事では詳細には触れませんが、開発現場ではDockerを利用した開発環境の構築が一般的になりつつあります。

PHP自体もDockerコンテナのものを利用するので、PCにPHPをインストールする必要はありません。もしPCにインストールされているPHPのバージョンが古くても問題ありません。

REST APIクライアントツール

REST APIを実装するにあたって、動作確認を行うツールが必要です。本記事では実行例として、curlコマンドを利用します。

また、GUIのREST APIクライアントツールもいくつか公開されているので、必要であればそちらを利用してください。主なGUIツールには以下のようなものがあります。

Docker Desktopのインストール

Dockerを利用するため、Docker Desktopをインストールします。すでにDockerが利用できる環境をお持ちの方は不要です。

Docker Desktop overview | Docker Documentation

MacもしくはWindows版がありますので、ご利用の環境に合わせてダウンロード、インストールしてください。本記事ではMac環境を想定していますが、Windows環境でも基本的な流れは同じです。

Docker Desktopがインストールできたら、ターミナルを開いてdocker --versionコマンドを入力してください。下記のようにDockerのバージョンが表示されればインストールが成功しています。バージョン番号やbuildの後ろの値はインストールを行ったタイミングによって変わっている可能性はあります。

$ docker --version
Docker version 19.03.12, build 48a66213fe

サンプルアプリケーションを実行するには、PHP(php-fpm)、nginx、PostgreSQLという3つのDockerコンテナを利用するので、それらを統合して管理できるようにdocker-composeを利用します。このコマンドはDocker Desktopに同梱されているので、下記のようにdocker-composeコマンドも実行できるか確認しておきます。

$ docker-compose version
docker-compose version 1.26.2, build eefe0d31
docker-py version: 4.2.2
CPython version: 3.7.7
OpenSSL version: OpenSSL 1.1.1g  21 Apr 2020

Laravelプロジェクトの作成

サンプルアプリケーションを実装するため、Laravelプロジェクトを生成します。

ComposerのDockerコンテナが公開されているので、これを利用して次のようにcomposer create-projectコマンドを実行します。

$ docker run --rm -v `pwd`:/opt -w /opt composer:1.10 create-project laravel/laravel todoApp

コマンドを実行すると、新しいLaravelプロジェクトがtodoAppディレクトリに生成されます。todoAppディレクトリの内容を確認すると、プロジェクトのディレクトリやファイルが生成されていることが分かります。

$ tree -L 1 todoApp
todoApp
├── README.md
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── package.json
├── phpunit.xml
├── public
├── resources
├── routes
├── server.php
├── storage
├── tests
├── vendor
└── webpack.mix.js

なお、本記事執筆時は最新版であるLaravel 8のプロジェクトが生成されますが、下記のようにバージョンを指定することで任意のバージョンのプロジェクトを生成することもできます。

# Laravel 6 プロジェクトを生成
$ docker run --rm -v `pwd`:/opt -w /opt composer:1.10 create-project "laravel/laravel=^6.0" todoApp6

docker-compose.ymlのダウンロード

docker-composeで環境構築するため、構成ファイルであるdocker-compose.ymlを下記URLからダウンロードして、todoAppディレクトリに設置してください。

https://raw.githubusercontent.com/shin1x1/eh-laravel-sample/master/docker-compose.yml

$ cd todoApp
$ wget https://raw.githubusercontent.com/shin1x1/eh-laravel-sample/master/docker-compose.yml
$ ls docker-compose.yml
docker-compose.yml

このdocker-compose.ymlには、以下のコンテナが含まれています。

  • nginx:HTTPサーバ(nginx 1.18)
  • php:PHP(php-fpm 7.4)
  • db:データベース(PostgreSQL 12)
  • db-test:テスト用データベース(PostgreSQL 12)
  • composer:Composer

Laravelを実行するためのコンテナをdocker-compose upコマンドで起動します。初回はコンテナのダウンロードを伴うので時間がかかります。

$ docker-compose up -d
Creating network "todoapp_default" with the default driver
Creating todoapp_composer_1 ... done
Creating todoapp_db_1       ... done
Creating todoapp_db-test_1  ... done
Creating todoapp_php_1      ... done
Creating todoapp_nginx_1    ... done

コンテナが起動できれば、Laravelの動作環境が整いました。

ブラウザでhttp://localhost:8000にアクセスすると、下記のようにLaravelの初期画面が表示されます。

laravel-startup

起動中のDockerコンテナを終了・破棄する場合は、docker-compose downコマンドを実行します。

$ docker-compose down
Stopping eh-laravel-sample_nginx_1   ... done
Stopping eh-laravel-sample_php_1     ... done
Stopping eh-laravel-sample_db-test_1 ... done
Stopping eh-laravel-sample_db_1      ... done
Removing eh-laravel-sample_nginx_1    ... done
Removing eh-laravel-sample_php_1      ... done
Removing eh-laravel-sample_composer_1 ... done
Removing eh-laravel-sample_db-test_1  ... done
Removing eh-laravel-sample_db_1       ... done
Removing network eh-laravel-sample_default

サンプル1. Hello APIの実装

Laravelの開発環境が整ったので、下記のようなJSONを返すだけのシンプルなAPIを実装してみましょう。

  • GET /api/hello
    • レスポンス
      • ステータスコード: 200
      • ボディ: {"message": "Hello"}

APIを実装するには、URIパスに対応する処理をルーティングに登録します。ルーティングはroutesディレクトリ以下のファイルに記述します。APIはroutes/api.phpが対象となります。このファイルに下記のコードを追加します。

routes/api.php

Route::get('/hello', function () {
    $message = 'Hello';

    return response()->json([
        'message' => $message
    ]);
});

Route::get()では、GETリクエストに対するルーティングを設定します。第一引数にはURIパスを指定します。プレフィックスの/apiはフレームワークのデフォルト設定で指定されているので、ここでは/helloのみとします。第二引数には処理を行うハンドラを指定します。ハンドラには、クロージャやクラス、クラスのメソッドなどが指定できます。上記ではクロージャを指定しています。

ハンドラの戻り値がHTTPレスポンスとなるので、ここではresponse()->json()を指定してJSON形式のレスポンスを返すようにしています。json()の引数ではレスポンスボディとなるJSONの内容を記述します。上記では、messageキーに対してHelloという文字列を返すようになっています。

これでAPIが実装できました。curlコマンドで/api/helloにGETリクエストを送信すると、HelloがJSONで返ってきます。

$ curl http://localhost:8000/api/hello
{"message":"Hello"}

Hello APIのテストを実装

Laravelには、テストを支援する仕組みがあります。これを利用して、Hello APIのテストを実装してみましょう。

テストは、testsディレクトリ以下に配置します。testsディレクトリ以下には、FeatureディレクトリとUnitディレクトリがあります。前者はAPIのような機能テスト、後者はクラスやメソッドなどの単体テストを配置します。ここでは、APIのテストを記述するのでFeatureディレクトリを利用します。

デフォルトではtests/Featureディレクトリとtests/Unitディレクトリにサンプル用のテストファイルが含まれています。これらは開発には不要なので削除しておきます。

$ rm tests/Feature/ExampleTest.php tests/Unit/ExampleTest.php

テストクラスはartisan make:testコマンドで雛形を生成できます。下記のようにコマンドの後ろにテストクラスのクラス名を指定して実行します。

$ docker-compose exec php ./artisan make:test GetHelloTest
Test created successfully.

生成したテストクラスはtests/Feature/GetHelloTest.phpになります。このファイルにHello APIのテストコードを追加したのが、下記のコードです。

tests/Feature/GetHelloTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;

class GetHelloTest extends TestCase
{
    public function testExample()
    {
        $response = $this->get('/api/hello');

        $response->assertStatus(200);
        $response->assertJson([
            'message' => 'Hello',
        ]);
    }
}

ここでは、/api/helloにGETリクエストを送信して、そのレスポンスとしてステータスコードが200であること、JSONの内容がmessageキーにHelloという文字列が入っていることを確認しています。

LaravelアプリケーションのテストではPHPUnitを利用しているので、phpunitコマンドでテストが実行できます。下記のように実行するとテストが通過し、想定どおりにHello APIが動いていることが確認できます。

$ docker-compose exec php ./vendor/bin/phpunit
PHPUnit 9.3.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:01.300, Memory: 18.00 MB

OK (1 test, 2 assertions)

ここまで、Laravelに新しいAPIを追加して、テストで動作を確認するという流れを見てきました。次は、これをベースにTo-DoアプリのAPIを実装していきましょう。

サンプル2. To-DoアプリケーションAPI

ここで作成する「To-Do」アプリケーションは、ToDoリストにタスクの追加と読込を行うシンプルなアプリケーションです。これらのAPI実装を通じて、Laravelとデータベースを利用した開発を体験してみましょう。実装するAPIは下記の2つです。

  • タスク追加API
    • POST /api/tasks
  • タスク取得API
    • GET /api/tasks/ID

タスクは、タスクの内容を示すタスクと作成日時、更新日時を持ちます。タスクモデルは下図のとおりです。

model

タスクはデータベースのtasksテーブルに格納します。

データベースとの接続設定

はじめにデータベースとの接続設定を行います。docker-compose.ymlで起動しているデータベースの接続情報を、.envの下記の箇所を書き換えて指定します。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

.env(変更後)

DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=pass

なお、.env.gitignoreに含まれており、Gitリポジトリには含まれていません。開発環境などで共通の接続情報を利用する場合は.env.exampleなどに記述しておくとよいでしょう(もちろん、本番用のデータベース接続情報はGitリポジトリに含めないほうがよいので別途管理が必要です)

接続情報が正しいかを確認するために、artisanコマンド(Laravelアプリケーション管理コマンド)でデータベースに接続してみましょう。

下記のようにartisan migrate:statusコマンドを実行して、Migration table not found.というメッセージが出力されれば、データベースへ接続できていることが分かります(マイグレーションテーブルは後述のマイグレーションにて生成されるので、この時点では存在しません)

$ docker-compose exec php ./artisan migrate:status
Migration table not found.

データベースマイグレーション

Laravelには、データーベーススキーマをPHPコードで記述できるマイグレーションという仕組みがあります。

テーブルの追加や削除、カラムの変更、インデックスの追加といった変更情報をマイグレーションで管理しておくことで、開発環境と本番環境やチームメンバーのそれぞれの環境といった異なる環境においても、同じデーターベーススキーマを構築できます。

tasksテーブルを構築するためにマイグレーションファイルを生成します。マイグレーションファイルを作成するには、artisan make:migrationコマンドを利用します。下記では、コマンドに続けてマイグレーション名を指定しています。

$ docker-compose exec php ./artisan make:migration CreateTasksTable
Created Migration: 2020_09_11_020221_create_tasks_table

# 2020_09_11_020221 の部分は実行したタイミングで異なります
$ ls database/migrations/2020_09_11_020221_create_tasks_table.php
database/migrations/2020_09_11_020221_create_tasks_table.php

コマンドを実行するとdatabase/migrations/以下にマイグレーションファイルが生成されます。生成されたマイグレーションファイルは下記のようになっています。

database/migrations/2020_09_11_020221_create_tasks_table.php

<?php

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

class CreateTasksTable extends Migration
{
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('name'); // ①name カラムのコードを追加
            $table->timestamps();
       });
    }

    public function down()
    {
        Schema::table('tasks', function (Blueprint $table) {
              $table->dropIfExists();
        });
    }
}

CreateTasksTableクラスには、updownという2つのメソッドが定義されています。それぞれのメソッドは下記のような役割になっています。

  • upメソッド: テーブル追加、更新、制約の追加などを指定
  • downメソッド: 追加したテーブルを削除するなど、upメソッドの逆の動きを指定

upメソッドでは、taskテーブルを追加しています。Schema::create()はテーブルを追加するメソッドで、第一引数にテーブル名、第二引数にクロージャを指定します。第二引数のクロージャでは引数にIlluminate\Database\Schema\Blueprintのインスタンス$tableが与えられます。このインスタンスにはカラムや制約などを指定するメソッドが多数用意されているので、これらを組み合わせテーブルスキームを設定します。

ここでは、①のコードを追加しており、idname、そしてtimestamps()によってcreated_atupdated_atという4つカラムを指定しています。

downメソッドでは、upメソッドで追加したtasksテーブルを削除するために、$table->dropIfExists()を実行しています。これはtasksテーブルが存在するときのみ、テーブルを削除します。

実装したマイグレーションファイルは、artisan migrateコマンドにてデータベースに反映します。下記のようにコマンドを実行するとtasksテーブルが生成されます。

$ docker-compose exec php ./artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (25.80ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (3.50ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (9.89ms)
Migrating: 2020_09_11_020221_create_tasks_table
Migrated:  2020_09_11_020221_create_tasks_table (6.48ms)

なお、create_users_tablecreate_password_resets_tablecreate_failed_jobs_tableは、Laravelプロジェクトにはじめから含まれるマイグレーションファイルです。これらは不要であれば削除してもかまいません。本記事では影響がないのでそのまま適用しています。

データベースにアクセスしてtasksテーブルを確認してみましょう。下記ではdocker-composeコマンドでdbサービスに対してpsqlコマンドを実行してtasksテーブルのスキーマを表示しています。マイグレーションファイルで定義した内容でテーブルが生成されていることが分かるでしょう。

$ docker-compose exec db psql -Uapp app -c "\d tasks"
                                          Table "public.tasks"
   Column   |              Type              | Collation | Nullable |              Default
------------+--------------------------------+-----------+----------+-----------------------------------
 id         | bigint                         |           | not null | nextval('tasks_id_seq'::regclass)
 name       | character varying(255)         |           | not null |
 created_at | timestamp(0) without time zone |           |          |
 updated_at | timestamp(0) without time zone |           |          |
Indexes:
    "tasks_pkey" PRIMARY KEY, btree (id)

Eloquentを定義

Laravelアプリケーションからデータベースへアクセスするには、大きく分けて3つの方法があります。直にSQLを書く、クエリビルダ、そしてEloquentです。

本記事では、Laravelでよく利用されるEloquentを利用します。Eloquentは、Active RecordパターンのORM(Object Relational Mappging)実装です。テーブルレコードをオブジェクトに見立てて、メソッド呼び出しによってレコードを操作します。

tasksテーブルを操作するEloquentを作ってみましょう。Eloquentを作るにはartisan make:modelコマンドを利用します。下記のようにコマンドの後にクラス名を指定します。クラス名はテーブル名の単数形を指定するとデフォルトの実装でそのテーブルを操作できます。

$ docker-compose exec php ./artisan make:model Task
Model created successfully.

コマンドを実行するとapp/Models/Task.phpに下記のようなEloquentクラスが生成されます。クラスの実装はありませんが、基底クラスに実装があるので、本記事の利用方法であればこのままで問題ありません。

<?php

namespace App\Models;

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

class Task extends Model
{
    use HasFactory;
}

サンプル2-1. タスク追加APIの実装

タスクを追加するAPIを実装していきます。本APIの仕様は下記のようになります。

  • POST /api/tasks
    • リクエスト
      • ボディ: {"name": "タスク名"}
    • レスポンス
      • 正常登録時
        • ステータスコード: 201
        • ボディ: {"id": NEW_ID, "name": "タスク名"}

CreateTaskActionクラスの生成

APIは、上述したHello APIのようにルーティングファイルroutes/api.phpにクロージャで実装してもよいのですが、ここではシングルアクションコントローラとして独立したクラスに実装します。

コントローラはartisan make:controllerコマンドで生成します。下記のようにコマンドの後にクラス名と指定し、さらに--invokableオプションを指定するとシングルアクションコントローラとなります。

$ docker-compose exec php ./artisan make:controller CreateTaskAction --invokable
Controller created successfully.

コントローラはapp/Http/Controllers/CreateTaskAction.phpに生成され、下記のようになっています。クラスは__invokeメソッドのみを持っています。このメソッドがルータからリクエストを受けて処理を行います。

app/Http/Controllers/CreateTaskAction.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CreateTaskAction extends Controller
{
    public function __invoke(Request $request)
    {
        //
    }
}

一般的なコントローラでは、1つのクラスが複数のリクエストを処理するメソッドを持ちますが、本クラスのように単一のルーティングのみ処理するようにすれば、クラスの責務が限定され、理解しやすいコードになります。もちろん、Laravelでは従来の複数のルーティングを処理するコントローラも実装できるので、必要に応じて選択するとよいでしょう。

__invokeメソッドにタスクを追加する処理を追加していきます。まず必要なのが、リクエストされた値のバリデーションです。バリデーションは__invokeメソッド内でも実装できますが、ここではフォームリクエストクラスを用意してそちらで行います。

CreateTaskRequestクラスの生成

バリデーションを行うフォームリクエストクラスを生成するには、下記のようにartisan make:requestコマンドを実行します。コマンド後にクラス名を指定します。

$ docker-compose exec php ./artisan make:request CreateTaskRequest
Request created successfully.

コマンドを実行すると、app/Http/Requests/CreateTaskRequest.phpが生成されます。これに必要なバリデーションを加えたのが下記のクラスです。authorizeメソッドはリクエストユーザにリクエストを認めるかを示すメソッドです。

app/Http/Requests/CreateTaskRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateTaskRequest extends FormRequest
{
    public function authorize()
    {
        return true; // (1) ここでは認証、認可は不要なので true
    }

    public function rules()
    {
        return [
            'name' => 'required|max:100', // (2) バリデーションルールを追加
        ];
    }
}

今回は認証、認可は実装しないのでtrueを返しておきます。rulesメソッドでバリデーションルールを連想配列で返します。ここではnameキーに対するバリデーションルール(必須、100文字以内)を指定しています。

CreateTaskActionクラスの実装

CreateTaskActionクラスに戻って、__invokeメソッドに実装を追加していきましょう。

まず、メソッドの引数を、先程実装したApp\Http\Requests\CreateTaskRequestに変更しておきます。このようにすると、__invokeメソッド内に処理が移る前にバリデーションが行われます。つまり、メソッドに入った時点でバリデーションが通過していることになります。

メソッドの中では、生成したTaskというEloquentを利用して、送信されたタスクをtasksテーブルに保存しています。保存処理が正常に完了すれば、レスポンスとしてステータスコード201とレスポンスボディを返します。

app/Http/Controllers/CreateTaskAction.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\CreateTaskRequest;
use App\Models\Task;

class CreateTaskAction extends Controller
{
    public function __invoke(CreateTaskRequest $request)
    {
        $task = new Task();
        $task->name = $request->validated()['name']; // バリデーションを行った値のみ取得
        $task->save(); // tasks テーブルに保存

        return response()->json(['id' => $task->id, 'name' => $task->name], 201);
    }
}

最後に、ルーティングでCreateTaskActionとURIパスをマッピングします。

routes/api.php

// use文を追記
use App\Http\Controllers\CreateTaskAction;

// 追加
Route::post('/tasks', CreateTaskAction::class);

CreateTaskActionクラスはシングルアクションコントローラなので、クラス名のみ指定します。これで実装は完了です。

実装したAPIをcurlコマンドで試してみましょう。下記のようにcurlコマンドでnew taskという名前のタスクをPOSTリクエストで送信すると、tasksテーブルにタスクが登録されてレスポンスが返ってきます。

$ curl http://localhost:8000/api/tasks -H 'Content-Type: application/json' \
                                       -H 'Accept: application/json' \
                                       -d '{"name": "new task"}' -v
(snip)
< HTTP/1.1 201 Created
< Server: nginx
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/7.4.7
< Cache-Control: no-cache, private
< Date: Tue, 23 Jun 2020 08:35:20 GMT
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 58
<
{"id":1,"name":"new task"}

データベースを確認すると送信されたレコードが登録されていることが分かります。

$ docker-compose exec db psql -Uapp app -c "SELECT * from tasks;"
 id |   name   |     created_at      |     updated_at
----+----------+---------------------+---------------------
  1 | new task | 2020-09-11 05:35:10 | 2020-09-11 05:35:10
(1 rows)

バリデーションエラーが発生するパターンも確認しておきましょう。下記のようにnameキーの値を空文字でリクエストを送信すると422 Unprocessable Entityがレスポンスとして返されます。

$ curl http://localhost:8000/api/tasks -H 'Content-Type: application/json' \
                                       -H 'Accept: application/json' \
                                       -d '{"name": ""}' -v
(snip)
< HTTP/1.1 422 Unprocessable Entity
(snip)
{"message":"The given data was invalid.","errors":{"name":["The name field is required."]}}

この場合、バリデーションでエラーとなっているので、CreateTaskActionのメソッドは実行されません。

タスク追加APIのテストを実装

タスクを追加するAPIのテストを実装してみましょう。

ここではデータベースを利用したテストを実装するので、開発用とテスト用でデータベースを分けるため、phpunit.xmlに下記を追加しておきます。テストではdb-testコンテナを利用します。

phpunit.xml

    <php>
        <!-- 下記を追加 -->
        <server name="DB_HOST" value="db-test"/>
    </php>

下記のように、テストクラスをartisan make:testコマンドで追加します。

$ docker-compose exec php ./artisan make:test PostTaskTest
Test created successfully.

追加したテストクラスは、tests/Feature/PostTaskTest.phpに生成されます。生成されたファイルに必要なコードを追加したのが下記です。

tests/Feature/PostTaskTest.php

<?php

namespace Tests\Feature;

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

class PostTaskTest extends TestCase
{
    use RefreshDatabase; // ①データベースをリセット

    public function testPostTask()
    {
        $name = 'new task';

        $response = $this->postJson('/api/tasks', [  // ②JSON を POST
            'name' => $name,
        ]);

        $id = $response->json(['id']);

        $response->assertStatus(201);  // ③レスポンスの検証
        $response->assertJson([
            'id' => $id,
            'name' => $name,
        ]);

        $this->assertDatabaseHas('tasks', [ // ④データベースの検証
            'id' => $id,
            'name' => $name,
        ]);
    }
}

①では、RefreshDatabaseトレイトをuseしています。これは、テスト実行時に変更したデータベースの状態をリフレッシュするトレイトです。このトレイトをuseすると、テスト実行ごとにマイグレーションを実行した状態で始めることができます。

②では、タスク追加APIを実行するため、postJsonメソッドにて、JSONをPOSTリクエストで送信しています。第二引数の連想配列の内容がJSONとして送信されます。

③では、レスポンスを確認しています。assertStatusメソッドでステータスコードを、assertJsonメソッドでレスポンスボディのJSONをチェックしています。

④では、API実行後にデータベースのtasksレコードに想定されたレコードが存在するか(タスクが追加されているか)を確認しています。assertDatabaseHasメソッドは、第一引数で指定したテーブルに第二引数で指定した値を持つレコードが存在するかどうかを確認するメソッドです。

このテストにより、APIを実行して、レスポンスの検証、データベースの検証を行います。

テストを実行してみましょう。下記のようにphpunitコマンドを実行すると、テストがパスすることが確認できました。

$ docker-compose exec php ./vendor/bin/phpunit
PHPUnit 9.3.8 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:02.307, Memory: 26.00 MB

OK (2 tests, 5 assertions)

実際の開発では、さらにバリデーションエラーを引き起こすテストなど、異常系のテストを追加していくことになりますが、ここでは正常系のテストにとどめておきます。GitHubのサンプルコードには異常系のテストも含まれているので参照してください。

サンプル2-2. タスク取得APIの実装

次に、タスクを取得するAPIを実装します。本APIの仕様は下記のようになります。

  • GET /api/tasks/ID
    • レスポンス
      • 正常時
        • ステータスコード: 200
        • ボディ: {"id": ID, "name":"task"}

タスクを追加するAPIと同様に、コントローラを下記のように生成します。

$ docker-compose exec php ./artisan make:controller GetTaskAction --invokable
Controller created successfully.

生成したコントローラはapp/Http/Controllers/GetTaskAction.phpにあります。このクラスに必要な処理が加えたのが下記です。

app/Http/Controllers/GetTaskAction.php

<?php

namespace App\Http\Controllers;

use App\Models\Task;

class GetTaskAction extends Controller
{
    public function __invoke(int $id)
    {
        $task = Task::query()->findOrFail($id);

        return response()->json([
            'id' => $task->id,
            'name' => $task->name,
        ]);
    }
}

__invokeメソッドの仮引数では$idを受け取ります。これはタスクIDを示すもので、後ほどルーティングにて定義します。メソッド内では、この$idに合致するレコードをtasksテーブルからTaskクラス(Eloquent)を利用して取得します。findOrFailメソッドはidが引数に合致するレコードを取得します。もし合致するレコードが存在しなければ例外をスローします。

最後に取得したレコード(Eloquentインスタンス)の値からレスポンスボディを生成して、それを返します。

ルーティングにGetTaskActionを追加します。下記では、/tasks/{id}へのGETリクエストに対してGetTasksActionクラスを割り当てています。

routes/api.php

<?php
// use文に追加
use App\Http\Controllers\GetTaskAction;

// 追加
Route::get('/tasks/{id}', GetTaskAction::class)->where('id', '[0-9]+');

{id}の箇所は、プレースホルダとして任意のパタメータとして扱うことができます。このidは割り当ててたアクション(クロージャやコントローラメソッド)の仮引数と対応しており、メソッドの仮引数$idに指定された値が与えられます。例えば、/tasks/1へのリクエストであれば仮引数$idの値は1となります。

続くwhereメソッドでは、プレースホルダで指定したパラメータの形式を指定しています。パラメータの形式は、正規表現で指定することができます。リクエストのパラメータが合致しない場合は例外がスローされ、アクションの処理は実行されません。ここでは任意の整数を想定しています。

実装したAPIをcurlコマンドで試してみましょう。下記のようにcurlコマンドで/api/tasks/1にGETリクエストを送信すると、合致するレコードの値がレスポンスとして返ってきます。

$ curl http://localhost:8000/api/tasks/1 -H 'Accept: application/json'
{"id":1,"name":"new task"}

存在しないIDや形式が異なるIDが送信された場合は、404 Not Foundが返されます。

# 存在しないID
$ curl http://localhost:8000/api/tasks/999 -H 'Accept: application/json' -I
HTTP/1.1 404 Not Found
(snip)

# 形式が異なるID
$ curl http://localhost:8000/api/tasks/a -H 'Accept: application/json' -I
HTTP/1.1 404 Not Found
(snip)

タスク取得APIのテストを実装

タスクを取得するAPIのテストを実装しましょう。本APIはtasksテーブルに存在するレコードを取得するので、あらかじめテーブルにレコードを登録しておく必要があります。このようにテストに必要なレコードを登録する機能としてファクトリが利用できます。

ファクトリを生成するにはartisan make:factoryコマンドを利用します。下記のようにコマンドの後にファクトリ名と--modelオプションで利用するEloquentを指定して実行すると、database/factories/TaskFactory.phpが生成されます。

$ docker-compose exec php ./artisan make:factory TaskFactory --model=Task
Factory created successfully.

生成されたファクトリに必要なコードを追加したのが以下です。

database/factories/TaskFactory.php

<?php

namespace Database\Factories;

use App\Models\Task;
use Illuminate\Database\Eloquent\Factories\Factory;

class TaskFactory extends Factory
{
    protected $model = Task::class; // ①Task を指定

    public function definition()
    {
        return [
            'name' => $this->faker->name, // ②レコードに登録する値を指定
        ];
    }
}

①では、先程--modelで指定したEloquentが指定されています。

②では、連想配列でTaskクラスの値tasksテーブルの値)を指定します。$this->faker->nameは、名前とおぼしき文字列を自動生成する機能です。このファクトリを利用することで、nameに名前と思われる文字列を含んだインスタンス(レコード)を生成できます。

APIをテストするテストクラスを、下記のように生成します。

$ docker-compose exec php ./artisan make:test GetTaskTest
Test created successfully.

生成したテストクラスは、tests/Feature/GetTaskTest.phpに生成されます。これに必要なコードを追加したのが下記です。

tests/Feature/GetTaskTest.php

<?php

namespace Tests\Feature;

use App\Models\Task;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class GetTaskTest extends TestCase
{
    use RefreshDatabase;

    public function testGetTask()
    {
        $task = Task::factory()->create(); // ①ファクトリでレコード追加

        $response = $this->get('/api/tasks/' . $task->id); // ②GET リクエスト

        $response->assertStatus(200); // ③レスポンスの検証
        $response->assertJson([
            'id' => $task->id,
            'name' => $task->name,
        ]);
    }
}

①では、上記で実装したファクトリを利用しています。ここでは、createメソッドにて生成したTaskインスタンスをtasksテーブルに保存しています。

②では、ファクトリで生成したインスタンスのidを利用して、タスク取得APIを実行しています。

③では、レスポンスを検証しています。ここでもファクトリで生成したインスタンスを利用して、レスポンスボディのJSONの値を検証しています。

テストを実行してみましょう。下記のようにphpunitコマンドを実行すると、テストがパスすることが確認できました。

$ docker-compose exec php ./vendor/bin/phpunit
PHPUnit 9.3.8 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 00:02.607, Memory: 28.00 MB

OK (3 tests, 7 assertions)

まとめ ─ フレームワークを学ぶ上で大切なこと

シンプルなREST APIの実装を通じて、Laravelを利用したWebアプリケーション開発の流れを見てきました。Laravelには多彩な機能があり、ここで紹介した機能はごく一部です。

フレームワークとしてアプリケーションの基盤となる部分だけではなく、データベースマイグレーションや、artisanコマンドによるコード生成、テスト支援といった仕組みが日々の開発を助けてくれるということを感じていただけたのではないでしょうか。

最後に、フレームワークを学ぶ上で、筆者が大切だと感じていることを2つ紹介します。

フレームワークの動作イメージを持つ

1つは、フレームワークの動作イメージを持つということです。

HTTPリクエストが来て、index.phpからフレームワークが起動して、アプリケーションを実行する。そしてアプリケーションが生成したレスポンスを返す。こうした一連の流れを朧気(おぼろげ)ながらでもイメージしておくことで、フレームワークの理解がよりいっそう進みます。

動作イメージを知る一歩としては、公式ドキュメントのRequest Lifecycleを参照するとよいでしょう。さらに詳細を知りたい場合はフレームワークのコードを順に読むと、そのイメージが鮮明になります。

フレームワークは何か特別なものではなく、PHPコードで書かれた1つのアプリケーションに過ぎません。ブラックボックスにせず、その動きを掴んでみてください。

大事なのは「何を」作りたいか

もう1つは、アプリケーションが主でフレームワークは従であるということです。

フレームワークを学びはじめると、多くの機能に圧倒されてしまい、ともすればフレームワークに習熟することに主眼を置きがちです。もちろんフレームワークを学ぶことも大切なのですが、それより大事なのは、作りたいアプリケーションがあり、それを実現するためにフレームワークがあるということです。

極端なことを言えば、作りたいアプリケーションにとって不要な機能を学ぶ必要はありません。フレームワークを覚えるのではなく、どう利用するか、活用するかという主体的な視点で見ていくとよいでしょう。

本記事が、あなたが開発するWebアプリケーションにLaravelを活用するきっかけになれば幸いです。

新原雅司(しんばら・まさし) @shin1x1

shin1x1
PHPをメインにWebシステムの開発や技術サポートを主業務としている。技術イベントでの講演や「PHPの現場」というPodcastの運営を行っている。主な著書に『Laravelリファレンス』『Laravel Web アプリケーション開発』などがある。
Blog: Shin x Blog
Podcast: PHPの現場

関連記事


  1. このサイトにはInsomnia DesignerとInsomnia Coreがありますが、REST APIクライアントツールはInsomnia Coreです。