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);