Laravel 5.4でWeb APIを作る

前回の記事では、Laravelでフロントエンド開発を行うための開発環境の作り方を解説しました。今回は、LaravelでWeb APIを作る方法を解説します。

データベースの準備

アプリケーションのデータはデータベースに保存するようにしたいので、セットアップを行います。
Laravelアプリケーションを作成すると、以下の値でデフォルトの接続情報が作成されます。

    • データベース: MySQL

 

    • データベース名: homestead

 

    • ユーザ名: homestead

 

    • パスワード: secret

ローカル開発環境にMySQLがインストール済みなら、上と同じ条件で新しいデータベースを作成するのが手軽です。
手元の環境にMySQLをインストールしたくない、といった場合には、開発用VMのLaravel Homesteadを利用するのが良いでしょう。

DBへの接続情報は、アプリケーションのルートディレクトリの「.env」という隠しファイルに記述します。


$ cat .env
APP_ENV=local
APP_KEY=base64:sWS+TR6ESaDXCXGJKtk7twOtsE2lHVUqJdLUula6t3I=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=

データベース名等が異なる場合は、.envを適宜編集してください。

セットアップができたら、DBに接続できるか確認するため、「php artisan migrate:status」を実行します。なお、Homestead等のVMを使用している場合は、VM内で実行してください。
成功すれば、以下のような出力が得られるはずです。


$ php artisan migrate:status
+------+------------------------------------------------+
| Ran? | Migration                                      |
+------+------------------------------------------------+
| N    | 2014_10_12_000000_create_users_table           |
| N    | 2014_10_12_100000_create_password_resets_table |
+------+------------------------------------------------+

もしもPDOException等のエラーが出てしまった場合は、.envに記述した接続情報が正しいか見直してください。

新しいテーブルの追加

はじめに、「artisan make:migration」を使って、マイグレーションファイルの雛形を作成します(ファイル名は実行した日時によって異なります)。


$ php artisan make:migration create_items_table
Created Migration: 2017_03_16_002633_create_items_table

作成できたら、以下の内容で置き換えます。


<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id');
            $table->text('content');
            $table->boolean('checked')->default(false);
            $table->timestamps();
            $table->foreign('user_id')
                ->references('id')
                ->on('users');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('items');
    }
}

簡単なToDoリストのテーブルで、user_idが持ち主、contentがタスクの内容、checkedが完了済みか否かを示します。
作成できたら「php artisan migrate」コマンドでテーブルを作成します。


$ php artisan migrate
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2017_03_16_002633_create_items_table

もし失敗してしまった場合は、「php artisan migrate:reset」コマンドで、全てのマイグレーションが実行される前の状態に戻しましょう。
また、リセットが上手くいっていない場合には、テーブルが消えずに残ってしまうことがあります。
その場合、データベースにログインしてDROP TABLEでテーブルを削除しましょう。

マイグレーションに成功すると、以下のテーブルが作成されます。


mysql> desc items;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| user_id    | int(10) unsigned | NO   | MUL | NULL    |                |
| content    | text             | NO   |     | NULL    |                |
| checked    | tinyint(1)       | NO   |     | 0       |                |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

 

雛形の作成

APIを作成するためには、(1) itemsテーブルに対応するモデルクラスの作成 (2) Itemの操作を行うためのコントローラーの作成 が必要です。
以下のコマンドを実行すると、上記作業をコマンド一発で行なえます。


$ php artisan make:model Item --controller --resource

make:modelコマンドに「--controller(または-c)」オプションを渡すと、モデルクラスに対応したコントローラーが作成されます。さらに、「--resource(または-r)」オプションを追加すると、「リソースコントローラー」が作成されます。リソースコントローラーでは、show()、edit()等のメソッドの引数にあらかじめモデルクラスが定義されています。また、後述するresourceルートと組み合わせると便利です。


<?php
// (省略)
    /**
     * Display the specified resource.
     *
     * @param  \App\Item  $item
     * @return \Illuminate\Http\Response
     */
    public function show(Item $item)
    {
        //
    }

 

初期データの登録

データベースへの初期データの登録には、Seederという仕組みを使うと便利です。
MySQLにログインしてINSERT文を発行してもいいのですが、ユーザを登録する際にpasswordをbcryptでハッシュ化する作業が必要だったりして、かえって面倒だったりします。Seederでデータを登録できるようにしておくと、テストの際にも利用しやすいので、オススメです。

database/seeds/DatabaseSeeder.phpを以下の内容に書き換えます。


<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = \Faker\Factory::create();
        
        $user = new \App\User();
        $user->name = $faker->name;
        $user->email = $faker->unique()->safeEmail;
        $user->password = bcrypt('password');
        $user->remember_token = str_random(10);
        $user->save();
        
        $item = new \App\Item();
        $item->user_id = $user->id;
        $item->content = $faker->text();
        $item->save();
    }
}

本格的なデータ登録処理を行うならテーブルごとにSeederを作ると良いですが、ここではUserとItemを1つずつ登録したいだけなので、処理をベタ書きしています。
また、Fakerを使用して適当なダミーデータを生成しています。
DatabaseSeederは「php artisan db:seed」コマンドで実行できます。成功したら、以下のようなデータが登録されます(内容はランダムで変わります)。


mysql> select * from users\G
*************************** 1. row ***************************
            id: 1
          name: Prof. Macy Stanton
         email: emarquardt@example.com
      password: $2y$10$fTiwsn9d8VPL81XrTslB4OqT1qv5Si8qYoADECnmFC04AbxVfcEVO
remember_token: ckzIlT20s1
    created_at: 2017-03-16 01:48:48
    updated_at: 2017-03-16 01:48:48
1 row in set (0.00 sec)
mysql> select * from items\G
*************************** 1. row ***************************
        id: 1
   user_id: 1
   content: Qui voluptatem ea qui in. Alias incidunt ullam rem. Et sequi et et atque sequi sunt modi alias. Odit aut sed fugiat natus. Adipisci eum et omnis debitis.
   checked: 0
created_at: 2017-03-16 01:48:48
updated_at: 2017-03-16 01:48:48
1 row in set (0.00 sec)

 

モデルの関連付け

テーブル同士の関係性(Relationships)は、「artisan make model」コマンドでは生成されないため、手書きする必要があります。

UserはItemを0個以上もつので、app/User.phpにitems()メソッドを追加して、hasMany()メソッドを呼び出します。


<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
    use Notifiable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];
    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function items()
    {
        return $this->hasMany(Item::class);
    }
}

同様に、Itemは必ずいずれかのUserに属するので、app/Item.phpを以下のように編集します。


<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

 

ルーティング

コントローラーを呼び出すためのルーティングを定義します。
routes/api.php を以下のように変更します。


<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// 認証は面倒なので一旦省略
Route::resource('/items', 'ItemController', ['except' => ['create', 'edit']]);

APIにも認証が必要ですが、ここでは一旦省略しています(次回の記事で、OAuth 2.0を使った認証方法を紹介します)。
Route::resource()メソッドを使うと、先ほど作成したリソースコントローラーに対応するルートが定義されます。
ただし、新規作成画面(GET /RESOURCE/create)と編集画面(GET /RESOURCE/ID/edit)のルートは、APIには必要ありません。そのため、「 ['except' => ['create', 'edit']]」でcreateとeditのルートは除外しています。
ルートを定義したら、「php artisan route:list」で確認しておきましょう。

コントローラーの実装

ルーティングができたので、コントローラーを実装します。
まずは動作確認のため、app/Http/Controllers/ItemController.phpのindex()メソッドを以下の内容に書き換えます。


<?php
// (省略)
    public function index()
    {
        return response(Item::all());
    }

この状態で、 http://localhost:8000/api/items にアクセスすると、以下のようなJSONが返ってくるはずです。

機能テスト

コントローラーが動くようになったので、コントローラーの機能テストを作成しましょう。
「php artisan make:test ItemTest」で機能テスト(Feature Test)を作成できます。
作成したtests/Feature/ItemTest.phpを以下のように置き換えます。


<?php
namespace Tests\Feature;
use App\Item;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ItemTest extends TestCase
{
    use DatabaseMigrations;
    protected function setUp()
    {
        parent::setUp();
        (new \DatabaseSeeder())->run(); // テストデータ登録
    }
    public function testIndex()
    {
        $response = $this->get('/api/items');
        $response->assertStatus(200);
        $this->assertCount(1, $response->json());
    }
    public function testShow()
    {
        $response = $this->get('/api/items/1');
        $response->assertStatus(200);
    }
    public function testStore()
    {
        $data = ['content' => 'ブログを書く'];
        $response = $this->post('/api/items', $data);
        $response->assertStatus(201);
        $response->assertJson($data);
        $item = Item::query()->find($response->json()['id']);
        $this->assertInstanceOf(Item::class, $item);
    }
    public function testUpdateContent()
    {
        $data = ['content' => 'ブログを書く'];
        $response = $this->patch('/api/items/1', $data);
        
        $response->assertStatus(200);
        $response->assertJson($data);
        $item = Item::query()->find(1);
        $this->assertSame('ブログを書く', $item->content);
    }
    public function testUpdateChecked()
    {
        $data = ['checked' => 1];
        $response = $this->patch('/api/items/1', $data);
        $response->assertStatus(200);
        $response->assertJson($data);
        $item = Item::query()->find(1);
        $this->assertEquals(true, $item->checked);
    }
    public function testDelete()
    {
        $response = $this->delete('/api/items/1');
        $response->assertStatus(200);
        $this->assertNull(Item::query()->find(1));
    }
}

はじめに、「use DatabaseMigrations;」することで、それぞれのテストの実行前にmigrate、実行後にmigrate:rollbackが実行されるようにしています。
これによって、あるテストで作成したデータが別のテストに影響することを避けられます。

テストデータの登録は手抜きをして、先ほど作成したSeederを使ってます。
テストケースはとりあえず正常系だけ書いてます。
やや特殊な点として、更新はcontentまたはcheckedのいずれか単独でも可能な仕様としています。

この状態でテストを実行してもいいのですが、接続先のデータベースが「homestead(デフォルトの場合)」となっているため、できればテスト用のデータベースを分けたいところです。

テスト環境のデータベースの切り換えは、.envなどでもできますが、phpunit.xmlに書くのが手軽でしょう。phpunit.xmlの<php>~</php>で囲まれた部分を、以下のように書き換えます(DB_DATABASEの定義を追加)。


        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_DATABASE" value="test_homestead"/>

また、データベースは自動作成されないので、MySQLにログインして「CREATE DATABASE test_homestead」でテスト用のデータベースを作成しておきます。

用意ができたら、以下のように実行します(まだ実装が完了していないので、エラーになります)。


$ ./vendor/bin/phpunit tests/Feature/ItemTest.php

 

バリデーション

ItemControllerで新しいItemを登録する機能は以下のように実装できます。
※ログインユーザーの取得処理については、ログイン機能実装後に作成します。


<?php
// 省略
    public function store(Request $request)
    {
        $item = new Item();
        // todo: ログインユーザのidが入るようにする
        $item->user_id = \App\User::query()->first()->id;
        $item->content = $request->input('content');
        $item->save();
        return response($item, 201);
    }

現状では、入力値のバリデーションが全くありません。
contentにすごく長い文字列が入っていると、DB保存時にエラーが発生します。

Laravelでバリデーションを実装する方法はいくつかありますが、個人的にはFormRequestを使った方法がオススメです。
FormRequestは以下のコマンドで作成できます。


$ php artisan make:request ItemStoreFormRequest

実行すると、app/Request/ItemFormStoreRequest.phpが追加されます。この中身を以下のように書き換えましょう。


<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ItemStoreFormRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true; // todo: 認証実装
    }
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'content' => 'required|string|max:255',
        ];
    }
}

contentというフィールドの値が、(1) nullまたは空文字列の場合 (2) 文字列ではない場合 (3) 255文字異常の長さの場合 にエラーが返るように設定しています。

次に、ItemControllerのstore()メソッドで使用するRequestクラスを置き換えます。


<?php
// 省略
use App\Http\Requests\ItemStoreFormRequest;
// 省略
    public function store(ItemStoreFormRequest $request)

このようにすると、バリデーションはFormRequest、保存処理はコントローラー、という風に責務を分担できます。

同じように、ItemUpdateFormRequestも作成します。


<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ItemUpdateFormRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true; // todo: 認証実装
    }
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'content' => 'string|max:255',
            'checked' => 'boolean'
        ];
    }
}

登録と更新では、バリデーションの条件が違うので、FormRequestクラスも別にしています。
具体的には、(1) contentは登録では必須だが、更新では必須ではない (2) checkedを変更できるのは更新の場合のみ という違いがあります。

最後に、コントローラーの全体像を掲載します。


<?php
namespace App\Http\Controllers;
use App\Http\Requests\ItemStoreFormRequest;
use App\Http\Requests\ItemUpdateFormRequest;
use App\Item;
class ItemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return response(Item::all());
    }
    /**
     * Store a newly created resource in storage.
     *
     * @param  \App\Http\Requests\ItemStoreFormRequest $request
     * @return \Illuminate\Http\Response
     */
    public function store(ItemStoreFormRequest $request)
    {
        $item = new Item();
        // todo: ログインユーザのidが入るようにする
        $item->user_id = \App\User::query()->first()->id;
        $item->content = $request->input('content');
        $item->save();
        return response($item, 201);
    }
    /**
     * Display the specified resource.
     *
     * @param  \App\Item $item
     * @return \Illuminate\Http\Response
     */
    public function show(Item $item)
    {
        return response($item);
    }
    /**
     * Update the specified resource in storage.
     *
     * @param  \App\Http\Requests\ItemUpdateFormRequest $request
     * @param  \App\Item                                $item
     * @return \Illuminate\Http\Response
     */
    public function update(ItemUpdateFormRequest $request, Item $item)
    {
        if ($request->input('content')) {
            $item->content = $request->input('content');
        }
        if ($request->input('checked')) {
            $item->checked = $request->input('checked');
        }
        $item->save();
        return response($item);
    }
    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Item $item
     * @return \Illuminate\Http\Response
     */
    public function destroy(Item $item)
    {
        $item->delete();
        return response('{}'); // 返すものがないので空のJSONを返す
    }
}

これで一通りの実装ができたので、テストがすべて通るはずです。


$ ./vendor/bin/phpunit tests/Feature/ItemTest.php
PHPUnit 5.7.15 by Sebastian Bergmann and contributors.
......                                                              6 / 6 (100%)
Time: 3.19 seconds, Memory: 8.00MB
OK (6 tests, 14 assertions)

ソースコードの全体はGitHubでも公開しているので、参考にしてください。

ここまでで、Itemの取得・作成・更新・削除が行えるWeb APIができました。
次回は、このAPIを使うVue.jsアプリケーションを作っていきます。

参考

Laravel Homestead
Database: Migrations
Database: Seeding
Eloquent: Relationships
Routing
Validation