thumbnail

【PHP8.1】列挙型(Enum)について徹底解説

PHPの列挙型について解説します。PHPの列挙型はメソッドを持つことが出来たり、インターフェースの実装、トレイトの利用も可能で様々な表現ができます。本記事は公式ドキュメントをもとに解説しますが、要点のみを抑えて簡潔な表現を心がけたり、具体例を増やすことによって分かりやすさに重点を置いています。

列挙型の概要

列挙型は、開発者が独自に定義した値のみを取る型です。enumで宣言します。
以下に具体例を挙げます。

<?php
enum Size
{
    case Small;
    case Medium;
    case Large;
}

Sizeというenumを宣言しました。
このときSizeは、caseで指定した3つの値(Small, Medium, Large)のみを取ります。
caseは0個以上、好きなだけ定義することが可能です。

次のようにして::演算子でアクセスします。

$a = Size::Small;
$b = Size::Large;

これが一体何の役に立つかというと、例えば、関数の引数の値を限定できて型安全になります。
次のコードはSizeで定義された3つのcaseのみを受け取ることが保証されます。

function hoge (Size $size)
{
    var_dump($size);
}

hoge(Size::Small); //OK
//hoge(3); //NG

概要を押さえたところで、PHPの列挙型について詳しく取り上げていきます。

(参考) 列挙型はPHP8.1からの新機能です。

PHP8.1には他にも多くの新機能があります。

列挙型のインスタンス

列挙型はクラスに似ています。::演算子で参照されたcase値は、列挙型のインスタンスとなります。

$a = Size::Small;
var_dump($a instanceof Size); //bool(true)

また、このインスタンスはシングルトンです。

$a = Size::Small;
$b = Size::Small;
var_dump($a === $b); //bool(true)

それぞれのインスタンスは読み取り専用のnameプロパティを持ちます。caseで定義した名前そのものを返します。

$a = Size::Small;
$b = Size::Medium;
var_dump($a->name); //string(5) "Small"
var_dump($b->name); //string(6) "Medium"

Pure EnumとBacked Enum

列挙型にはPure EnumとBacked Enumがあります。

前章のように定義された列挙型をPure Enumと言います。
再掲します。

enum Size
{
    case Small;
    case Medium;
    case Large;
}

もう一方で、次のように定義したEnumをBacked Enumと言います。

enum Size: string
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
}

その特徴は、case式が右辺にスカラー値を持たせることが可能なことです。
int型かstring型のみを持つことができ、その型を列挙型名の後ろの:の後に記述します。
ただし、スカラー値はすべてユニークにする必要があります。

valueプロパティを使うことで、定義したスカラー値を参照することが可能です。

$a = Size::Small;
$b = Size::Medium;
var_dump($a->value); //string(1) "s"
var_dump($b->value); //string(1) "m"

また、fromメソッドを用いることでスカラー値からcase名を取得することも可能です。
これには2つのメソッドがあり、該当case名がない場合にエラーを投げる from()メソッドと、null値を返すtryFrom()があります。

$a = Size::from("s");
var_dump($a); //enum(Size::Small)

$b = Size::tryFrom("hoge") ?? Size::Large;
var_dump($b); //enum(Size::Large)

列挙型とメソッド

列挙型はメソッドを持つことが可能で、インスタンスからアクセスできます。

<?php
enum Size: string
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
    
    //$thisで自身のcaseを返す
    public function thisCase(): Size
    {
        return $this;
    }
    
    //どのcaseでも一律に文字列を返す
    public function hoge(): string
    {
        return "hoge";
    }
}

$a = Size::Small;
$b = Size::Large;
var_dump($a->thisCase()); //enum(Size::Small)
var_dump($b->hoge()); //string(4) "hoge"

ここで、メソッドにpublic修飾子をつけていることからわかるように、アクセス修飾子の指定が可能です。
しかし、列挙型は継承できないため、protectedもprivateも同じものとなります。

メソッド内の$thisで自身のcaseを返すので、例えばmatch式と組み合わせてcaseによって条件分岐するメソッドの実装も可能です。

<?php

enum Size: string
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
    
    //$thisで自身のcaseを返す
    public function price(): int
    {
        return match($this) {
            Size::Small => 10,
            Size::Medium => 100,
            Size::Large => 1000
            
        };
    }
}


$a = Size::Small;
$b = Size::Large;
var_dump($a->price()); //int(10)
var_dump($b->price()); //int(1000)

上の例ではBacked Enumにメソッドを定義しましたが、Pure Enumでも同様のことが可能です。

列挙型とinterface

列挙型はinterfaceを実装することも可能です。

<?php

interface ISize
{
    public function price(): int;
}

enum Size: string implements ISize
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
    
    //$thisで自身のcaseを返す
    public function price(): int
    {
        return match($this) {
            Size::Small => 10,
            Size::Medium => 100,
            Size::Large => 1000
            
        };
    }
}

$a = Size::Small;

function hoge(ISize $size): int
{
    return $size->price();
}

var_dump(hoge($a)); //int(10)

Sizeはインターフェースを実装しており、関数hogeではポリモーフィズムがきちんと働いています。

列挙型とstaticメソッド

staticメソッドの定義もできます。
列挙型ではコンストラクタが使えないため、代用としてstaticメソッドが使えます、

<?php

enum Size: string
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
    
    public static function fromPrice(int $price): static
    {
        return match(true) {
            $price < 10 => static::Small,
            $price < 100 => static::Medium,
            default => statoc.Large
        };
    }
}

var_dump(Size::fromPrice(50)); //enum(Size::Medium)

列挙型と定数

constで定数の定義も可能です。自身のcaseも代入可能です。

<?php

enum Size: string
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
    
    public const Hoge = "hoge";
    public const Fuga = self::Small;
}

var_dump(Size::Hoge); //string(4) "hoge"
var_dump(Size::Fuga); //enum(Size::Small)

列挙型とトレイト

トレイトの利用も可能です。
ただし列挙型でuseされるトレイトは、プロパティを含めるとエラーが出るので注意が必要です。

<?php

interface ISize
{
    public function price(): int;
}

trait SizeTrait
{
    public function price(): int
    {
        return match($this) {
            Size::Small => 10,
            Size::Medium => 100,
            default => 1000
        };
    }
}


enum Size: string
{
    use SizeTrait;
    
    case Small = "s";
    case Medium = "m";
    case Large = "l";
}

$a = Size::Small;
var_dump($a->price()); //int(10)

クラスとの違い

今まで何度か出てきたものを含めて、クラスとの違いについて改めてまとめます。

  • コンストラクタ、デストラクタは禁止
  • 継承は無い。よってprotectedもprivateも同じ。
  • staticまたはプロパティは禁止
  • シングルトンなのでcloneできない
  • マジックメソッドは一部のみ利用可能(__call, __callStatic, __invoke)
  • newでインスタンス化できない

値のリスト

cases()メソッドでcaseのリストを取得できます。

enum Size: string 
{
    case Small = "s";
    case Medium = "m";
    case Large = "l";
}

var_dump(Size::cases()); 
/**
 * array(3) {
 * [0]=>
 *enum(Size::Small)
 * [1]=>
 * enum(Size::Medium)
 * [2]=>
 * enum(Size::Large)
 *}
 */

応用例

最後に、公式ドキュメントで紹介されている応用例を紹介します。

<?php
enum UserStatus: string
{
    case Pending = 'P';
    case Active = 'A';
    case Suspended = 'S';
    case CanceledByUser = 'C';

    public function label(): string
    {
        return match($this) {
            static::Pending => 'Pending',
            static::Active => 'Active',
            static::Suspended => 'Suspended',
            static::CanceledByUser => 'Canceled by user',
        };
    }
}

foreach (UserStatus::cases() as $case) {
    printf('<option value="%s">%s</option>\n', $case->value, $case->label());
}

/**
*<option value="P">Pending</option>
*<option value="A">Active</option>
*<option value="S">Suspended</option>
*<option value="C">Canceled by user</option>
* 
*/

UserStatusは4つの状態のうち1つに限定されます。
またすべてのインスタンスはlabelメソッドを持つので、例のforeach文のようにループを回せます。

まとめ

PHPにおける列挙型とその使い方について解説しました。基本的な解説に留めましたが、データベース操作などと組み合わせると役に立ちそうです。