【PHPフレームワークFlow】ドメインモデルを元にTBLを作成する

# 初めに
PHPフレームワークFlowはドメイン駆動設計を用いた実装をするために必要な機能を多く有しており、その中の一つに『ドメインモデルを元に**TBLが自動生成される**』というものがあります。今回はこの機能について基本的な使い方を解説していきます。

# Flowでドメインモデリング
Flowはドメイン駆動設計（DDD）の概念を用いた実装を強く推奨しているフレームワークです。そのため、開発者がDDDを用いた実装がしやすくなるような機能を多数提供しています。

* **ドメインモデルを格納するためのディレクトリ**が最初から用意されている
* **ドメイン知識を表現するためのアノテーション**が用意されている
* ドメインモデルを元に**TBLが自動生成される**
* 自動生成されたTBLへの**CRUDメソッド**が用意されている
* AOPやSignal&Slotなどの**イベント駆動型プログラミング**が実装できる

これらの機能により、以下のような実装が可能になります。
* **記述量の少ないドメインモデルを作成できる**
* **DB, TBLの存在を意識せずに実装できる**
* **単純なCRUDであれば、SQLを書く必要がない**
* **複数集約間の整合性をイベント管理で一元的に実装できる**

これらの機能により実装時間やバグの数を減らし、浮いた時間をドメインエキスパートとの会話やドメインモデリングに費やすことができるというわけですね！


# ドメインモデルを作成してみる
ということで実際に作ってみましょう。
前提として、Flowは以下のディレクトリにドメインモデルを作成するルールがあります。

```
Quickstart
    └ Packages/
         ├ Application/
         |    └ Neos.Welcome/
         |         └ Classes/
         |              └ Domain/
         |                   └ Model
         |                        └ ★ここに作成！
         ├ Framework/
         └ Libraries/
```

例として、ECサイトの店舗と商品の関係を実装してみます。
以下のような関係です。

![Store.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/a761f352-f8a3-88d0-589b-bd35c900fa7e.png)




## 1. エンティティを作る

ということで、まずはエンティティを作成してみましょう。
まずは店舗エンティティを作成します。

```php
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 */
class Store
{

    /**
     * @var string
     */
    protected $id;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $address;

    // constructor

    // getters
}
```

同様に商品エンティティも作成します。
後程紐づけるので、型は一旦プリミティブにしておきます。

```php
<?php
namespace Neos\Welcome\Domain\Model;

/*
 * This file is part of the Neos.Welcome package.
 */

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 */
class Item
{

    /**
     * @var string
     */
    protected $id;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $store;

    /**
     * @var int
     */
    protected $price;

    // constructor

    // getters
}

```

### ポイント
* **`@Flow\Entity`をクラスに付与することでエンティティを作成できる！**


## 2. 複数エンティティを紐づける

続いて、商品と店舗を紐づけてみましょう。
商品エンティティが持つstoreを店舗エンティティと紐づけます。


```php
/**
 * @Flow\Entity
 */
class Item
{

    /**
     * @ORM\ManyToOne
     * @var Store
     */
    protected $store;
    
    // othres

    // constructor

    // getters

}
```


:::note info
今回はやりませんが、店舗エンティティ側で商品の一覧を持ちたい場合は以下のように表現します。
どのエンティティのとのプロパティと紐づけるかを`targetEntity`と`mappedBy`で指定することで紐づけを行います。

```php
/**
 * @Flow\Entity
 */
class Store
{
    /**
     * @ORM\OneToMany(targetEntity="Neos\Welcome\Domain\Model\Item", mappedBy="store")
     * @var \Doctrine\Common\Collections\ArrayCollection<Neos\Welcome\Domain\Model\Item>
     */
    protected $items;
}

```
:::




### ポイント
* **`@ORM\ManyToOne`や`@ORM\OneToMany`を付与することで、リレーションを表現できる！**


## 3. 値オブジェクトを作る
価格の値オブジェクトを作成します。
`@Flow\ValueObject`を付与することで、値オブジェクトを作成できます。


```php
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\ValueObject
 */
class Price {
    /**
     * @var int
     */
    protected int $price;

    // コンストラクタがないとエラーになる
    public function __construct(int $price)
    {
        $this->price = $price;
    }

}
```

:::note info
値オブジェクトはイミュータブルなオブジェクトです。そのため、コンストラクタがなかったり、setterを作成するとコンパイル時にエラーになります。
:::



作成後、以下のようにItemエンティティのプロパティの型に指定すればOKです。

```php
class Item
{
    /**
     * @var Price
     */
    protected $price;

    // othres
}
```

### ポイント
* **`@Flow\ValueObject`アノテーションを付与することで値オブジェクトを作成することができる！**


## 4. TBLを作成する
続いて、作成したドメインモデルのデータをDBに保存するためのTBLを作成してみましょう。

### TBL情報をエンティティに付与
まずは、各エンティティにアノテーションを付与します。

* クラスに`@ORM\Table(name=~~)`アノテーションを付与
* プロパティに`@ORM\Column`アノテーションを付与

商品エンティティはこんな感じにしました。

```php
<?php
namespace Neos\Welcome\Domain\Model;

/*
 * This file is part of the Neos.Welcome package.
 */

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 * @ORM\Table(name="item")
 */
class Item
{

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column()
     * @ORM\ManyToOne
     * @var Store
     */
    protected $store;

    /**
     * @ORM\Column()
     * @var Price
     */
    protected $price;

    // constructor

    // getters
}

```

店舗エンティティは以下です。

```php
/**
 * @Flow\Entity
 * @ORM\Table(name="store")
 */
class Store
{

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(type="string")
     * @var string
     */
    protected $address;

    // constructor

    // getters
}
```

:::note info
`@ORM\Table(name=~~)`と`@ORM\Column()`は非必須です。無くてもTBL作成はできます。
テーブル名やカラムの制約などをつける際には必要です。
:::


### flowコマンドでTBL作成
各エンティティの準備ができたら、以下のコマンドでTBLを作成します。

```shell
$ ./flow doctrine:update
```

商品エンティティと店舗エンティティに対応するテーブルが作成できました。

```shell:item
mysql> show create table item\G
*************************** 1. row ***************************
       Table: item
Create Table: CREATE TABLE `item` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `store` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `priceprice` int NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`),
  KEY `IDX_1F1B251EFF575877` (`store`),
  CONSTRAINT `FK_1F1B251EFF575877` FOREIGN KEY (`store`) REFERENCES `store` (`persistence_object_identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

```

```shell:store
mysql> show create table store\G
*************************** 1. row ***************************
       Table: store
Create Table: CREATE TABLE `store` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `address` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

```
itemテーブルを見ると、storeが外部キーになっていることが分かります。**`@ORM\ManyToOne`をつけたプロパティは自動的に外部キーに設定される**ようですね。

また、itemテーブルにpricepriceというカラムがありますが、これは価格の値オブジェクトです。分かりづらいですが、前半のpriceは商品エンティティで定義しているプロパティ名で、後半のpriceはPrice値オブジェクトで定義しているプロパティになります。
このように、値オブジェクトのプロパティは参照元のテーブルに埋め込まれるようです。

:::note info
値オブジェクトが参照されている場合、プロパティを親TBLに埋め込むかどうかは設定で変更できます。
以下のように、設定することで、値オブジェクトを埋め込まない設定にすることも可能です。
その場合、以下のような作りで値オブジェクトを参照します。
* IDのカラムを持つ値オブジェクトのTBLが作成される
* 参照元TBLから値オブジェクトTBLのIDを保存

```php
/**
 * @Flow\ValueObject(embedded=false)
 */
```

:::


### ポイント
* **`@ORM\Table(name=~~)`でテーブル名を指定**
* **カラムには`@ORM\Column()`を付与**
* **`@ORM\ManyToOne`を付与したプロパティは外部キーになる**
* **値オブジェクトのプロパティが親TBLのカラムに作成される（デフォルトの場合）**


# 終わりに
今回は基本的な作成の仕方を解説しました。次回は応用編を書くつもりなので、ぜひご覧ください！