Laravelをなんとなく書いていると起こる問題点
- コントローラ膨らみすぎ問題
- Model膨らみすぎ問題
- MdoelにORMマッパーにベタベタに依存したビジネスロジックを書いてしまってテスト書くのがしんどくなる問題
設計方針
-
重要度が高いビジネスロジックはDBやLaravelの機能に依存させない
LaravelのModelにビジネスロジックを書いてしまうとEloquent依存のコードを簡単にかけてしまいます。
とりあえず動かすにはこれが一番早いので選択肢としてはありかと思いますが、後に機能が複雑になってくるとテスト結果がDBの状態になったりテストがしにくくなるのでやめましょう
とはいえEloquentは便利なのでRepository内で使いましょう -
できるところはLaravelに依存する
ログイン、ページネーション、バリデーションなどはLaravelに依存しましょう。 -
できる限りテストを簡単に
テストを簡単にかけるような設計でないとテスト書くのが苦痛になり、テストが雑になりがちです。
解決策
DDDや、クリーンアーキテクチャはそのまま導入しようとしましたが、現状に対してあまりに壮大過ぎる&チームでやるには学習コストが高すぎるので以下を導入することにしました。
Modelからビジネスロジックを分離させる
Entitiesディレクトリを新規に作成し、ここに重要度が高いビジネスロジックをModelに依存させない形で作成します。
LaravelのFacadeも使ってはいけません。CakePHPやWordPressにそのままコピペできるようにしましょう。
<?php
namespace App\Entities;
class Video
{
protected int $id;
protected string $title;
protected string $description;
protected string $publish_at;
protected User $author;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return string
*/
public function getDescription(): string
{
return $this->description;
}
/**
* @return User
*/
public function getAuthor(): User
{
return $this->author;
}
/**
* @return User|string
*/
public function getPublishAt()
{
return $this->publish_at;
}
/**
* Video constructor.
* @param int $id
* @param string $title
* @param string $description
* @param string $publish_at
* @param User $author
*/
public function __construct(int $id, string $title, string $description, string $publish_at, User $author)
{
$this->id = $id;
$this->title = $title;
$this->description = $description;
$this->publish_at = $publish_at;
$this->author = $author;
}
public function check_publish(){
// いろいろなロジックを書いていく
}
}
Repository層の導入
Repositoriesディレクトリを作成し、ここでEloquentを使いながら上のEntityを作成します。
まずはapp/Repositoriesにインターフェイスを作成します。インターフェイスを作成することでデータベースへの依存を防げます。
<?php
namespace App\Repositories;
use App\Entities\Video;
interface VideoRepository
{
public function getById(int $id): ?Video;
}
app/Repositories/implに実際にEloquentを使った組み込みを書いていきます。
<?php
namespace App\Repositories\impl;
use App\Entities\Video;
use App\Repositories\VideoRepository;
class VideoDBRepository implements VideoRepository
{
function getById(int $id): ?Video
{
$video = \App\Model\Video::find($id);
if(empty($video)){
return null;
}
return new Video($video->id, $video->title, $video->discription, $video->author}
}
サービスプロバイダーを追加し、VideoRepositoryとVideoDBRepositoryをバインドします。
これでVideoRepository参照するとVideoDBRepositoryが読み込まれます。
VideoRepositoryに依存することにより入れ替えで簡単にテストができます。
<?php
namespace App\Providers;
use App\Repositories\impl\VideoDBRepository;
use App\Repositories\VideoRepository;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->app->bind(VideoRepository::class, VideoDBRepository::class);
}
}
実際にリポジトリを使用する時はコンストラクタインジェクションを使います。
サービス・プロバイダーでbindしているのでInterfaceからアクセス可能です。
<?php
namespace App\Http\Controllers;
use App\Entities\Video;
use App\Repositories\VideoRepository;
class VideoController extends Controller
{
protected VideoRepository $videoRepository;
/**
* VideoController constructor.
* @param VideoRepository $videoRepository
*/
public function __construct(VideoRepository $videoRepository)
{
$this->videoRepository = $videoRepository;
}
public function video(VideoRequest $request){
/** @var Video $video */
$video = $this->videoRepository->getById($request->get('id'));
return view('video', [
'video' => $video
]);
}
}
まとめ
ControllerはRepositoryのIntarfaceに依存しているので、ControllerはDBを意識しないでよくなりました。
テスト用のMockRepositoryでも、DBではなくAPIで永続化をするApiRepositoryでもimpl組み込みを追加しbindし直すだけで対応が可能になりました。
そしてEntityではRepositoryもControllerも意識する必要がありません。ロジックのテストにDBの依存が入らないのでテストがやりやすくなります。