【PHPフレームワークFlow】Enum型をdoctrineで扱う方法

# 初めに
PHPフレームワークFlowは、DoctrineというORMを搭載しています。DoctrineはデフォルトではEnum型を扱うことができず、そのためには一工夫する必要があります。
今回はPHPフレームワークFlowにおける、DoctrineでEnum型を扱う方法を解説します。


# Enum型とは
別名「列挙型」。PHP8.1から追加された機能です。


https://www.php.net/manual/ja/language.types.enumerations.php

こんな感じで、取りうる値を列挙することができます。
列挙した値以外は入れることができないため、予期せぬ値が入る余地をなくすことができます。
```php
enum Status: string
{
    case STATUS_VISIBLE = 'visible';
    case STATUS_INVISIBLE = 'invisible';
}
```

# DoctrineでEnum型を扱いたい
そんなEnumですが、Doctrineでは一工夫しないと使うことができません。

## なぜ扱えないのか
Docrineは、「ドメインモデルを元に自動でテーブルを作成することができる」という素晴らしい機能を有しています。ドメインモデルで定義されたstring型やint型の変数は、接続するDBの種類に合わせて自動で型変換されます。

しかし、ユーザが定義したEnum型の場合そうはいきません。TBL作成の際、Doctrineが**どの型にマッピングすればいいのかが分からず**、エラーが起きてしまうのです。

## 解決策
これを解決するために、[Doctrineの公式ドキュメント](https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/cookbook/mysql-enums.html)には2つの方法が記載されています。

* 案1：**string型やint型などで代用する**
* 案2：**enum型とDBの型の対応をDoctrineに読み込ませる**

それぞれ解説していきます。

### 案1：string型やint型などで代用する
enumの値をstring型やint型などで代用する方法です。
columnDefinitionでENUM型を定義することで、DB上はENUM型で扱うことができます。

```php
<?php
#[Entity]
class Article
{
    #[Column(type: "string", columnDefinition: "ENUM('visible', 'invisible')")]
    private $status;
}
```

しかし、この実装には問題があります。
それは、**DBに合わせてドメインモデルの型を変更している**という点です。
DBを意識せずにドメインモデルを作成できるのがORMのいいところですが、その良さが完全に失われてしまいます。

### 案2：enum型とDBの型の対応をDoctrineに読み込ませる（推奨！）
こちらは、PHPのenumを対応を型に読み込ませるというものです。
具体的な方法は後述しますが、案1と違いドメインモデルの変数をEnum型にすることができるため、DBを気にしないで実装することができます。

```php
<?php
#[Entity]
class Article
{
    // 一工夫入れることで、Enum型のまま扱うことができる！
    #[Column(type: "enumvisibility")]
    private $status;
}
```

# 試してみる
ということで、実際に案2を試してみましょう。
今回はFlowにおけるEnum型を扱う方法のため、純粋なDoctrineの作法とは少し異なる箇所もございますが、ご了承ください。

プロジェクト構成は以下です。

```
Quickstart
    └ Packages/
         ├ Application/
         |    └ Neos.Welcome/
         |         ├ Classes/
         |         |    ├ Domain/
         |         |    |    └ Model
         |         |    |         └ EnumTest.php（★）
         |         |    ├ Enum
         |         |    |    └ StatusEnum.php（★）
         |         |    └ Type
         |         |         └ StatusEnumType.php（★）
         |         |
         |         └ Configuration
         |              └ Settings.yaml（★）
         ├ Framework/
         └ Libraries/
```

## Enum
まずはPHPで扱うEnum型を定義しました。

```php:StatusEnum.php
<?php
namespace Neos\Welcome\Enum;

enum StatusEnum: string
{
    case STATUS_VISIBLE = 'visible';
    case STATUS_INVISIBLE = 'invisible';
}
```
## Type
続いて、PHPのEnum型とDBの型のマッピングを行うための処理を作成します。
Doctrineでは、Typeというクラスを継承することでこの仕組みを実現しています。

:::note info
Doctrineは内部で型を持っており、型は全てこのTypeクラスを継承して作成されています。この型は、PHPの型とDBの型の両方を有しており、変換の際は間に入ってくれるというようなイメージです。
:::


今回は以下のようなstatus_enumという型を作成しました。

```php:StatusEnumType.php
<?php
namespace Neos\Welcome\Type;

use Composer\XdebugHandler\Status;
use Neos\Flow\Annotations as Flow;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Neos\Welcome\Enum\StatusEnum;

class StatusEnumType extends Type
{
    const STATUS_ENUM = 'status_enum';

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return "ENUM('visible', 'invisible')";
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return StatusEnum::tryFrom($value);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof StatusEnum) {
            return $value->value;
        }
        throw new \InvalidArgumentException('Invalid StatusEnum');
    }

    public function getName()
    {
        return self::STATUS_ENUM;
    }
}
```
各メソッドの役割は以下です。

|メソッド|役割|
|--|--|
|getSQLDeclaration|カラム作成時の型の指定方法を文字列で返す|
|convertToPHPValue|DB⇒PHPの型に変換するときの処理|
|convertToDatabaseValue|PHP⇒DBの型に変換する時の処理|
|getName|Doctrineの型の名前|

## Settings.yaml

Settings.yamlには先ほど作成した```status_enum```がPHPとDBそれぞれどの型と対応するのかを定義します。

```yaml:Settings.yaml
Neos:
  Flow:
    persistence:
      doctrine:
        dbal:
          mappingTypes:
            status_enum:
              className: Neos\Welcome\Type\StatusEnumType
              dbType: enum
```

|設定キー|詳細|
|--|--|
|className|PHPの型のクラス名|
|dbType|DBの型|

## Entity
最後に、enumの型を用いたドメインモデルを作成して、準備完了です。

```php:EnumTest.php
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;
use Neos\Welcome\Enum\StatusEnum;

/**
 * @Flow\Entity
 * @ORM\Table(name="enum_test")
 */
class EnumTest {

    /**
     * @ORM\Column(type="status_enum")
     * @var StatusEnum
     */
    protected $status;

}
```

## 実行
ということで実際にTBLを作成してみましょう。

```sh
$ ./flow doctrine:update
Executed a database schema update.
```
問題なく実行されました！

実際に作成されたTBLがこちらです。
statusがEnum型になっており、`visible`と`invisible`が定義されているのが分かります。
```sh
mysql> show create table enum_test\G
*************************** 1. row ***************************
       Table: enum_test
Create Table: CREATE TABLE `enum_test` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `status` enum('visible','invisible') 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)

mysql>
```


# 終わりに
今回はFlowのDoctrineでEnum型を扱う方法について書きました。
うまくやればORMの良さを活かしながらEnumを扱うことができそうですね。
実際にデータが入ることも次回確認していこうと思います。

ここまで読んでいただきありがとうございました！