第一章7 『Todoモデルに関わるコントローラーの作成』
2022/03/26 Laravel
todoの追加と同時にラベルとの紐付けを行うためにサービスクラスを追加したり編集したりした。期限をCarbonじゃなくstringで登録するようにもした。
app/Repositories/TodoRepository.phpのcreateとupdateを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * @param string $task * @param string $deadline * @return Todo */ public function create(string $task, string $deadline): Todo { $todo = new Todo(); $todo->task = $task; $todo->deadline = $deadline; $todo->save(); return $todo; } /** * @param int $id * @param string $task * @param string $deadline * @return Todo */ public function update(int $id, string $task, string $deadline): Todo { $todo = $this->findById($id); $todo->task = $task; $todo->deadline = $deadline; $todo->save(); return $todo; } |
app/Services/TodoLabel/LinkTodoAndLabelsService.phpというサービスクラスを作り、以下のように編集した。
もともとTodoに紐付いていたLabelを取り外し、新しく紐付けるようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php declare(strict_types=1); namespace App\Services\TodoLabel; use App\Models\Todo; class LinkTodoAndLabelsService { /** * @param Todo $todo * @param array $labelIds<int> * @return Todo */ public function __invoke(Todo $todo, array $labelIds): Todo { if (($todo->labels()->count()) !== 0) { $todo->labels()->detach(); } foreach ($labelIds as $labelId) { $todo->labels()->attach(($labelId)); } return $todo; } } |
app/Services/Todo/CreateTodoService.phpを以下のように編集する。Todoを作成したときにラベルと紐付け、作ったTodoとそれに紐付いているLabelを返すようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php declare(strict_types=1); namespace App\Services\Todo; use App\Models\Todo; use App\Repositories\TodoRepository; use App\Services\TodoLabel\LinkTodoAndLabelsService; class CreateTodoService { private TodoRepository $todoRepository; private LinkTodoAndLabelsService $linkTodoAndLabelsService; public function __construct(TodoRepository $todoRepository, LinkTodoAndLabelsService $linkTodoAndLabelsService) { $this->todoRepository = $todoRepository; $this->linkTodoAndLabelsService = $linkTodoAndLabelsService; } /** * @param string $task * @param string $deadline * @param array $labelIds<int> * @return Todo */ public function __invoke(string $task, string $deadline, array $labelIds): Todo { $todo = $this->todoRepository->create($task, $deadline); return $this->linkTodoAndLabelsService->__invoke($todo, $labelIds)->load('labels'); } } |
app/Services/Todo/UpdateTodoService.phpを以下のように編集した。ここでも更新した後に紐付いているLabelを一緒に返すようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php declare(strict_types=1); namespace App\Services\Todo; use App\Models\Todo; use App\Repositories\TodoRepository; use App\Services\TodoLabel\LinkTodoAndLabelsService; class UpdateTodoService { private TodoRepository $todoRepository; private LinkTodoAndLabelsService $linkTodoAndLabelsService; public function __construct(TodoRepository $todoRepository, LinkTodoAndLabelsService $linkTodoAndLabelsService) { $this->todoRepository = $todoRepository; $this->linkTodoAndLabelsService = $linkTodoAndLabelsService; } /** * @param int $id * @param string $task * @param string $deadline * @param array $labelIds<int> * @return Todo */ public function __invoke(int $id, string $task, string $deadline, array $labelIds): Todo { $todo = $this->todoRepository->update($id, $task, $deadline); return $this->linkTodoAndLabelsService->__invoke($todo, $labelIds)->load('labels'); } } |
全てのTodoを取得するときに一緒にLabelも取って来れるようにmodelにEagerロードを追加した。app/Models/Todo.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php declare(strict_types=1); namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * @property ?int id * @property ?string task * @property ?string deadline */ class Todo extends Model { use HasFactory; protected $with = ['labels']; /** * @return BelongsToMany */ public function labels(): BelongsToMany { return $this->belongsToMany(Label::class, 'todo_label', 'todo_id', 'label_id'); } } |
サービスクラスとリポジトリクラスの変更に伴ってテストも修正した。
tests/Unit/Repositories/TodoRepositoryTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<?php declare(strict_types=1); namespace Tests\Unit\Repositories; use App\Repositories\TodoRepository; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Date; use Tests\TestCase; class TodoRepositoryTest extends TestCase { use RefreshDatabase; public function testSuccessAllReturnedTenTodos() { $todoRepository = new TodoRepository(); for ($i = 0; $i < 10; $i++) { $todoRepository->create('test', Date::today()->toDateString()); } $todos = $todoRepository->all(); $this->assertCount(10, $todos); } public function testSuccessAllReturnedNull() { $todoRepository = new TodoRepository(); $todos = $todoRepository->all(); $this->assertSame(null, $todos); } public function testSuccessCreate() { $todoRepository = new TodoRepository(); $todo = $todoRepository->create('test', '2022-01-01'); $this->assertSame('test', $todo->task); $this->assertEquals('2022-01-01', $todo->deadline); } public function testSuccessUpdate() { $todoRepository = new TodoRepository(); $todo = $todoRepository->create('test', '2022-01-01'); $updatedTodo = $todoRepository->update($todo->id, 'updated', '2022-01-02'); $this->assertSame('updated', $updatedTodo->task); $this->assertEquals('2022-01-02', $updatedTodo->deadline); } public function testFailUpdateBecauseModelNotFound() { $this->expectException(ModelNotFoundException::class); $todoRepository = new TodoRepository(); $todoRepository->update(1, 'update', '2022-01-01'); } public function testSuccessDelete() { $todoRepository = new TodoRepository(); $todo = $todoRepository->create('test', '2022-01-01'); $this->assertDatabaseCount('todos', 1); $todoRepository->delete($todo->id); $this->assertDatabaseCount('todos', 0); } public function testFailDeleteBecauseModelNotFound() { $this->expectException(ModelNotFoundException::class); $todoRepository = new TodoRepository(); $todoRepository->delete(1); } public function testSuccessFindById() { $todoRepository = new TodoRepository(); $todo = $todoRepository->create('test', '2022-01-01'); $returnedTodo = $todoRepository->findById($todo->id); $this->assertSame($todo->id, $returnedTodo->id); } public function testFailFindByIdBecauseModelNotFound() { $this->expectException(ModelNotFoundException::class); $todoRepository = new TodoRepository(); $todoRepository->findById(1); } } |
tests/Unit/Services/Todo/CreateTodoServiceTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<?php declare(strict_types=1); namespace Tests\Unit\Services\Todo; use App\Models\Todo; use App\Repositories\TodoRepository; use App\Services\Todo\CreateTodoService; use Tests\TestCase; use App\Services\TodoLabel\LinkTodoAndLabelsService; class CreateTodoServiceTest extends TestCase { private $todoRepository; private $linkTodoAndLabelService; public function setUp(): void { parent::setUp(); $this->todoRepository = new class extends TodoRepository { public function create(string $task, string $deadline): Todo { $todo = new Todo(); $todo->task = $task; $todo->deadline = $deadline; return $todo; } }; $this->linkTodoAndLabelService = new class extends LinkTodoAndLabelsService { public function __invoke(Todo $todo, array $labelIds): Todo { return $todo; } }; } public function testSuccess() { $task = 'Test'; $deadline = '2022-01-01'; $labelIds = [1,2,3]; $todo = new Todo(); $todo->task = 'Test'; $todo->deadline = '2022-01-01'; $createTodoService = new CreateTodoService( $this->todoRepository, $this->linkTodoAndLabelService ); $createTodoService->__invoke($task, $deadline, $labelIds); $this->assertEquals($todo, $this->todoRepository->create($task, $deadline)); $this->assertEquals($todo, $this->linkTodoAndLabelService->__invoke($todo, $labelIds)); } } |
tests/Unit/Services/Todo/UpdateTodoServiceTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<?php declare(strict_types=1); namespace Tests\Unit\Services\Todo; use App\Models\Todo; use App\Repositories\TodoRepository; use App\Services\Todo\UpdateTodoService; use Tests\TestCase; use App\Services\TodoLabel\LinkTodoAndLabelsService; class UpdateTodoServiceTest extends TestCase { private $todoRepository; private $linkTodoAndLabelService; public function setUp(): void { parent::setUp(); $this->todoRepository = new class extends TodoRepository { public function update(int $id, string $task, string $deadline): Todo { $todo = new Todo(); $todo->task = $task; $todo->deadline = $deadline; return $todo; } }; $this->linkTodoAndLabelService = new class extends LinkTodoAndLabelsService { public function __invoke(Todo $todo, array $labelIds): Todo { return $todo; } }; } public function testSuccess() { $id = 1; $task = 'Test'; $deadline = '2022-01-01'; $labelIds = [1,2,3]; $todo = new Todo(); $todo->task = 'Test'; $todo->deadline = '2022-01-01'; $updateTodoService = new UpdateTodoService( $this->todoRepository, $this->linkTodoAndLabelService ); $updateTodoService->__invoke($id, $task, $deadline, $labelIds); $this->assertEquals($todo, $this->todoRepository->update($id, $task, $deadline)); $this->assertEquals($todo, $this->linkTodoAndLabelService->__invoke($todo, $labelIds)); } } |
サービスとリポジトリができたのでコントローラーを作っていく。
コントローラーを作る前にroutes/api.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php declare(strict_types=1); use App\Http\Controllers\Api\Todo\CreateTodoController; use App\Http\Controllers\Api\Todo\DeleteTodoController; use App\Http\Controllers\Api\Todo\GetTodosController; use App\Http\Controllers\Api\Todo\UpdateTodoController; use Illuminate\Support\Facades\Route; Route::get('todos', GetTodosController::class); Route::post('todo', CreateTodoController::class); Route::put('todo/{id}', UpdateTodoController::class); Route::delete('todo/{id}', DeleteTodoController::class); |
app/Http/Controllers/Api/Todo/CreateTodoController.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php declare(strict_types=1); namespace App\Http\Controllers\Api\Todo; use App\Http\Controllers\Controller; use App\Services\Todo\CreateTodoService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class CreateTodoController extends Controller { /** * @param Request $request * @param CreateTodoService $createTodoService * @return JsonResponse */ public function __invoke(Request $request, CreateTodoService $createTodoService): JsonResponse { return response()->json($createTodoService->__invoke($request->task, $request->deadline, $request->labelIds)); } } |
app/Http/Controllers/Api/Todo/DeleteTodoController.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php declare(strict_types=1); namespace App\Http\Controllers\Api\Todo; use App\Http\Controllers\Controller; use App\Services\Todo\DeleteTodoService; use Illuminate\Http\JsonResponse; class DeleteTodoController extends Controller { /** * @param int $id * @param DeleteTodoService $deleteTodoService * @return JsonResponse */ public function __invoke(int $id, DeleteTodoService $deleteTodoService): JsonResponse { return response()->json($deleteTodoService->__invoke($id)); } } |
app/Http/Controllers/Api/Todo/GetTodosController.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php declare(strict_types=1); namespace App\Http\Controllers\Api\Todo; use App\Http\Controllers\Controller; use App\Services\Todo\GetTodosService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class GetTodosController extends Controller { /** * @param Request $request * @param GetTodosService $getTodosService * @return JsonResponse */ public function __invoke(Request $request, GetTodosService $getTodosService): JsonResponse { return response()->json($getTodosService->__invoke()); } } |
app/Http/Controllers/Api/Todo/UpdateTodoController.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php declare(strict_types=1); namespace App\Http\Controllers\Api\Todo; use App\Http\Controllers\Controller; use App\Services\Todo\UpdateTodoService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class UpdateTodoController extends Controller { /** * @param int $id * @param Request $request * @param UpdateTodoService $updateTodoService * @return JsonResponse */ public function __invoke(int $id, Request $request, UpdateTodoService $updateTodoService): JsonResponse { return response()->json($updateTodoService->__invoke($id, $request->task, $request->deadline, $request->labelIds)); } } |
コントローラーのテストを作っていく。
1 2 3 4 |
todo-list % sail artisan make:test Todo/CreateTodoControllerTest todo-list % sail artisan make:test Todo/GetTodosControllerTest todo-list % sail artisan make:test Todo/UpdateTodoControllerTest todo-list % sail artisan make:test Todo/DeleteTodoControllerTest |
テストを書いていく。
tests/Feature/Todo/CreateTodoControllerTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?php declare(strict_types=1); namespace Tests\Feature\Todo; use App\Models\Label; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Testing\Fluent\AssertableJson; use Tests\TestCase; class CreateTodoControllerTest extends TestCase { use RefreshDatabase; public function testExec() { $label = new Label(); $label->title = 'Test'; $label->save(); $response = $this->post('api/todo', [ 'task' => 'test', 'deadline' => '2022-01-01', 'labelIds' => [$label->id] ] ); $response ->assertStatus(200) ->assertJson(fn (AssertableJson $json) => $json ->has('id') ->where('task', 'test') ->where('deadline', '2022-01-01') ->has('created_at') ->has('updated_at') ->has('labels') ); } } |
tests/Feature/Todo/GetTodosControllerTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php declare(strict_types=1); namespace Tests\Feature\Todo; use App\Models\Label; use App\Models\Todo; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Testing\Fluent\AssertableJson; use Tests\TestCase; class GetTodosControllerTest extends TestCase { use RefreshDatabase; public function testExec() { $label = new Label(); $label->title = 'Test'; $label->save(); for ($i = 0; $i < 3; $i++) { $todo = Todo::factory()->create(['deadline' => '2022-01-01']); $todo->labels()->attach($label->id); } $response = $this->get('api/todos'); $response ->assertStatus(200) ->assertJson(fn (AssertableJson $json) => $json->has((string)2) ->first(fn ($json) => $json ->has('id') ->has('task') ->has('deadline') ->has('created_at') ->has('updated_at') ->has('labels') ) ); } } |
tests/Feature/Todo/UpdateTodoControllerTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<?php declare(strict_types=1); namespace Tests\Feature\Todo; use App\Models\Label; use App\Models\Todo; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Testing\Fluent\AssertableJson; use Tests\TestCase; class UpdateTodoControllerTest extends TestCase { use RefreshDatabase; public function testExec() { $label = new Label(); $label->title = 'Test'; $label->save(); $todo = new Todo(); $todo->task = 'test'; $todo->deadline = '2022-01-01'; $todo->save(); $response = $this->put("api/todo/$todo->id", [ 'task' => 'update', 'deadline' => '2022-01-02', 'labelIds' => [$label->id] ]); $response ->assertStatus(200) ->assertJson(fn (AssertableJson $json) => $json ->where('id', $todo->id) ->where('task', 'update') ->where('deadline', '2022-01-02') ->has('created_at') ->has('updated_at') ->has('labels') ); } } |
tests/Feature/Todo/DeleteTodoControllerTest.phpを以下のように編集した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php declare(strict_types=1); namespace Tests\Feature\Todo; use App\Models\Label; use App\Models\Todo; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class DeleteTodoControllerTest extends TestCase { use RefreshDatabase; public function testExec() { $label = new Label(); $label->title = 'Test'; $label->save(); $todo = new Todo(); $todo->task = 'test'; $todo->deadline = '2022-01-01'; $todo->save(); $response = $this->delete("api/todo/$todo->id"); $response->assertStatus(200); $this->assertDatabaseMissing('todos', ['id' => $todo->id]); } } |
テストを実行する。
1 |
todo-list % sail artisan test |
これが表示されれば成功。