11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

はじめに

本記事ではPHPのフレームワークLaravelDDDを導入する方法を解説します。
前提として...LaravelMVCが採用されていたりService Providerなどの便利なDIツールが採用されていることから、DDDを導入せずとも規則的なコードを書くことが可能になっています。そのため本記事で紹介する内容はあくまで一案を紹介することを目的としています。

GitHub

  • 本記事で紹介するリポジトリ

プロジェクト構成

  • ベースはレイヤードアーキテクチャ
  • Eloquent Model(ORM)やService Provider(DI)などを利用してLaravelの既存機能を活かした構成にする
  • NginxではなくLaravel Octane(Swoole)を使用する

ディレクトリ構成

  • app配下のみ記載
  • routerはLaravelの既存機能(routes/api.php)を使用する
app/
 ├──Console
 ├──Domain(ドメイン層)
 │    ├──Models
 │    └──Repositories
 ├──Exceptions
 ├──Http(プレゼンテーション層)
 │    ├──Controllers
 │    ├──Middleware
 │    ├──Outputs
 │    └──Kernel.php
 ├──Infra(インフラ層)
 │    ├──Apis(外部APIに渡す)
 │    └──Daos(DBに入れる)
 ├──Models
 ├──Providers
 └──Services(アプリケーション層)

依存関係

image.png

環境構築

※Docker環境が必要です。

  • コンテナを起動
make docker_up
  • Octuneサーバーを起動
make docker_run

コード解説

例では下記のAPIを実装して解説します。

POST: http://localhost:8000/api/create_example

  • リクエスト
{
    "name": "example1"
}
  • レスポンス
{
    "id": 1,
    "name": "example1",
    "message": "Example completed"
}

GET: http://localhost:8000/api/get_example/1

  • レスポンス
{
    "id": 1,
    "name": "example1",
    "message": "Example completed"
}

Model

レイヤードアーキテクチャで実装を行う場合、依存関係の低いレイヤーから実装していきます。今回の場合ではModelから実装します。

Modelではロジックの中心となる構造体(※PHPには構造体の機能はありませんが便宜上構造体と呼んでいます)やModelに強く依存している変換処理を記述していきます。今回の場合は単純な構造体とそれをJson化して呼び出すjsonSerialize()メソッドを追加しています。

例えばよくある実装例としてはemailのバリデーションや特定の配列やJsonをエンコードしてModelに格納する処理などはこちらにメソッドを追加します。

<?php

namespace App\Domain\Models;

class Example implements \JsonSerializable
{
    private int $id;
    private string $name;

    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    public function jsonSerialize()
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}

Dao

Daoではメソッドに渡されたモデルや値をEloquent Modelに置き換えてデータベースとのやり取りを行います。また、この段階でEloquent Modelを作成します。

<?php

namespace App\Infra\Daos;

use App\Domain\Models\Example;
use App\Models\Example as EloquentExample;
use App\Domain\Repositories\ExampleRepository;

class ExampleDao implements ExampleRepository
{
    public function findById(int $id): ?Example
    {
        $eloquentExample = EloquentExample::find($id);
        if ($eloquentExample === null) {
            return null;
        }

        return new Example(
            $eloquentExample->id,
            $eloquentExample->name
        );
    }

    public function save(Example $example): Example
    {
        $eloquentExample = EloquentExample::create([
            'name' => $example->jsonSerialize()['name'],
        ]);

        return new Example(
            $eloquentExample->id,
            $eloquentExample->name
        );
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Example extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     * @var array
     */
    protected $fillable = [
        'name',
    ];
}

Repository

RepositoryではDaoに記述したメソッドのインターフェースを実装します。

<?php

namespace App\Domain\Repositories;

use App\Domain\Models\Example;

interface ExampleRepository
{
    public function findById(int $id): ?Example;
    public function save(Example $example): ?Example;
}

Service Provider

次にRepositoryDaoのインターフェースとして組み込むためにDIで依存性関係を注入していきます。LaravelにはAppServiceProviderという便利なDIツールが標準採用されているためこちらのregister()メソッドに使用するクラスを記述していきます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\Repositories\ExampleRepository;
use App\Infra\Daos\ExampleDao;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(
            ExampleRepository::class,
            ExampleDao::class,
        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        //
    }
}

Service

Serviceではメソッドに渡された値をModelに当てはめてRepositoryに渡す処理を記述します。また、Infra層では複雑になる依存関係の強い処理をまとめて記述して、他のレイヤーでの依存度を低くすることを意識します。

Serviceでどれだけ依存度の高い処理を記述するのかは個人やチームでの判断になるとは思うのであくまで参考程度に考えてください。

<?php

namespace App\Services;

use App\Domain\Models\Example;
use App\Domain\Repositories\ExampleRepository;

class ExampleService
{
    private $exampleRepository;

    public function __construct(ExampleRepository $exampleRepository)
    {
        $this->exampleRepository = $exampleRepository;
    }

    /**
     * Get Example By ID
     * @param int id
     * @return Example|null
     */
    public function getExampleById(int $id): ?Example
    {
        return $this->exampleRepository->findById($id);
    }

    /**
     * Create Example
     * @param string name
     * @return Example|null 
     */
    public function createExample(string $name): ?Example
    {
        $example = new Example(0, $name);

        return $this->exampleRepository->save($example);
    }
}

Controller

Controllerでは通常のLaravelと同様にRouterから送られたリクエストをModelや適切な値に変換してレスポンスを返す処理を記述します。今回はレスポンスの値を定義するOutputクラスで構造体を定義して使用しています。

<?php

namespace App\Http\Controllers;

use App\Domain\Models\Example;
use App\Services\ExampleService;
use App\Http\Outputs\ExampleOutput;
use Illuminate\Http\Request;

class ExampleController extends Controller
{
    /**
     * @var ExampleService
     */
    private $exampleService;

    /**
     * @var ExampleOutput
     */
    private $exampleOutput;

    /**
     * ExampleController constructor
     * @param ExampleService $exampleService
     */
    public function __construct(ExampleService $exampleService,  ExampleOutput $exampleOutput)
    {
        $this->exampleService = $exampleService;
        $this->exampleOutput = $exampleOutput;
    }

    /**
     * Get Example
     * @param int id
     * @return \Illuminate\Http\JsonResponse
     */
    public function getExample(int $id)
    {
        $example = $this->exampleService->getExampleById($id);
        if ($example === null) {
            return response()->json(['error' => 'Example not found'], 404);
        }

        $output = $this->exampleOutput->toExample($example, "Example completed");

        return response()->json($output, 200);
    }

    /**
     * Create Example
     * @param Request request
     * @return \Illuminate\Http\JsonResponse
     */
    public function createExample(Request $request)
    {
        $data = $request->all();
        $example = $this->exampleService->createExample($data['name']);

        $output = $this->exampleOutput->toExample($example, "Example completed");

        return response()->json($output, 200);
    }
}

Output

Outputではレスポンスで使用する構造体を定義します。

<?php

namespace App\Http\Outputs;

use App\Domain\Models\Example;

class ExampleOutput
{
    /**
     * ExampleOutput
     * @param Example Example
     * @param string message
     * @return array array
     */
    public function toExample(Example $example, string $message)
    {
        return [
            'id' => $example->jsonSerialize()['id'],
            'name' =>  $example->jsonSerialize()['name'],
            'message' => $message,
        ];
}

Router

最後にAPIのPathからControllerにハンドリングを行う処理を記述していきます。この辺りは通常のLaravelの記述方法と全く同じです。

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ExampleController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::get('/get_example/{id}', [ExampleController::class, 'getExample']);
Route::post('/create_example', [ExampleController::class, 'createExample']);

まとめ

Laravelを使用するにあたりMVCのアーキテクチャをそのまま使用するかDDDクリーンアーキテクチャなどの概念を取り入れるかは賛否が分かれるところですが、個人的には開発チームの特性やサービスの特性によって適切に変えていくべきだと考えます。例えば大規模開発やチームの入れ替わりが少なくローカルルールを採用しやすい組織では、Laravelのような既存アーキテクチャがあるフレームワークにDDDクリーンアーキテクチャのような外の概念を導入してオリジナルフレームワークを構築することで開発効率や保守性を高めることは可能でしょう。しかし、小規模開発やメンバーの入れ替わりが多い組織では、多くの場合Laravelの既存機能を用いて開発する方が効率的だと思います。

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?