第一章5 『サービスクラスとリポジトリクラスを生成するコマンドの作成とTodoモデルに関わるサービスとリポジトリの実装』
2022/03/11 Laravel
コントローラー周りの事は1記事で終わると思ったけど思ったより長くなってしまったので何回かに分割する。
app/Http/Controllersの下にController.phpとLabelController.phpとTodoController.phpがあるけど(LabelController.phpとTodoController.phpはモデルと一緒に作った)シングルアクションコントローラーにしたいのでLabelController.phpとTodoController.phpは消した。
Single Action Controllers(シングルアクションコントローラ)
app/Http/Controllers/Api/Todoディレクトリの中にシングルアクションコントローラーを作っていく。
Todoの登録はCreateTodo、一覧を取得するのはGetTodos、更新はUpdateTodo、削除はDeleteTodoという名前にした。
1 2 3 4 |
todo-list % sail artisan make:controller Api/Todo/CreateTodoController --invokable todo-list % sail artisan make:controller Api/Todo/GetTodosController --invokable todo-list % sail artisan make:controller Api/Todo/UpdateTodoController --invokable todo-list % sail artisan make:controller Api/Todo/DeleteTodoController --invokable |
Labelも同様に作っていく。
1 2 3 4 |
todo-list % sail artisan make:controller Api/Label/CreateLabelController --invokable todo-list % sail artisan make:controller Api/Label/GetLabelsController --invokable todo-list % sail artisan make:controller Api/Label/UpdateLabelController --invokable todo-list % sail artisan make:controller Api/Label/DeleteLabelsController --invokable |
リポジトリクラスとサービスクラスも作っていく。
vendor/laravel/framework/src/Illuminate/Routing/Console/ControllerMakeCommand.phpにmake:controllerのコマンドの設定が書いてあったのでこれを見ながらRepositoryクラスとServiceクラスを作るコマンドを作ってみる。
まずはコマンド作成。Generating Commands(コマンド生成)
1 2 |
todo-list % sail artisan make:command ServiceMakeCommand todo-list % sail artisan make:command RepositoryMakeCommand |
コマンドが実行された時に作られるファイルのテンプレートを置いておくディレクトリを作成
1 |
todo-list % mkdir app/Console/Commands/stubs |
テンプレート用のテキストファイルを作成
1 2 |
todo-list % touch app/Console/Commands/stubs/service.invokable.stub todo-list % touch app/Console/Commands/stubs/repository.stub |
いろいろ書いていく。
app/Console/Commands/stubs/repository.stubを以下のように編集。
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 namespace {{ namespace }}; use App\Models\{{ modelClass }}; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; class {{ class }} { public function all(): Collection { return {{ modelClass }}::all(); } public function create(): {{ modelClass }} { ${{ modelClassVariable }} = new {{ modelClass }}(); //add items ${{ modelClassVariable }}->save(); return ${{ modelClassVariable }}; } public function update(int $id): {{ modelClass }} { ${{ modelClassVariable }} = $this->findById($id); //add items ${{ modelClassVariable }}->save(); return ${{ modelClassVariable }}; } public function delete(int $id): bool { ${{ modelClassVariable }} = $this->findById($id); return ${{ modelClassVariable }}->delete(); } public function findById(int $id): ?{{ modelClass }} { ${{ modelClassVariable }} = {{ modelClass }}::find($id); if (${{ modelClassVariable }} === null) { throw new ModelNotFoundException(); } return ${{ modelClassVariable }}; } } |
コマンドを叩くときに作るリポジトリクラスの名前と使うモデルクラスの名前を受け取るようにした。
app/Console/Commands/RepositoryMakeCommand.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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
<?php declare(strict_types=1); namespace App\Console\Commands; use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Input\InputArgument; class RepositoryMakeCommand extends GeneratorCommand { /** * The console command name. * * @var string */ protected $name = 'make:repository'; /** * The name of the console command. * * This name is used to identify the command during lazy loading. * * @var string|null */ protected static $defaultName = 'make:repository'; /** * The console command description. * * @var string */ protected $description = 'Create a new repository class'; /** * Get the stub file for the generator. * * @return string */ protected function getStub() { $stub = '/stubs/repository.stub'; return $this->resolveStubPath($stub); } /** * Resolve the fully-qualified path to the stub. * * @param string $stub * @return string */ protected function resolveStubPath($stub) { return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) ? $customPath : __DIR__.$stub; } /** * Get the default namespace for the class. * * @param string $rootNamespace * @return string */ protected function getDefaultNamespace($rootNamespace) { return $rootNamespace.'\Repositories'; } protected function getArguments() { return [ ['name', InputArgument::REQUIRED, 'The name of the repository class'], ['modelClassName', InputArgument::REQUIRED, 'The name of the model class this repository class will call'], ]; } protected function buildClass($name) { $stub = $this->files->get($this->getStub()); $modelClassName = $this->getModelClassNameInput(); return $this->replaceNamespace($stub, $name) ->replaceModel($stub, $modelClassName, lcfirst($modelClassName)) ->replaceClass($stub, $name); } protected function getModelClassNameInput() { return trim($this->argument('modelClassName')); } protected function replaceModel(&$stub, $modelClassName, $modelClassVariable) { $search = ['{{ modelClass }}', '{{ modelClassVariable }}']; $stub = str_replace( $search, [$modelClassName, $modelClassVariable], $stub ); return $this; } } |
app/Console/Commands/stubs/service.invokable.stubを以下のように編集。
サービスクラスはコントローラーと同じくinvokeメソッドにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php namespace {{ namespace }}; use {{ rootNamespace }}Repositories\{{ repositoryClassName }}; class {{ class }} { private {{ repositoryClassName }} ${{ repositoryClassVariable }}; public function __construct({{ repositoryClassName }} ${{ repositoryClassVariable }}) { $this->{{ repositoryClassVariable }} = ${{ repositoryClassVariable }}; } public function __invoke() { return $this->{{ repositoryClassVariable }}->{{ repositoryClassMethod }}(); } } |
コマンドを叩くときに作るサービスクラスの名前と呼び出すリポジトリクラスの名前とその関数を受け取るようにした。
app/Console/Commands/ServiceMakeCommand.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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
<?php declare(strict_types=1); namespace App\Console\Commands; use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; class ServiceMakeCommand extends GeneratorCommand { /** * The console command name. * * @var string */ protected $name = 'make:service'; /** * The name of the console command. * * This name is used to identify the command during lazy loading. * * @var string|null */ protected static $defaultName = 'make:service'; /** * The console command description. * * @var string */ protected $description = 'Create a new service class'; /** * Get the stub file for the generator. * * @return string */ protected function getStub() { $stub = null; if ($this->option('invokable')) { $stub = '/stubs/service.invokable.stub'; } return $this->resolveStubPath($stub); } /** * Resolve the fully-qualified path to the stub. * * @param string $stub * @return string */ protected function resolveStubPath($stub) { return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) ? $customPath : __DIR__.$stub; } /** * Get the default namespace for the class. * * @param string $rootNamespace * @return string */ protected function getDefaultNamespace($rootNamespace) { return $rootNamespace.'\Services'; } protected function getOptions() { return [ ['invokable', 'i', InputOption::VALUE_NONE, 'Generate a single method, invokable service class.'], ]; } protected function getArguments() { return [ ['name', InputArgument::REQUIRED, 'The name of the service class'], ['repositoryClassName', InputArgument::REQUIRED, 'The name of the repository class this service class will call'], ['repositoryClassMethod', InputArgument::REQUIRED, 'The name of the method this service class will call'], ]; } protected function buildClass($name) { $stub = $this->files->get($this->getStub()); $repositoryClassName = $this->getRepositoryClassNameInput(); return $this->replaceNamespace($stub, $name) ->replaceRepository($stub, $repositoryClassName, lcfirst($repositoryClassName), $this->getRepositoryClassMethodNameInput()) ->replaceClass($stub, $name); } protected function getRepositoryClassNameInput() { return trim($this->argument('repositoryClassName')); } protected function getRepositoryClassMethodNameInput() { return trim($this->argument('repositoryClassMethod')); } protected function replaceRepository(&$stub, $repositoryClassName, $repositoryClassVariable, $repositoryClassMethod) { $search = ['{{ repositoryClassName }}', '{{ repositoryClassVariable }}', '{{ repositoryClassMethod }}']; $stub = str_replace( $search, [$repositoryClassName, $repositoryClassVariable, $repositoryClassMethod], $stub ); return $this; } } |
コマンドを実行してリポジトリクラスを作ってみる。
1 |
todo-list % sail artisan make:repository TodoRepository Todo |
これができた。
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 namespace App\Repositories; use App\Models\Todo; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; class TodoRepository { public function all(): Collection { return Todo::all(); } public function create(): Todo { $todo = new Todo(); //add items $todo->save(); return $todo; } public function update(int $id): Todo { $todo = $this->findById($id); //add items $todo->save(); return $todo; } public function delete(int $id): bool { $todo = $this->findById($id); return $todo->delete(); } public function findById(int $id): ?Todo { $todo = Todo::find($id); if ($todo === null) { throw new ModelNotFoundException(); } return $todo; } } |
Labelのリポジトリクラスも作っておく。以下を実行した。
1 |
todo-list % sail artisan make:repository LabelRepository Label |
次はサービスクラスを作っていく。以下のコマンドを実行してみる。
1 |
todo-list % sail artisan make:service Todo/GetTodosService TodoRepository all --invokable |
これができた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php namespace App\Services\Todo; use App\Repositories\TodoRepository; class GetTodosService { private TodoRepository $todoRepository; public function __construct(TodoRepository $todoRepository) { $this->todoRepository = $todoRepository; } public function __invoke() { return $this->todoRepository->all(); } } |
他のサービスクラスも作る。
1 2 3 4 5 6 7 |
todo-list % sail artisan make:service Todo/CreateTodoService TodoRepository create --invokable todo-list % sail artisan make:service Todo/UpdateTodoService TodoRepository update --invokable todo-list % sail artisan make:service Todo/DeleteTodoService TodoRepository delete --invokable todo-list % sail artisan make:service Label/GetLabelsService LabelRepository all --invokable todo-list % sail artisan make:service Label/CreateLabelService LabelRepository create --invokable todo-list % sail artisan make:service Label/UpdateLabelService LabelRepository update --invokable todo-list % sail artisan make:service Label/DeleteLabelService LabelRepository delete --invokable |
コントローラーで呼び出すサービスクラスとそのサービスクラスが呼び出すリポジトリクラスが作れたのでリポジトリクラスの設定をいじっていく。
app/Repositories/TodoRepository.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 |
<?php declare(strict_types=1); namespace App\Repositories; use App\Models\Todo; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Carbon; class TodoRepository { /** * @return ?Collection */ public function all(): ?Collection { $todos = Todo::all(); if ($todos->count() === 0) { return null; } return $todos; } /** * @param string $task * @param Carbon $deadline * @return Todo */ public function create(string $task, Carbon $deadline): Todo { $todo = new Todo(); $todo->task = $task; $todo->deadline = $deadline; $todo->save(); return $todo; } /** * @param int $id * @param string $task * @param Carbon $deadline * @return Todo */ public function update(int $id, string $task, Carbon $deadline): Todo { $todo = $this->findById($id); $todo->task = $task; $todo->deadline = $deadline; $todo->save(); return $todo; } /** * @param int $id * @return bool */ public function delete(int $id): bool { $todo = $this->findById($id); return $todo->delete(); } /** * @param int $id * @return ?Todo */ public function findById(int $id): ?Todo { $todo = Todo::find($id); if ($todo === null) { throw new ModelNotFoundException(); } return $todo; } } |
リポジトリクラスができたのでテストを書いていく。ターミナルで以下を実行してユニットテストを作成。
1 |
todo-list % sail artisan make:test Repositories/TodoRepositoryTest --unit |
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 |
<?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()); } $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', Date::today()); $this->assertSame('test', $todo->task); $this->assertEquals(Date::today(), $todo->deadline); } public function testSuccessUpdate() { $todoRepository = new TodoRepository(); $todo = $todoRepository->create('test', Date::today()); $updatedTodo = $todoRepository->update($todo->id, 'updated', Date::tomorrow()); $this->assertSame('updated', $updatedTodo->task); $this->assertEquals(Date::tomorrow(), $updatedTodo->deadline); } public function testFailUpdateBecauseModelNotFound() { $this->expectException(ModelNotFoundException::class); $todoRepository = new TodoRepository(); $todoRepository->update(1, 'update', Date::tomorrow()); } public function testSuccessDelete() { $todoRepository = new TodoRepository(); $todo = $todoRepository->create('test', Date::today()); $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', Date::today()); $returnedTodo = $todoRepository->findById($todo->id); $this->assertSame($todo->id, $returnedTodo->id); } public function testFailFindByIdBecauseModelNotFound() { $this->expectException(ModelNotFoundException::class); $todoRepository = new TodoRepository(); $todoRepository->findById(1); } } |
テストを実行する。
1 |
todo-list % sail artisan test |
こんな感じのが表示されれば成功。
サービスクラスにまだ引数とかを書いてなかったので書いていく。
app/Services/Todo/CreateTodoService.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 |
<?php declare(strict_types=1); namespace App\Services\Todo; use App\Models\Todo; use App\Repositories\TodoRepository; use Illuminate\Support\Carbon; class CreateTodoService { private TodoRepository $todoRepository; /** * CreateTodoService constructor. * @param TodoRepository $todoRepository */ public function __construct(TodoRepository $todoRepository) { $this->todoRepository = $todoRepository; } /** * @param string $task * @param Carbon $deadline * @return Todo */ public function __invoke(string $task, Carbon $deadline): Todo { return $this->todoRepository->create($task, $deadline); } } |
app/Services/Todo/DeleteTodoService.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\Services\Todo; use App\Repositories\TodoRepository; class DeleteTodoService { private TodoRepository $todoRepository; /** * DeleteTodoService constructor. * @param TodoRepository $todoRepository */ public function __construct(TodoRepository $todoRepository) { $this->todoRepository = $todoRepository; } /** * @param int $id * @return bool */ public function __invoke(int $id): bool { return $this->todoRepository->delete($id); } } |
app/Services/Todo/GetTodosService.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\Services\Todo; use App\Repositories\TodoRepository; use Illuminate\Database\Eloquent\Collection; class GetTodosService { private TodoRepository $todoRepository; public function __construct(TodoRepository $todoRepository) { $this->todoRepository = $todoRepository; } /** * @return ?Collection */ public function __invoke(): ?Collection { return $this->todoRepository->all(); } } |
app/Services/Todo/UpdateTodoService.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 |
<?php declare(strict_types=1); namespace App\Services\Todo; use App\Models\Todo; use App\Repositories\TodoRepository; use Illuminate\Support\Carbon; class UpdateTodoService { private TodoRepository $todoRepository; /** * UpdateTodoService constructor. * @param TodoRepository $todoRepository */ public function __construct(TodoRepository $todoRepository) { $this->todoRepository = $todoRepository; } /** * @param int $id * @param string $task * @param Carbon $deadline * @return Todo */ public function __invoke(int $id, string $task, Carbon $deadline): Todo { return $this->todoRepository->update($id, $task, $deadline); } } |
次はapp/Services/Todoに入っているサービスクラスのテストを書いていく。
1 2 3 4 |
todo-list % sail artisan make:test Services/CreateTodoServiceTest --unit todo-list % sail artisan make:test Services/DeleteTodoServiceTest --unit todo-list % sail artisan make:test Services/GetTodosServiceTest --unit todo-list % sail artisan make:test Services/UpdateTodoServiceTest --unit |
tests/Unit/Services/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 |
<?php declare(strict_types=1); namespace Tests\Unit\Services; use App\Repositories\TodoRepository; use App\Services\Todo\CreateTodoService; use Illuminate\Support\Facades\Date; use Mockery\MockInterface; use Tests\TestCase; class CreateTodoServiceTest extends TestCase { public function testSuccess() { $task = 'task'; $deadline = Date::today(); $this->mock(TodoRepository::class, function (MockInterface $mock) use ($task, $deadline) { $mock->shouldReceive('create') ->with($task, $deadline) ->once(); }); $createTodoService = app(CreateTodoService::class); $createTodoService->__invoke($task, $deadline); } } |
tests/Unit/Services/DeleteTodoServiceTest.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 |
<?php declare(strict_types=1); namespace Tests\Unit\Services; use App\Repositories\TodoRepository; use App\Services\Todo\DeleteTodoService; use Mockery\MockInterface; use Tests\TestCase; class DeleteTodoServiceTest extends TestCase { public function testSuccess() { $id = 1; $this->mock(TodoRepository::class, function (MockInterface $mock) use ($id) { $mock->shouldReceive('delete') ->with($id) ->once(); }); $createTodoService = app(DeleteTodoService::class); $createTodoService->__invoke($id); } } |
tests/Unit/Services/GetTodosServiceTest.phpを以下のように編集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php declare(strict_types=1); namespace Tests\Unit\Services; use App\Repositories\TodoRepository; use App\Services\Todo\GetTodosService; use Mockery\MockInterface; use Tests\TestCase; class GetTodosServiceTest extends TestCase { public function testSuccess() { $this->mock(TodoRepository::class, function (MockInterface $mock) { $mock->shouldReceive('all') ->once(); }); $createTodoService = app(GetTodosService::class); $createTodoService->__invoke(); } } |
tests/Unit/Services/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 |
<?php declare(strict_types=1); namespace Tests\Unit\Services; use App\Repositories\TodoRepository; use App\Services\Todo\UpdateTodoService; use Illuminate\Support\Facades\Date; use Mockery\MockInterface; use Tests\TestCase; class UpdateTodoServiceTest extends TestCase { public function testSuccess() { $id = 1; $task = 'update'; $deadline = Date::tomorrow(); $this->mock(TodoRepository::class, function (MockInterface $mock) use ($id, $task, $deadline) { $mock->shouldReceive('update') ->with($id, $task, $deadline) ->once(); }); $createTodoService = app(UpdateTodoService::class); $createTodoService->__invoke($id, $task, $deadline); } } |
テストを実行する。
1 |
todo-list % sail artisan test |
こんな感じのが表示されれば成功。