株式会社クイックのWebサービス開発blog

HAPPYなサービスプランナー・エンジニア・デザイナーのブログです。

LaravelでDoctrineを使ってみた

こんにちは、ソフトウェアエンジニアのissyです。

現在のプロジェクトでは、Laravel+Doctrine+クリーンアーキテクチャで開発を行なっています。 本来Laravelを使う場合には、ORMはEloquentを使いますが、Doctrineを使っています!

なぜ?

アクティブレコードを使った場合、データベースとエンティティが密結合になってしまうので、 データマッパーのORMを使う事で分離したかったからです。 PHPのORMはほとんどがアクティブレコード実装のため、データマッパーだとDoctrine一択でした。

現在開発中のシステムは業務システムであるため、複雑なビジネスロジックもあり、アクティブレコードは不向きだと判断しました。

どうやって?

Laravel Doctrineを使っています。

インストールの方法などは公式を参照して下さい。

github.com

実装

以下のようなディレクトリ構成で設計しました。

  • app/

Laravelの標準のディレクトリ構成。

  • packages/Domain

エンティティ、値オブジェクトなどのビジネスルールを格納する。(User,Addressクラスなど。)

  • packages/Infrastructure

技術的関心事の処理を行うクラスを格納する。(UserRepositoryクラス、マッピングファイルなど。)

  • packages/Usecase

ユースケースクラスを格納する。

マッピング

  • xmlマッピングがオススメです。 yamlは2.7で非推奨、アノテーションはinfrastructure層とdomain層の関心事が混ざり、変更のコストが高くなりがちです。

  • Embeddedが便利。 xmlなどのマッピングファイルを記述する事で、value objectを生成できます。 以下では、「address」に使用しています。

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
  <entity name="packages\Domain\Model\User" table="users">
    <id name="id" type="bigint" column="id">
      <generator strategy="IDENTITY"/>
    </id>
    <field name="name" type="string" column="name" length="255" nullable="false">
      <options>
        <option name="fixed"/>
      </options>
    </field>
      <embedded
          name="address"
          class="packages\Domain\Model\Address"
          use-column-prefix="false"
      />
  </entity>
</doctrine-mapping>
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    <embeddable name="packages\Domain\Model\Address">
        <field name="street" type="string" />
        <field name="postalCode" type="string" />
        <field name="city" type="string" />
        <field name="country" type="string" />
    </embeddable>
</doctrine-mapping>

エンティティ

xmlマッピングする事で、infrastructure層に依存しない形で実装できました!

<?php

declare(strict_types=1);

namespace packages\Domain\Model;

class User
{
    private int $id;
    private string $name;
    private Address $address;

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

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return Address
     */
    public function getAddress(): Address
    {
        return $this->address;
    }
}


値オブジェクト

<?php

declare(strict_types=1);

namespace packages\Domain\Model;

class Address
{
    private string $street;

    private string $postalCode;

    private string $city;

    private string $country;

    // 以下略...
}

リポジトリ

ドメインオブジェクトを返却するリポジトリクラスです。

ORマッパーやSQLは、データベースに依存しないようにリポジトリに記述しています。

<?php

declare(strict_types=1);

namespace packages\Infrastructure\Doctrine;
use Doctrine\ORM\EntityRepository;
use packages\Domain\Model\User;
use packages\Domain\Model\UserId;
use packages\Infrastructure\UserRepository;

class DoctrineUserRepository extends EntityRepository implements UserRepository
{

    public function findUser(UserId $userId): User
    {
        return $this->find($userId->value());
    }

    public function add(User $user): void
    {
        $em = $this->getEntityManager();
        $em->persist($user);
        $em->flush();
    }
}

ユースケース

リポジトリから取得したエンティティをプレーンオールドなオブジェクトに詰めて返却しています。

<?php


namespace packages\Usecase;


use packages\Domain\Model\UserId;
use packages\Infrastructure\UserRepository;

class UserGetInteractor implements UserGetUsecase
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }
    public function execute(int $userId) : UserGetOutputData
    {
       $user = $this->userRepository->findUser(new UserId($userId));
       return new UserGetOutputData(
           $user->getId(),
           $user->getName(),
           $user->getAddress()->street(),
           $user->getAddress()->postalCode(),
           $user->getAddress()->city(),
           $user->getAddress()->country(),
       );


    }
}

所感

Doctrineのおかげでdomainとinfrastructureが分離した綺麗な設計が出来たと思います! リポジトリパターンを使えば、Eloquentに切り替えることも容易にできます。

コードの品質を高めることで、ユーザーに素早く価値を届けられるように 現在のプロジェクトでは他にも様々な取り組みを行なっていますが、それはまた次回に書こうと思います!


\\『真のユーザーファーストでマーケットを創造する』「ありがとう」で溢れる仲間を募集中です!! // 919.jp