Laravel 5 のチュートリアルをやりながらついでにテストも書いてみる
はじめに
Laravel の勉強をするにあたり、 Laravel 5 のチュートリアル を見つけました。
せっかくなので現時点の LTS である 6 系でやってみました。
ついでに、 PHPUnit でのテストも書いてみました。
もし Laravel way っぽくないところがあればご指摘いただけると幸いです。
余談ですが、この記事を書く前に一度、現時点の最新バージョンである 8 系でこのチュートリアルをやってみました。
前半はあまり変更はなさそうでしたが、後半の認証系の部分で勝手が変わっておりました。
環境構築
こちらの記事を参考にさせていただきました。
【初心者向け】20分でLaravel開発環境を爆速構築するDockerハンズオン
この通りに実施すれば、環境構築できるかと思います。
ただ、インストールする Laravel のバージョンは 6 にしています。
$ composer create-project --prefer-dist "laravel/laravel=6.*" .
なお、今後 artisan コマンドを実行する場合はコンテナ内で実行します。
tinker
Laravel には rails コマンドのように、 artisan コマンドでいろいろできます。
対話型コンソールの tinker を使用してテストしてみます。
tinker
users テーブルからデータを取得するため、Eloquent を使用しています。
↑の環境構築が済んでいれば、 users テーブル、および User モデルが作られているはずです。
# php artisan tinker
Psy Shell v0.10.4 (PHP 7.4.10 — cli) by Justin Hileman
>>> $user = new App\User();
>>> $user->name = 'phper';
>>> $user->email = 'phper@example.com';
>>> $user->password = Hash::make('secret');
>>> $user->save();
>>> App\Models\User::all();
=> Illuminate\Database\Eloquent\Collection {#4125
all: [
App\Models\User {#4126
id: 1,
name: "phper",
email: "phper@example.com",
email_verified_at: null,
created_at: "2020-09-18 13:38:29",
updated_at: "2020-09-18 13:38:29",
},
],
}
>>> quit
Exit: Goodbye
テスト用 DB の作成と設定値の追加
後々のためにテスト用の DB を作成しておきます。
コンテナ名などはよしなに読み替えていただいて、こんな感じでデータベースを作成します。
$ docker exec -it laravel_practice_db_1 sh -c "mysql -u root -p -e \"CREATE DATABASE laravel_test\""
そして、テスト環境用の設定ファイル .env.testing
を作成します。
.env.sample
をコピーして作成すれば大丈夫ですが、下記だけ変更しておきます。
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel_test
DB_USERNAME=root
DB_PASSWORD=secret
ここで、アプリケーション用のコンテナに入り、テスト用の DB にマイグレーションを実施します。
artisan コマンドに対し、オプションで env
を指定することで、環境変数を設定できます。
$ php artisan migrate --env=testing
これで、 laravel_test の方にもテーブルが作成されたかと思います。
あと、こちらのコマンドも実行しておきます。
Laravel のアプリケーションで使用する暗号鍵ですが、これが空欄だと後々エラーになってしまいます。
No application encryption key has been specified.となったときの対応方法
env
オプションをつけることで、対応した環境変数ファイルに勝手に追記してくれるのでこちらを利用します。
APP_KEY
の欄が埋まっているかと思います。
$ php artisan key:generate --env=testing
基本的な TODO リストを作る
ここから、 Beginner Task List をやっていきます。
tasks テーブルの作成
TODO リストを保存するための tasks テーブルを作成します。
デフォルトでいくつかテーブルが作成されていますが、今回は使用しません。
Laravel では artisan コマンドでマイグレーションファイルを生成できます。
https://laravel.com/docs/6.x/migrations#creating-tables
$ php artisan make:migration create_tasks_table --create=tasks
実行すると database/migrations
以下に、マイグレーションファイルが生成されると思います。
プライマリーキーの id やタイムスタンプは追加されているので、今回必要な name カラムを追加します。
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
+ $table->string('name');
$table->timestamps();
});
}
修正後、マイグレーションを実行します。(テスト用のデータベースにも)
$ php artisan migrate
$ php artisan migrate --env=testing
実際にテーブルができたかどうか、 MySQL にログインして確認してみます。
このようなテーブルができていました。
mysql> desc tasks;
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
Eloquent モデルの作成
Laravel の ORM を使用した Task モデルを作成します。
こちらもコマンドで生成可能です。
php artisan make:model Task
実行すると、 app
以下に Task モデルが生成されています。
ただ、直近のバージョンだと、 app
以下に Models
というネームスペースがデフォルトで作成されるようです。
個人的にはそっちの方が良いかなぁと思っています。
ルーティングの定義
HTTP リクエストが来た時に、どの処理を実行するか定義します。
Laravel では routes
ディレクトリ以下に保存されています。
いくつかファイルがありますが、一般的な Web アプリケーションを作成する場合は web.php
で良いようです。
https://laravel.com/docs/6.x/routing#basic-routing
今回は、タスクの一覧、タスクの作成、タスクの削除を実行したいので、 web.php
をこんな感じで編集します。
<?php
use Illuminate\Http\Request;
/**
* Display All Tasks
*/
Route::get('/', function () {
return view('tasks');
});
/**
* Add A New Task
*/
Route::post('/task', function(Request $request) {
//
});
/**
* Delete An Existing Task
*/
Route::delete('/task/{id}', function($id) {
//
});
さて、 /tasks
にリクエストした場合はこのような定義になっています。
Route::get('/', function () {
return view('tasks');
});
これは、 tasks.blade.php
を表示する、と言う指示になります。
https://laravel.com/docs/6.x/views
まだそのファイルはないので作成していきます。
レイアウトとビューファイルの作成
Laravel では Blade Template を使用しています。
なお、ビューファイルは必要だと思ったところのみ記載しておりますが、元のチュートリアルでも、サンプルコードがあるので、興味がある方はそちらを利用しても良いかもしれません。
https://github.com/laravel/quickstart-basic/tree/master/resources/views
まずはベースとなるレイアウトを作成します。
resources/views/layouts/app.blade.php
を以下のように作成します。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Laravel Quickstart - Basic</title>
<!-- Fonts -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css" rel='stylesheet' type='text/css'>
<link href="https://fonts.googleapis.com/css?family=Lato:100,300,400,700" rel='stylesheet' type='text/css'>
<!-- Styles -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
font-family: 'Lato';
}
.fa-btn {
margin-right: 6px;
}
</style>
</head>
<body>
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<!-- Branding Image -->
<a class="navbar-brand" href="{{ url('/') }}">
Task List
</a>
</div>
</div>
</nav>
@yield('content')
<!-- JavaScripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</body>
</html>
上記コードで重要なのは @yeield('content')
です。
このファイルはベースの画面で、 yield
の部分が次の子ビューファイルで挿し変わります。
子ビューファイルを resources/views/tasks.blade.php
に作ります。
@extends('layouts.app')
@section('content')
<!-- Bootstrap Boilerplate... -->
<div class="panel-body">
<!-- Display Validation Errors -->
@include('common.errors')
<!-- New Task Form -->
<form action="/task" method="POST" class="form-horizontal">
{{ csrf_field() }}
<!-- Task Name -->
<div class="form-group">
<label for="task" class="col-sm-3 control-label">Task</label>
<div class="col-sm-6">
<input type="text" name="name" id="task-name" class="form-control">
</div>
</div>
<!-- Add Task Button -->
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6">
<button type="submit" class="btn btn-default">
<i class="fa fa-plus"></i> Add Task
</button>
</div>
</div>
</form>
</div>
<!-- TODO: Current Tasks -->
@endsection
@extends
でレイアウトを宣言し、 @section('content') ~ @endsection
までがレイアウトに尺変わります。
ついでに今後使用する、エラーメッセージを表示するバーツを作っておきます。
これは、 resources/views/common/errors.blade.php
に下記を追加します。
@if (count($errors) > 0)
<!-- Form Error List -->
<div class="alert alert-danger">
<strong>Whoops! Something went wrong!</strong>
<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
このパーツは、 resources/views/tasks.blade.php
で @include('common.errors')
で呼ばれています。
http://localhost:10080
にアクセスすると、こんな感じの表示になっているかと思います。
タスクの追加
バリデーション
タスク名が 255 文字を超える場合はエラーを表示するようにします。
routes/web.php
の POST /task の部分に以下を追記します。
/**
* Add A New Task
*/
Route::post('/task', function(Request $request) {
+ $validator = Validator::make($request->all(), [
+ 'name' => 'required|max:255',
+ ]);
+
+ if ($validator->fails()) {
+ return redirect('/')
+ ->withInput()
+ ->withErrors($validator);
+ }
//
});
ここまでできた段階で、 http://localhost:10080/tasks
にアクセスして、タスク名を 255 文字を超える値で入力してみます。
すると、下記のような表示が出るようになるかと思います。
バリデーションにひっかかると $errors
に値がセットされてビューに渡されます。
タスクの作成
タスクを作成できるようにします。
これも routes/web.php
に記載します。
他の Web フレームワークを経験していると、コントローラーかハンドラを用意するべきでは、と思いますが、後々用意しますので一旦はルーティングに記載します。
+ use App\Task;
Route::post('/task', function(Request $request) {
// ...
if ($validator->fails()) {
return redirect('/')
->withInput()
->withErrors($validator);
}
+ $task = new Task;
+ $task->name = $request->name;
+ $task->save();
+
+ return redirect('/');
});
これで、タスクを作成できるようになりますが、まだタスク一覧がないので実際にタスクができているかよく分かりません。
今の段階では実際に MySQL を見るか、 artisan tinker
で確認します。
テストの作成
ここらでタスクの保存に関してテストを作ろうと思います。
まずはそもそもテストに必要な物がインストールされているかどうかですが、 Laravel では PHPUnit がデフォルトでインストールされます。
サンプルのテストコードがすでに tests
ディレクトリ内にあるので、 phpunit コマンドを実行することでテストできます。
# vendor/bin/phpunit
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 1.83 seconds, Memory: 16.00 MB
OK (2 tests, 2 assertions)
ちなみに、Laravel バージョン 7 以降は artisan にテストランナーが追加されるようです。
https://laravel.com/docs/7.x/testing#introduction
PHPUnit がより使いやすくなるかと思うので、楽しみです。
PHPUnit の設定ファイルを修正
PHPUnit は専用の設定ファイル、 phpunit.xml
があります。
このファイルに記載されている設定値を優先して読み取ってしまいます。
.env.testing
の内容で管理したいので、 <php>
タグの中は APP_ENV のみを残して削除します。
<php>
<server name="APP_ENV" value="testing"/>
</php>
タスク作成のテスト
tests
ディレクトリ以下には、 Feature
と Unit
があります。
Unit
の方は限られたメソッドなどのテスト、 Feature
は機能などの一連のテストの住み分けのようです。
今回はページをリクエストしようと思うので、 Feature
の方に、 RouteTaskTest.php
を作成します。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Task;
class RouteTaskTest extends TestCase
{
use RefreshDatabase;
/**
* POST /task
* test the success of task creation.
* 事前にタスクの数を数えておき、作成後に 1 つ数が増えていること。
*
* @return void
*/
public function testSuccessOfPostTask()
{
$before_tasks_count = Task::all()->count();
$response = $this->post('/task', ['name' => 'new task']);
$response->assertStatus(302);
$this->assertEquals($before_tasks_count + 1, Task::all()->count());
}
/**
* POST /task
* test the failure of task creation.
* 文字数をオーバーしている場合にリダイレクトされること
*
* @return void
*/
public function testFailureOfPostTask()
{
$response = $this->post('/task', ['name' => str_repeat('a', 256)]);
$response->assertStatus(302);
}
}
このあたりのコードはほぼ PHPUnit のシンタックスです。
ただ、 use RefreshDatabase;
を追記することで、テスト終了時にデータベースをドロップして、再度セットアップしてくれます。
毎回データベースを作り直してしまうので、使い方によっては注意が必要です。
https://laravel.com/docs/6.x/database-testing#resetting-the-database-after-each-test
引数にテストファイルを指定すれば、そのテストファイルだけ実行できます。
# vendor/bin/phpunit tests/Feature/RouteTaskTest.php
存在するタスクの表示
タスクを表示するために、一覧ページにて現在保存されているタスクを全て取得します。
Task::orderBy('created_at', 'asc')->get()
で、 SELECT * FROM tasks ORDER BY created_at ASC
のようなクエリが発行されます。
view
の第二引数で、ビューに共有する変数を定義できます。
Route::get('/', function () {
$tasks = Task::orderBy('created_at', 'asc')->get();
return view('tasks', [
'tasks' => $tasks
]);
});
ビューの方は、 resources/views/tasks.blade.php
の TODO の部分を以下のように置き換えます。
<!-- Current Tasks -->
@if (count($tasks) > 0)
<div class="panel panel-default">
<div class="panel-heading">
Current Tasks
</div>
<div class="panel-body">
<table class="table table-striped task-table">
<!-- Table Headings -->
<thead>
<th>Task</th>
<th> </th>
</thead>
<!-- Table Body -->
<tbody>
@foreach ($tasks as $task)
<tr>
<!-- Task Name -->
<td class="table-text">
<div>{{ $task->name }}</div>
</td>
<td>
<!-- TODO: Delete Button -->
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@endsection
こんな感じになります。
タスク表示のテスト
Factory を作成する
既存のタスクを表示するために、あらかじめデータを作成する必要があります。
この時に、 Factory を作っておくと楽なので、作っておきます。
対象のモデルのテストデータを作り安くしてくれるものです。
https://laravel.com/docs/6.x/database-testing#generating-factories
実はデフォルトで User モデルの Factory は作成されていますが、今回は Task の Factory を作成します。
artisan コマンドで雛形を作れます。
$ php artisan make:factory TaskFactory --model=Task
これで、 database/factories
以下にファイルが作成されます。
このファイルを修正することで、例えばテストデータのデフォルト値を定義したりできますが、とりあえずそのまま使います。
Factory を使ってテストデータを作る
こんな感じで書きました。
/**
* GET /tasks
*
* @return void
*/
public function testGetIndex()
{
$task = factory(Task::class)->create(['name' => 'new_task']);
$response = $this->get('/');
$response->assertStatus(200);
$this->assertRegExp('/'.$task->name.'/', $response->getContent());
}
$task = factory(Task::class)->create(['name' => 'new_task']);
これでテストデータを作成します。
create
メソッドはデータベースに作成までしますが、似たようなメソッドで make
があります。
これはモデルのインスタンスを作成するところまでです。
今回はデータベース内に存在して欲しいので create
を使いました。
$this->assertRegExp('/'.$task->name.'/', $response->getContent());
PHPUnit の正規表現マッチャーである assertRegExp を使います。
$response->getContent()
で応答される HTML が取得できるので、その中に新しいタスクの名前が含まれているかを確認しています。
おそらくこれでテストは通るかと思います。
試しに、 assertRegExp の第一引数を全然関係ない文字列にすれば、テストは失敗すると思います。
タスクの削除
先にテストを書く
まずは Factory をちょっと修正します。
TaskFactory はデフォルトでそのまま使っていましたが、 name
にデフォルト値を使うようにします。
Factory では Faker というライブラリを使うことができます。
適当なテストデータを作る時に名前を良い感じなやつにしてくれます。
Faker で作れるデータはドキュメントの方を参考にしてください。
これを使って、 name にデフォルト値を設定します。
$factory->define(Task::class, function (Faker $faker) {
return [
'name' => $faker->name
];
});
テストを用意します。
タスク削除は、リクエストした URI 末尾の id のものを削除します。
逆を言うと、末尾の id が存在しない id を指定されたらエラーを返すべきかと思います。
そんな感じで以下の 2 つを用意しました。
/**
* DELETE /task
* test the success of task creation.
*
* @return void
*/
public function testSuccessOfDeleteTask()
{
$task = factory(Task::class)->create();
$response = $this->delete('/task/'.$task->id);
$response->assertStatus(302);
$this->assertEmpty(Task::find($task->id));
}
/**
* DELETE /task
* test disable deleting other user's task
*
* @return void
*/
public function testDisableOfDeletingOtherUserTask()
{
$task = factory(Task::class)->create();
$request_id = $task->id + 1;
$response = $this->delete('/task/'.$request_id);
$response->assertStatus(404);
}
現状、テストは失敗するかと思います。
タスクを削除する
まずは削除ボタンを作成します。
TODO: Delete Button
と書いていた部分を以下のように置き換えます。
<!-- Delete Button -->
<td>
<form action="/task/{{ $task->id }}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button>Delete Task</button>
</form>
</td>
DELETE リクエストを発行するために、 method_field('DELETE')
を追記すると、以下の HTML が挿入されます。
<input type="hidden" name="_method" value="DELETE">
また、csrf_field()
についてはタスク作成の際に触れませんでしたが、 CSRF ミドルウェアにてトークンを発行し、それを使ったタグが追加されるようになります。
curl や HTTP クライアントツールを使って直接 DELETE や POST のリクエストを発行すると、 419 が返ります。
ただ、テスト時には無効になっているので、PHPUnit では直接リクエストしても成功していました。
こんな感じのボタンができます。
以下の 2 つは blade 記法で以下のように書き換えも可能です。
{{-- csrf_field() --}}
@csrf
{{-- method_field('DELETE') --}}
@method('DELETE')
削除処理を追加します。
Route::delete('/task/{id}', function($id) {
Task::findOrFail($id)->delete();
return redirect('/');
});
findOrFail で、id のタスクが見つかれば、それを削除し、そうでなければ 404 エラーを返します。
これでテストが通るようになり、タスクの削除が可能になりました。
存在しないタスクを指定して遷移すると、以下のように 404 エラーになります。
ユーザー認証を追加する
ここから https://laravel.com/docs/5.1/quickstart-intermediate の内容をやりますが、共通の部分もあるので、意訳をしております。
tasks テーブルにユーザー ID を追加する
ログインユーザーにタスクを作ってもらえるようにするために、 tasks テーブルに user_id を追加します。
カラム追加のマイグレーションを実行するために、以下のコマンドでファイルを生成します。
$ php artisan make:migration AddUserIdToTasksTable --table=tasks
カラムに関するドキュメントはこちらにあります。
https://laravel.com/docs/6.x/migrations#creating-columns
これをもとに以下のようにマイグレーションファイルを修正します。
class AddUserIdToTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->integer('user_id')->default(0)->after('id')->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
}
up
ではマイグレーション実行時に実行することです。
今回は NOT NULL DEFAULT 0 な user_id カラムを作成します。
なお、インデックスも作成しておきます。
down
はマイグレーションをロールバックした場合の定義なので、カラムを削除します。
これを実行します。
$ php artisan migrate
$ php artisan migrate --env=testing
MySQL を見ると、こんな感じのスキーマになってます。
mysql> desc tasks;
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| user_id | int | NO | MUL | 0 | |
| name | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)
モデルのリレーションを設定する
User モデルと Task モデルのリレーションを設定することで、データの参照や作成が容易になります。
https://laravel.com/docs/6.x/eloquent-relationships
User モデル
こんな感じで Task に対するリレーションを設定します。
user : task の関係は 1 対多なので、 hasMany を使用します。
class User extends Authenticatable
{
// ...
/**
* Get all of the tasks for the user.
*/
public function tasks()
{
return $this->hasMany(Task::class);
}
}
Task モデル
まずは tasks モデルに fillable
フィールドを追加します。
これはモデルから書き換え可能なプロパティを定義します。
(これとほぼ逆なプロパティとして guard
があります。)
今回の場合、 name
は書き換えしても良いですが、 user_id
はあまり書き換えられて欲しくないところです。
class Task extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name'];
}
次に、 User モデルに紐づいていることを宣言します。
今度は belongsTo
を使用します。
class Task extends Model
{
// ...
/**
* Get the user that owns the task.
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
確認する
まだ画面からは確認できないので、 tinker
でデータを作ってみます。
ここでも Factory を使うことで簡単にデータを作れます。
$ php artisan tinker
# 新しい User を作成する
>>> $user = factory(App\User::class)->create();
# そのユーザーに紐づいたタスクを作成する
>>> $user->tasks()->create(['name' => 'relation task']);
=> App\Task {#4142
name: "relation task",
user_id: 1,
updated_at: "2020-10-03 06:18:38",
created_at: "2020-10-03 06:18:38",
id: 3,
}
# そのユーザーに紐づいたタスクを取得する
>>> $user->tasks()->get();
=> Illuminate\Database\Eloquent\Collection {#3190
all: [
App\Task {#3513
id: 3,
user_id: 1,
name: "relation task",
created_at: "2020-10-03 06:18:38",
updated_at: "2020-10-03 06:18:38",
},
],
}
>>> quit
ユーザー登録とログインを追加する
Laravel ではデフォルトでユーザー登録やログインのための仕組みが用意されているので、簡単に実装できます。
それらは app/HTTP/Controllers/Auth
ディレクトリの下にあります。
ただ、これらのコントローラーは、用意されている AuthenticatesUsers
や RegistersUsers
を使用しているだけです。
もし、自分の思うようにカスタマイズしたいなら、上書きする必要する必要があります。
https://laravel.com/docs/6.x/authentication
まずはルーティングを追加する必要があります。
(ちなみにどのアクションを使えば良いかってドキュメントの中にないんですかね?見当たらなかった。。)
+ // Authentication Routes...
+ Route::get('/login', 'Auth\LoginController@showLoginForm');
+ Route::post('/login', 'Auth\LoginController@login');
+ Route::get('/logout', 'Auth\LoginController@logout');
+ // Registration Routes...
+ Route::get('/register', 'Auth\RegisterController@showRegistrationForm');
+ Route::post('/register', 'Auth\RegisterController@register');
また、合わせて resources/views/auth
以下に login.blade.php
と register.blade.php
を作る必要があります。
これは、サンプルのコードを利用させていただくと良いかと思います。
また、 resources/views/layouts/app.blade.php
にも、ログインやユーザー登録のヘッダーを追加しておくと良いと思います。
最後に、1 箇所変更しておきます。
app/Providers/RouteServiceProvider.php
の中の HOME
の値を変えます。
ログインやログアウト後のリダイレクト先です。
public const HOME = '/';
こんな感じになるかと思います。
TaskController を作成する
今までルーティングに書いていた処理をコントローラに移します。
これも artisan コマンをを使って雛形を作れます。
# php artisan make:controller TaskController
TaskController に web.php の内容を書き写します。
こんな感じです。
<?php
namespace App\Http\Controllers;
use App\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
/**
* Display a list of all of the user's task.
*
* @param Request $request
* @return Response
*/
public function index(Request $request)
{
$tasks = Task::orderBy('created_at', 'asc')->get();
return view('tasks', [
'tasks' => $tasks
]);
}
/**
* Create a new task.
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|max:255',
]);
$task = new Task;
$task->name = $request->name;
$task->save();
return redirect('/');
}
/**
* Destroy the given task.
*
* @param Request $request
* @param string $taskId
* @return Response
*/
public function destroy(Request $request, $taskId)
{
Task::findOrFail($taskId)->delete();
return redirect('/');
}
}
注意するべきは store
に移した、追加時のバリデーションです。
ルーティングページでは Validator クラスを明示していましたが、コントローラーは自身がバリデーションを持っているので、だいぶ簡潔にかけます。
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|max:255',
]);
$task = new Task;
$task->name = $request->name;
$task->save();
return redirect('/');
}
web.php も書き換えます。
以下の 3 つのルートはまとめて書き換えます。
use App\Http\Controllers\TaskController;
// ...
Route::get('/', 'TaskController@index');
Route::post('/task', 'TaskController@store');
Route::delete('/task/{taskId}', 'TaskController@destroy');
ここまで書き換えてテストを実行すれば、全てグリーンのままかと思います。
Task は要ログイン状態にする
Controller のコンストラクタでこちらの宣言をすることで、認証状態が必要な状態にできます。
class TaskController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
// ...
}
また、 routes/web.php で、 /login のルートに名前をつけます。
Route::get('/login', 'Auth\LoginController@showLoginForm')->name('login');
これは、非ログイン状態でログイン必要なページにアクセスした場合に、ログインページがここだということをアピールするためです。
この設定をすると、ログアウト状態でタスク一覧ページ( http://localhost:10080 )にアクセスすると、ログインページに戻されます。
ただ、悲しいことにテストはほぼ全て失敗するようになってしまいます。
テストはログインした状態にする
テストは特にログインしているわけではないので、軒並み弾かれてしまいます。
テストを書く時に、ログイン状態にするために色々メソッドを用意したりするんですが、 Laravel ではすでに用意してくれているようです。
https://laravel.com/docs/6.x/http-tests#session-and-authentication
actingAs
に User オブジェクトを入れることで、そのユーザーでログインしている状態になります。
まずは全てのテストの開始前に User オブジェクトを作成しておきます。
setUp メソッドはテスト実行前に処理してくれるものを記載します。
<?php
// ...
+use App\User;
class RouteTaskTest extends TestCase
{
use RefreshDatabase;
+ private ?User $user = null;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->user = factory(User::class)->create();
+ }
+
// ...
}
まずは説明しやすいタスク作成から修正します。
POST リクエストする処理の行に actingAs
を差し込みます。
これにより、ログイン状態にします。
public function testSuccessOfPostTask()
{
// ...
+ $response = $this->actingAs($this->user)->post('/task', ['name' => 'new task']);
// ...
}
public function testFailureOfPostTask()
{
+ $response = $this->actingAs($this->user)->post('/task', ['name' => str_repeat('a', 256)]);
// ...
}
これでタスク作成のテストは通るようになったと思います。
他のテストもとりあえずこのように作っておけば良いのですが、ログインしているユーザーが作ったタスクを予め作っておきたいです。
この辺を参考 に、ログインしているユーザーのリレーションを使って作成するとこんな感じになるかと思います。
Eloquent のリレーションの引数として、 Factory のパラメータを渡して作成しています。
public function testGetIndex()
{
+ $task = $this->user->tasks()->save(factory(Task::class)->make(['name' => 'new_task']));
+ $response = $this->actingAs($this->user)->get('/');
$response->assertStatus(200);
$this->assertRegExp('/'.$task->name.'/', $response->getContent());
}
同様に、削除のテストも変えてみます。
public function testSuccessOfDeleteTask()
{
+ $task = $this->user->tasks()->save(factory(Task::class)->make());
+ $response = $this->actingAs($this->user)->delete('/task/'.$task->id);
$response->assertStatus(302);
$this->assertEmpty(Task::find($task->id));
}
public function testDisableOfDeletingOtherUserTask()
{
+ $task = $this->user->tasks()->save(factory(Task::class)->make());
$request_id = $task->id + 1;
+ $response = $this->actingAs($this->user)->delete('/task/'.$request_id);
$response->assertStatus(404);
}
これでテストが通るかと思いますが、デフォルトで存在していた ExampleTest
がまだ失敗するかと思います(残っていれば)。
これは一覧のテストとかぶるところがあるので削除して良いかと思います。
ログインしているユーザーのタスクを作成する
新しく作成されるタスクのユーザー ID が、ログインしているユーザーの ID と一致していれば良いかと思うので、まずはテストに追加します。
public function testSuccessOfPostTask()
{
// ...
$this->assertEquals($before_tasks_count + 1, Task::all()->count());
// id が最新のタスクを取得して、ログインユーザーの ID と比較する
+ $newTask = Task::orderBy('id', 'desc')->first();
+ $this->assertEquals($this->user->id, $newTask->user_id);
}
もちろん失敗します。
TaskController の store を修正します。
先ほど使ったように、 User モデルからリレーションで作成すると良いです。
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|max:255',
]);
$request->user()->tasks()->create([
'name' => $request->name,
]);
return redirect('/');
}
これでテストも通ります。
ログインしているユーザーのタスクを表示する
今は一覧にて orderBy しているだけですが、 where で条件を指定することでできます。
public function index(Request $request)
{
$tasks = Task::where('user_id', $request->user()->id)->get();
return view('tasks', [
'tasks' => $tasks
]);
}
依存性の注入(DI)
Laravel では、コントローラーに対する依存性の注入方法も用意しているようです。
今回は Task モデルのためのデータ取得方法を定義した TaskRepository を作成し、それをコントローラに注入します。
app/Repositories
ディレクトリを作成し、そこに TaskRepository.php
を下記のように作ります。
app
以下のディレクトリは自動で読み込みされます。
<?php
namespace App\Repositories;
use App\User;
use App\Task;
class TaskRepository
{
/**
* Get all of the tasks for a given user.
*
* @param User $user
* @return Collection
*/
public function forUser(User $user)
{
return Task::where('user_id', $user->id)
->orderBy('created_at', 'asc')
->get();
}
}
User モデルを引数にとり、その User に紐づいたタスクを取得する感じですね。
この Repository をコントローラーに注入しますが、
Laravel はコンテナを使用して全てのコントローラーを解決するため、依存関係はコントローラーインスタンスに自動的に挿入される
ようです。
乱暴にいうと、コンストラクタの引数で型指定しておけば、その引数で渡されるそうです。
use App\Repositories\TaskRepository;
class TaskController extends Controller
{
/**
* The task repository instance.
*
* @var TaskRepository
*/
protected $tasks;
public function __construct(TaskRepository $tasks)
{
$this->middleware('auth');
$this->tasks = $tasks;
}
// ...
}
プロパティにセットしたので、一覧取得時に Repository を使用するようにします。
public function index(Request $request)
{
return view('tasks', [
'tasks' => $this->tasks->forUser($request->user()),
]);
}
こちらでも結果は変わらないはずです。
ログインしているユーザーのタスクを削除する
ルーティングにモデルを紐付ける
タスクの削除を修正していきますが、その前に、現在のタスク削除のルーティングはこのような感じです。
Route::delete('/task/{taskId}', 'TaskController@destroy');
パスパラメータにて、削除対象の ID が渡される想定です。
この後、この ID を使って対象のタスクを検索するわけですが、その辺りを事前にやってくれる仕組みが Laravel にはあります。
つまり、コントローラに渡されるタイミングでは、Task の特定まで済んでいるということです。
https://laravel.com/docs/6.x/routing#route-model-binding
まずは、 app/Providers/RouteServiceProvider.php
の boot メソッドを以下のように書き換えます。
public function boot()
{
parent::boot();
Route::model('task', 'App\Task');
}
これで、ルーティングの task という文字列は Task モデルと対応されることが宣言されました。
次にルーティングを task に変更します。
Route::delete('/task/{task}', 'TaskController@destroy');
最後に TaskController の destoroy を以下のように変更します。
引数が id ではなく、 Task モデルが直接与えられるようになりました。
/**
* Destroy the given task.
*
* @param Request $request
* @param Task $task
* @return Response
*/
public function destroy(Request $request, Task $task)
{
$task->delete();
return redirect('/');
}
これでもテストはまだ通ります。
自分のタスクしか削除させない(承認)
URL にタスク ID を闇雲に渡して DELETE リクエストを投げれば、他人のリクエストでも消すことができてしまいます。
そのため、 Laravel の承認機能を使って、ユーザーが自身のタスクしか消せないようにします。
まずはポリシーを作ります。
$ php artisan make:policy TaskPolicy
次にこのポリシーに destory メソッドを追加します。
このメソッドは User インスタンスと Task インスタンスを受け取ります。
このメソッドは Task の user_id をチェックして、一致していれば true を返します。
<?php
namespace App\Policies;
use App\User;
use App\Task;
use Illuminate\Auth\Access\HandlesAuthorization;
class TaskPolicy
{
use HandlesAuthorization;
/**
* Determine if the given user can delete the given task.
*
* @param User $user
* @param Task $task
* @return bool
*/
public function destroy(User $user, Task $task)
{
return $user->id === $task->user_id;
}
}
最後に、タスクモデルをタスクポリシーに関連付ける必要があります。
app/Providers/AuthServiceProvider.php
の $policies
に追記します。
protected $policies = [
Task::class => TaskPolicy::class,
];
最後に、 TaskController の destoroy メソッドに追記します。
public function destroy(Request $request, Task $task)
{
$this->authorize('destroy', $task);
// Delete The Task...
}
この authorize
メソッドは、 AuthorizesRequest
trait によって全てのコントローラーで使用できます。
第一引数は、適用するポリシーのメソッド名、第二引数は関連するモデルのインスタンスです。
今回は Task モデルのインスタンスが渡されるので、 TaskPolicy と一致し、その destroy メソッドが適用されます。
もし、承認されていないリクエストが来た場合は 403 が応答されます。
さいごに
今回の最終的なコードはこちらに置いてあります。
https://github.com/naoki85/laravel-practice
参考にしていただければ嬉しいです。
Discussion