デザインワン・ジャパン Tech Blog

DesignOne Japan | Activate the World.

【PHP 8.1】とうとうPHPにもEnumがやってきた

概要説明

こんにちは!株式会社デザインワン・ジャパンで口コミサービス エキテンのリニューアルを担当しているサービス開発部の鈴木セシル(@suzuki_cecil_)です。

弊社が運用しているエキテンのバックエンドはほぼPHPで実装しております。そんなPHPに関して、先日メジャーリリースされたバージョン8.1からPHPでもEnumが利用できるようになりました 🎉

(私も含め、長い間Enumを待ち望んでいたPHPerの方々もいらっしゃることでしょう笑)

なので今回の記事ではPHP8.1のEnumに関して紹介したいと思います。

そもそもEnumとは何か?

PHP8.1の 公式ドキュメント では列挙型(Enum)に関して以下のように説明されています。

列挙型(Enumerations) または Enum を使うと、 開発者は取りうる値を限定した独自の型を定義できます。 これによって、"不正な状態を表現できなくなる" ので、 ドメインモデルを定義する時に特に役立ちます。

例えば都道府県モデルを実装したいとします。少なくとも2022年現在において日本には47の都道府県が存在するため、モデルでも47個の都道府県のうちいずれか1つの都道府県を持てるようにする、逆に言えば47個の都道府県以外を持てないようにする必要があります。

それぞれの都道府県に1~47の整数値を割り振りint型で取り扱ったとして、int型では48以上の整数値や0, マイナスといった値も取り扱えるため「47個の都道府県以外を持てないようにする」という要件を満たすことはできません。

そこで、Enumを活用すると開発者は取りうる値を限定した独自の型を定義することが可能なため、47個の都道府県のみをとりうる都道府県型を定義することが可能になります。

Enumの登場でどう変わるか

Enumの登場でどう変わるかを知るためにクラス(擬似的なEnum)による実装とEnumによる実装を比較してみたいと思います。

クラス(擬似的なEnum)による実装

今まではPHPEnumを利用したいと思ってもPHPには備わっていないため、 laravel-enum などの非公式のライブラリを利用したり自前で以下のようなクラスで擬似的にEnumを実装してきたのではないでしょうか。

<?php
class Prefecture
{
    private const HOKKAIDO = 1;
    private const AOMORI = 2;
    private const IWATE = 3;
    // 省略
    private const KAGOSHIMA = 46;
    private const OKINAWA = 47;

    private const NAMES = [
        self::HOKKAIDO => "北海道",
        self::AOMORI => "青森県",
        self::IWATE => "岩手県",
        // 省略
        self::KAGOSHIMA => "鹿児島県",
        self::OKINAWA => "沖縄県",
    ];

    private int $value;

    private function __construct(int $value)
    {
        $this->value = $value;
    }

    public static function hokkaido(): self
    {
        return new self(self::HOKKAIDO);
    }

    public static function aomori(): self
    {
        return new self(self::AOMORI);
    }

    public static function iwate(): self
    {
        return new self(self::IWATE);
    }

    // 省略

    public static function kagoshima(): self
    {
        return new self(self::KAGOSHIMA);
    }

    public static function okinawa(): self
    {
        return new self(self::OKINAWA);
    }

    public function name(): string
    {
        return self::NAMES[$this->value];
    }
}

var_dump(Prefecture::hokkaido()->name());
$ php Prefecture.php 
string(9) "北海道"

このクラスでも「47個の都道府県以外を持てないようにする」という要件を満たすことは可能です。しかし、いささかコード量が多いように思えます。

Enumによる実装

PHP8.1よりEnumが利用できるようになったため上記とほぼ同義のモデルを以下のコードで表現することが可能になりました。

<?php
enum Prefecture: string
{
    case HOKKAIDO = "北海道";
    case AOMORI = "青森県";
    case IWATE = "岩手県";
    // 省略
    case KAGOSHIMA = "鹿児島県";
    case OKINAWA = "沖縄県";
}

var_dump(Prefecture::HOKKAIDO->name);
var_dump(Prefecture::HOKKAIDO->value);
$ php Prefecture.php 
string(8) "HOKKAIDO"
string(9) "北海道"

コード量がかなり少なく非常に可読性の高いモデルに出来たのではないでしょうか。

Enumの仕様

前述の説明だけではEnumの細かい仕様まで説明できていないので、Enumの仕様に関して言及したいと思います。

cases

Enumは内部的にUnitEnumインターフェースを実装しています。以下のソースコードはCore_c.phpより引用したUnitEnumインターフェースです。

<?php
/**
 * @since 8.1
 */
interface UnitEnum
{
    public string $name;

    /**
     * @return static[]
     */
    public static function cases(): array;
}

UnitEnumインターフェースではcasesメソッドが定義されていることがわかります。

casesメソッドはEnumに定義されている全てのcaseを要素に持つ配列を返すメソッドです。配列の要素は宣言された順に含まれます。

Pure Enum, Backed Enum

デフォルトのEnumではスカラー値の情報を持っていません。しかし先ほどのPrefectureのようにそれぞれのcaseごとにスカラー値を持たせないユースケースは多くあると思います。

スカラー値の情報を持たないEnumPure Enumスカラー値の情報を持つEnumBacked Enum といいます。またPure Enumのcaseを Pure Case、Backed EnumのcaseをBacked Caseといいます。Pure EnumにはPure Caseのみ、Backed EnumにはBacked Enumのみを含めるため1つのEnumがPure CaseおよびBacked Caseを持つということは不可能です。

<?php
enum Pure
{
    case A;
    case B;
    case C;
}
enum Backed: int
{
    case A = 1;
    case B = 2;
    case C = 3;
}
var_dump(Pure::A);
var_dump(Backed::A);
$ php PureAndBacked.php 
enum(Pure::A)
enum(Backed::A)

Backed Enumが持てるスカラー値にはいくつかの制約がつきます。

  • intまたはstringの値のみを持つことができる(bool, floatは不可)
  • 単一の型のみを持つことができる(Union型は不可)
  • Backed Caseの値は全てユニークでなければならない
  • スカラー値はリテラルリテラルを表す式でなければならない(定数はサポートしていない)

from, tryFrom

Backed Enumは内部的にUnitEnumを継承したBackedEnumインターフェースを実装しています。以下のソースコードはCore_c.phpより引用したBackedEnumインターフェースです。

<?php
/**
 * @since 8.1
 */
interface BackedEnum extends UnitEnum
{
    public string $value;

    /**
     * @param int|string $value
     * @return static
     */
    public static function from(int|string $value): static;

    /**
     * @param int|string $value
     * @return static|null
     */
    public static function tryFrom(int|string $value): ?static;
}

BackedEnumインターフェースではfromメソッドとtryFromメソッドが定義されていることがわかります。

fromメソッド、tryFromメソッドともにスカラー値からBacked Caseを返すメソッドですが、対応するBacked Caseがない場合fromメソッドはValueErrorがスローされるのに対し、tryFromメソッドはNullを返します。

継承、インターフェース

Enumはインターフェースを実装することが可能ですが、EnumおよびClassを継承することはできません。

メソッド、staticメソッドの定義

EnumはClass同様にメソッド、 staticメソッドを定義することが可能です。メソッドのスコープもClass同様にpublic、protected、privateから選択することが可能です。ただし前述の通りEnumを継承することはできないため、protectedとprivateはほぼ同義です。

トレイト

EnumはClass同様にトレイトをuseすることが可能です。useされるトレイトにはメソッドとstaticメソッドだけを含めることが可能です(プロパティを持つトレイトをuseするとFatal Errorが発生します)

感想

Enumで実装可能なことはClassでも実装可能であるため特にドメインモデルの実装の幅が広がるということはありません。

ただしコード量をかなり抑えることができ可読性も向上することから現在携わっているリニューアルプロジェクトにもどんどん導入していきたいと思いました。

おわりに

仲間を募集しております

募集中の職種については以下を御覧ください。

www.wantedly.com