2
0

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.

LaravelDoctrineでXMLマッピング

Last updated at Posted at 2022-09-01

これはなに?

LaravelをFWとして採用した際に通常はEloquentをORMとして採用することがほとんどだとおもいます。
しかし、例えばレガシーなDB設計になっているWEBアプリケーションをLaravelで作り直す
などの必要が生じた場合や、リポジトリパターンを活用して重厚なシステムを構築したい場合
Eloquentでは厳しい場面も出てきます。
その際に第二の選択しとしてDoctrineというORMがあげられます。
このORMをxmlマッピングにて使用すると、DBに関する関心事と実際の業務ロジックを限りなく分離させることが可能となります。

この記事では、公式のドキュメントからのみではわかりづらいことが多い
LaravelDoctrineのXMLマッピング
を実現するまでの手順を記載していきます。
尚本記事内で紹介している手法を実際に使用しているリポジトリも紹介しておきます。

↑をバンドルしているrepository

LaravelDoctrineのインストール

composerからインストールします

.bash
composer require doctrine/inflector:^1.4 laravel-doctrine/orm laravel-doctrine/migrations

設定ファイル生成

.bash
php artisan vendor:publish --tag="config"

ここまで実行すると
{LaravelプロジェクトPath}/config/doctrine.php
が生成されていると思います。

設定

Doctrineでは
「どのオブジェクト」に「どのようなテーブルのレコード」を「どのようにマッピング」するかを

  • XML
  • YAML
  • ANNOTATION

と3種類の手法を使って定義できます。

有識者や熟練の設計者には誤解を招く表現かもしれませんがこのような表現がとっかかりには丁度よいかと思ってます

今回の記事ではXMLマッピングの方法を記載していきます

この記事で扱うsampleシチュエーション

  • ユーザーマスタテーブルとしてsample.usersが定義されている。
  • ユーザー情報を記録するテーブルとしてsample.user_profilesが定義されている。
  • アプリケーション側では、ユーザー情報をオブジェクトとして取得して業務ロジックで扱いたい。

データベース定義

sample.usersテーブル

user_id access_id password expires_at created_at
1 u1 xxxx 2023-01-01 12:00:00 2020-01-01 12:00:00
2 u2 xxxx 2023-01-01 13:00:00 2020-01-01 12:00:00
3 u3 xxxx 2023-01-01 14:00:00 2020-01-01 12:00:00

sample.user_profilesテーブル

user_profile_id user_id name tel mail
1 1 user1 000-0000-000 user1@sample.com
2 2 user2 000-0000-001 user2@sample.com
3 3 user3 000-0000-002 user2@sample.com

業務ロジックを扱うためのオブジェクト

jsonで表現した場合

下記のような構造として扱いたい

user.json
{
    "user":{
        "userId":{"value": 1},
        "accessId":{"value": "u1"},
        "password": "xxxx",
        "expiresAt": {"value": "2023-01-01 12:00:00"}
        "profile" :{
            "userProfileId": {"value": 1},        
            "name": {"value": "user1"},
            "tel": {"value": "000-0000-0000"},
            "mail":{"value": "user1@sample.com"}
       }
    }
}

ユーザー情報のオブジェクト

PHPで上記Jsonを下記オブジェクトで表現します。

ディレクトリ構造のsample

Laravelをインストールした段階ではappという標準のdirectoryがいて、その中にアプリケーションロジックを入れていくと思いますが、このsampleではappとは完全に分離してこのdir内にPHPオブジェクトクラスとXMLファイルを詰め込みます

sample.path
{LaravelProjectPath}
   |-app //Laravel標準dir
   |-packages //このsample用に新設したdir
        |-domain
        |    |-model //この中にオブジェクトを定義したphpファイルを追加していく
        |-infrastructure
        |        |- database
        |            |- xml // この中にXMLファイルを追加していく
        |
       ....

上記
packages/domain/model/
内に、下記のようにオブジェクトを定義していきます。

packages/domain/model/user/User.php
<?php
namespace packages\domain\model\user;
use packages\domain\model\user\profile\UserProfile;
class User
{
    private UserId $userId;
    private AccessId $accessId;
    private string $password;
    private ExpiresAt $expiresAt;
    private UserProfile $profile;
    /**
     * @return int
     */
    public function getUserId(): int
    {
        return $this->userId->getValue();
    }
    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->profile->getName();
    }
    /**
     * @return UserProfile
     */
    public function getProfile(): UserProfile
    {
        return $this->profile;
    }
    
}
packages/domain/model/user/UserId.php
namespace packages\domain\model\user;
class UserId 
{
    private ?int $value;
    public function __construct(int $value = null)
    {
        $this->value = $value;
    }
    public static function create(string $value): self
    {
        return new self((int)$value);
    }
    public function isEmpty(): bool
    {
        return is_null($this->value);
    }
    public function toInteger(): int
    {
        return $this->value;
    }
    public function getValue(): ?int
    {
        return $this->value;
    }
}
packages/domain/model/user/AccessId.php
namespace packages\domain\model\user;
class AccessId 
{
    private ?string $value;
    public function __construct(string $value = null)
    {
        $this->value = $value;
    }
    public function isEmpty(): bool
    {
        if (!$this->value) {
            return true;
        }
        return false;
    }
    public function toString(): string
    {
        if ($this->isEmpty()) {
            return "";
        }
        return $this->value;
    }
    public function getValue(): ?string
    {
        return $this->value;
    }
}
packages/domain/model/user/ExpiresAt.php
namespace packages\domain\model\user;
class ExpiresAt {
    private ?DateTime $value;
    public function __construct(DateTime $value = null)
    {
        $this->value = $value;
    }
    public static function create(string $value = null): self
    {
        if ($value) {
            return new self(DateTime::createFromFormat('Y-m-d H:i:s', $value));
        }
        return new self();
    }
    public function isEmpty(): bool
    {
        return is_null($this->value);
    }
    public function toLocalDateTime(): DateTime
    {
        return $this->value;
    }
    public function getValue(): ?DateTime
    {
        return $this->value;
    }
    public function format(): ?string
    {
        if ($this->isEmpty()) {
            return null;
        }
        return $this->value->format('Y-m-d H:i:s');
    }
}
packages/domain/model/user/profile/UserProfile.php
namespace packages\domain\model\user\profile;
class UserProfile
{
    private UserProfileId $userProfileId;
    private UserName $name;
    private UserTel $tel;
    private UserMail $mail;
    /**
     * @return int
     */
    public function getName(): string
    {
        return $this->name->getValue();
    }
    /**
     * @return int
     */
    public function getTel(): string
    {
        return $this->tel->getValue();
    }
    /**
     * 電話番号を配列に分解します。
     * @return array
     */
    public function getArrayTel(): array
    {
        return $this->tel->toArrayValue();
    }
}
packages/domain/model/user/profile/UserProfileId.php
namespace packages\domain\model\user\profile;
class UserProfileId 
{
    private ?int $value;
    public function __construct(int $value = null)
    {
        $this->value = $value;
    }
    public static function create(string $value): self
    {
        return new self((int)$value);
    }
    public function isEmpty(): bool
    {
        return is_null($this->value);
    }
    public function toInteger(): int
    {
        return $this->value;
    }
    public function getValue(): ?int
    {
        return $this->value;
    }
}
packages/domain/model/user/profile/UserName.php
namespace packages\domain\model\user\profile;
class UserName 
{
    private ?string $value;
    public function __construct(string $value = null)
    {
        $this->value = $value;
    }
    public function isEmpty(): bool
    {
        if (!$this->value) {
            return true;
        }
        return false;
    }
    public function toString(): string
    {
        if ($this->isEmpty()) {
            return "";
        }
        return $this->value;
    }
    public function getValue(): ?string
    {
        return $this->value;
    }
}
packages/domain/model/user/profile/UserTel.php
namespace packages\domain\model\user\profile;
class UserTel 
{
    private ?string $value;
    public function __construct(string $value = null)
    {
        $this->value = $value;
    }
    public function isEmpty(): bool
    {
        if (!$this->value) {
            return true;
        }
        return false;
    }
    public function toString(): string
    {
        if ($this->isEmpty()) {
            return "";
        }
        return $this->value;
    }
    public function getValue(): ?string
    {
        return $this->value;
    }
    public function toArrayValue(): array
    {
       return explode('-',$this->value);
    }
}
packages/domain/model/user/profile/UserMail.php
namespace packages\domain\model\user\profile;
class UserMail 
{
    private string $value;
    public function __construct(string $value = null)
    {
        $this->value = $value;
    }
    public function isEmpty(): bool
    {
        if (!$this->value) {
            return true;
        }
        return false;
    }
    public function toString(): string
    {
        if ($this->isEmpty()) {
            return "";
        }
        return $this->value;
    }
    public function getValue(): string
    {
        return $this->value;
    }
    public function toInternetAddress(): string
    {
        $valid = new RFCValidation();
        try {
            $valid->isValid($this->mail->toString(), new EmailLexer());
        } catch (Exception $e) {
            //$valid->getError();
            //$valid->getWarnings();
            // TODO ユーザー定義Exception設置
        }
        $this->address = $this->mail->toString();
    }
}

ここまででdir構造は

sample.path
{LaravelProjectPath}
   |-app //この中にLaravel標準のdirectoryとかが入っていると思います。
   |-packages //このsampleではappとは完全に分離してこのdir内にPHPオブジェクトクラスとXMLファイルを詰め込みます
        |-domain
        |    |-model
        |        |-user
        |            |- User.php
        |            |- UserId.php
        |            |- AccessId.php
        |            |- ExpiresAt.php
        |            |
        |            |-profile
        |                  |- UserProfileId.php
        |                  |- UserName.php
        |                  |- UserTel.php
        |                  |- UserMail.php
        |
        |-infrastructure
                |- database
                    |- xml

となります。
実際にLaravel上で業務ロジックを記述していくときには、上記のうち
packages/domain/models内のクラスのみを意識して実装することができるようにすることを目的とします。

XML用の設定

config/doctrine.phpに

config/doctrine.php
    'managers'                   => [
        'default' => [
            'dev'           => env('APP_DEBUG', false),
            'meta'          => env('DOCTRINE_METADATA', 'simplified_xml'),
            'connection'    => env('DB_CONNECTION', 'pgsql'),
            'namespaces'    => [],
            'paths'         => [],
            'repository'    => Doctrine\ORM\EntityRepository::class,
            'proxies'       => [
                'namespace'     => false,
                'path'          => storage_path('proxies'),
                'auto_generate' => env('DOCTRINE_PROXY_AUTOGENERATE', false)
            ],
            'events'        => [
                'listeners'   => [],
                'subscribers' => []
            ],
            'filters'       => [],
            'mapping_types' => [
                //'enum' => 'string'
            ]
        ]
    ],
    //以下略

のように記載していきます。
ポイントは

config/doctrine.php
 'meta'          => env('DOCTRINE_METADATA', 'simplified_xml')

config/doctrine.php
 paths => []

です。
となっていますが、このpaths内に

どのXMLファイルと
どのオブジェクトを紐づけるか

を定義していく必要があります。

このファイルは開発をしていくうえで頻繁に更新していく必要があります。

マッピング定義XML

どのテーブルのどのカラムをどのオブジェクトに紐づけるか。
をXMLに定義していきます。このXMLを定義したのちに、

config/doctrine.php
 paths => []

へオブジェクトとXMLのパスを記載していきます。
また、XMLの命名規則として
orm.xml
を拡張子として記載します。

sample.orm.xml
<?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="{クラスpath}" table="{DBテーブル名}">
    ...
    </entity>

今回注入先のオブジェクトのメンバ変数はそれぞれオブジェクトになっていたりします。
この場合doctrineでは
メンバ変数のオブジェクトに対するXMLに対してentityタグの代わりに

doctine-XML-schema.xml
<embeddable name='{メンバ変数名オブジェクトのpath}' table='{対象テーブル名}'>

というタグで定義しつつ、
参照元のXMLにて

doctine-XML-schema.xml
<embedded name="{オブジェクトのメンバ変数名}"
                  class="{メンバ変数名オブジェクトのpath}"
                  use-column-prefix="false"/>

というタグを定義することでオブジェクトの階層を表現できます。
上記を利用して

sample.path
infrastructure
                |- database
                    |- xml
                         |- user
                              |-profile

ないにそれぞれXML定義を追加していきます。

table="{テーブル名}"が指定されたXML定義には必ず一個以上
idタグが必要でこれはdoctirne仕様的にも結構重要な部分であることにご注意ください。
idタグはembeddedとなるオブジェクト内に記載されていても有効です。
Userのxmlにはidタグはありませんが、embeddedでUserIdを指定し、UserIdの中身はidタグが記載されています。
この時UserのプライマリーキーはDoctrine上UserIdとなります。

infrastructure/database/xml/user/User.orm.xml
<?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\User" table="sample.users">
        <embedded name="userId"
                  class="packages\domain\model\user\UserId"
                  use-column-prefix="false"/>
        <embedded name="accessId"
                  class="packages\domain\model\user\AccessId"
                  use-column-prefix="false"/>
        <field name="password" type="string" column="password"/>
        <embedded name="expiresAt"
                  class="packages\domain\model\user\ExpiresAt"
                  use-column-prefix="false"/>
        <embedded name="profile"
                  class="packages\domain\model\user\profile\UserProfile"
                  use-column-prefix="false"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/UserId.orm.xml
<?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\user\UserId">
        <id name="value" type="integer" column="user_id"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/AccessId.orm.xml
<?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\user\AccessId">
        <field name="value" type="string" column="access_id"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/ExpiresAt.orm.xml
<?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\user\ExpiresAt">
        <field name="value" type="datetime" column="expires_at"/>
    </embeddable>
</doctrine-mapping>

Userが持つprofileというメンバのオブジェクトマッピング定義

infrastructure/database/xml/user/profile/UserProfile.orm.xml
<?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\user\profile\UserProfile" table='sample.user_profiles'>
        <embedded name="userProfileId"
                  class="packages\domain\model\user\profile\UserProfileId"
                  use-column-prefix="false"/>
        <embedded name="name"
                  class="packages\domain\model\user\profile\UserName"
                  use-column-prefix="false"/>
        <embedded name="tel"
                  class="packages\domain\model\user\profile\UserTel"
                  use-column-prefix="false"/>
        <embedded name="mail"
                  class="packages\domain\model\user\profile\UserMail"
                  use-column-prefix="false"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/profile/UserProfileId.orm.xml
<?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\user\profile\UserProfileId">
        <id name="value" type="integer" column="user_profile_id"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/profile/UserName.orm.xml
<?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\user\profile\UserName">
        <id name="value" type="string" column="name"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/profile/UserTel.orm.xml
<?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\user\profile\UserTel">
        <id name="value" type="string" column="tel"/>
    </embeddable>
</doctrine-mapping>
infrastructure/database/xml/user/profile/UserMail.orm.xml
<?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\user\profile\UserMail">
        <id name="value" type="string" column="mail"/>
    </embeddable>
</doctrine-mapping>

ここまででディレクトリ構造は下記のようになります。

sample.path
{LaravelProjectPath}
   |-app //この中にLaravel標準のdirectoryとかが入っていると思います。
   |-packages //このsampleではappとは完全に分離してこのdir内にPHPオブジェクトクラスとXMLファイルを詰め込みます
        |-domain
        |    |-model
        |        |-user
        |            |- User.php
        |            |- UserId.php
        |            |- AccessId.php
        |            |- ExpiresAt.php
        |            |
        |            |-profile
        |                  |- UserProfileId.php
        |                  |- UserName.php
        |                  |- UserTel.php
        |                  |- UserMail.php
        |
        |-infrastructure
                |- database
                    |- xml
                        |- user
                             |- User.orm.xml
                             |- UserId.orm.xml
                             |- AccessId.orm.xml
                             |- ExpiresAt.orm.xml
                             |
                             |- profile
                                   |- UserProfile.orm.xml
                                   |- UserName.orm.xml
                                   |- UserTel.orm.xml
                                   |- UserMail.orm.xml

XMLとオブジェクトの関連性をパスに定義する

ここまでで、かなりのファイルを作成してきました。
XML内に、フルパスでクラスの指定をしているのですが、残念ながらDoctrineではさらに同じような紐づけ定義をしていく必要があります。
それが前述した

config/doctrine.php
 'paths' => []

です。
この配列にXMLファイルまでのパス、対象ドメインファイルまでのパス
を下記のように定義していく必要があります。

config/doctrine.php
 'paths' => [
    {XMLファイルまでのパス} => {対象オブジェクトファイルまでのパス}
 ]

今回のsampleを動作させる場合。下記の設定が必要となります。

config/doctrine.php
'paths'         => [
    base_path('packages/infrastructure/database/xml/user/')
    =>'packages\domain\model\user',
    base_path('packages/infrastructure/database/xml/user/profile')
    =>'packages\domain\model\user\profile',

嵌りそうなポイント
user/profile
に対する関連付けですね。例えば

config/doctrine.php
'paths'         => [
    base_path('packages/infrastructure/database/xml/user/')
    =>'packages\domain\model\user',

のみだと。
packages/infrastructure/database/xml/user/profile
ないのXMLを再帰的にリードしてくれないようなのです。
よって、各ファイルが格納されているパスはすべて上記のように関連付けをしてあげる必要があるのです。
※ここは本当は

config/doctrine.php
'paths'         => [
    base_path('packages/infrastructure/database/xml')=>'packages\domain\model',

これだけで完結するようにしたいですが、'namespaces' 属性を使ってみたりと試行錯誤してみましたがなんとも解決しませんでした。
doctrine側で改善できる気がしてるんですが、、、現状機能としていまいちといわざるをえないですね。
このあたりもしなにかよい方法を知っている方がいれば是非教えていただけるとありがたいです。

使ってみる

ここまでで、XMLマッピングに関する設定が完了しました。
あとは実際にDoctirneからsample.usersテーブルデータを取得し、意図通りとなっていることを確認していくのみです。
本記事ではDoctrineのRepositoryに関する詳細は省略しますが、概要としては下記のような構造にしつつユーザー情報取得処理を記述していきます。

sample.path
{LaravelProjectPath}
   |-app //この中にLaravel標準のdirectoryとかが入っていると思います。
   |-packages //このsampleではappとは完全に分離してこのdir内にPHPオブジェクトクラスとXMLファイルを詰め込みます
        |-domain
        |    |-model
        |        |-user
        |            |- UserRepository.php
        |-infrastructure
        |        |- database
        |            |- doctrine
        |                  |- DoctrineUserRepository.php
        |
        |- service
               |- UserGetService.php

のようにUserオブジェクトを扱うためのRepositoryを実装します。

packages/domain/model/user/UserRepository.php
<?php
namespace packages\domain\model\User;
interface UserRepository
{
    /** @retrun User */
    public function findUser(UserId $userId): User;
    /** @retrun User profileにも注入されます */
    public function findUserInfo(UserId $userId): User;
    /** @retrun Array<User> */
    public function findAllUsers(): array;
}
  • sample.userを単純にfindしてUserオブジェクトに入れ込むメソッド(profileは空になります)、
  • sample.user_profilesの情報も一括で取得するメソッド。※ここではNativeQueryを利用する例も書いておきます
  • sample.userをすべて取得する(profileはなし)

の三種類のメソッドをsampleで書いてみました。

そもそもuser_profilesはone-to-oneつかった方がいいとか、NativeQuery書くほどか?って話がありますが
直感的にわかりやすいかなと思いこのような形のsampleにしています。

packages/infrastructure/database/doctrine/user/DoctrineUserRepository.php
<?php
namespace packages\infrastructure\database\doctrine\user;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use packages\domain\model\User\User;
use packages\domain\model\User\UserRepository;
use packages\infrastructure\database\doctrine\DoctrineRepository;

class DoctrineUserRepository extends EntityRepository implements UserRepository
{
    /**
     * @throws NonUniqueResultException
     * @throws NoResultException
     */
    public function findUser(UserId $userId): User
    {
        try {
            // User.orm.xml内でidタグを定義しているuser_idに対して検索がかかる。
            // 複数idを指定している場合はcriteriaとか使うといいんじゃないかな
            return $this->find($userId->getValue());
        } catch (NoResultException $e) {
            throw $e;
        }
    }
    
    /**
     *  ユーザープロファイル情報も同時に取得する。ここではNativeQueryで書いてみる
     *  User.profileにもSQLで取得した値が格納されていきます
     * @return User
     */
    public function findUserInfo(): User
    {
        $rsm = new ResultSetMappingBuilder($this->getEntityManager());
        $rsm->addNamedNativeQueryResultClassMapping($this->getClassMetadata(), $this->getClassName());
        $sql = 'SELECT 
                  user_id
                 , password
                 , access_id
                 , expires_at
                 , user_profile_id
                 , name
                 , tel
                 , mail
            FROM sample.users
            JOIN sample.user_profiles USING (user_id)
            WHERE 1 = 1
            AND user_id = :userId
        ';
        $query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
        $query->setParameters([
            'userId' => $userId->toInteger()
        ]);
        try {
            return $query->getResult();
        } catch (NoResultException $e) {
            throw $e;
        }
    }
    
    /**
     * @return Array<User>
     */
    public function findAllUsers(): array
    {
         try {
            return $this->findAll();
        } catch (NoResultException $e) {
            throw $e;
        }
    }
}

上記リポジトリーをLaravelのDIコンテナを利用しつつ

app/Providers/DatasourceServiceProvider.php
<?php
namespace App\Providers;
use packages\infrastructure\database\doctrine as Doctrine;
use packages\infrastructure\database as Database;
use packages\domain\model as DomainModel;
use Illuminate\Support\ServiceProvider;

class DatasourceServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()    
    {
        $this->app->bind(DomainModel\user\UserRepository::class, function ($app) {
            return new Doctrine\user\DoctrineUserRepository(
                $app['em'],
                $app['em']->getClassMetaData(DomainModel\user\User::class)
            );
        });
    }

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

このようにDIすることでUserGetService.phpでは、DBや、DoctorineRepositoryの実装を意識せずに
ビジネスロジックを書いていくことができます。

packages/service/UserGetService.php
<?php
namespace packages\service;
use packages\domain\model\User\UserId;
use packages\domain\model\User\User;
use packages\domain\model\User\UserRepository;
class UserGetService 
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function getUser(UserId $userId): User
    {
        return $this->userRepository->findUser($userId);
    }
    public function getUserList(): array
    {
        return $this->userRepository->findAllUsers();
    }
}

ここまでできたら、あとは通常のLaravel開発と同様の手順通り。
app/Http/Controllers/Sample.php
等にコントローラを設置し、 UserGetService を呼び出し、Viewに変換していく。
というながれになりますね。

言い訳とかとか

  • 色々説明足りないのではないか
    この記事ではあくまでXMLマッピングについての言及を主とするので細かなことは省きます。
  • embeddableばかりに焦点あてて、doctrine本来のマッピングの例としては弱くないか
    マッピングの仕方としてont-to-oneやone-to-manyなどなどの関連付けについては別途記事を起こそうかと思ってます。
    が、このあたりはドメイン駆動開発をするにあたって個人的にあまり好きではない部分なのでかなり偏った記事になる気がする。

最後に

以上で、LaravelDoctorineのXMLマッピングを利用するまでの手順が完了します。
この記事内で触れられなかった内容は別途記事を起こし、本記事にリンク貼ってこうと思います。

質問や、ここ間違ってるよーとかいけてないよーとかって部分がありましたら是非コメントお願いいたします。

誰かにとっての何かの助けになりますと幸いです。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?