🌟

[PHP]Enum型を使ってますか?

2023/01/10に公開

はじめに

PHP8.2が2022/12月にリリースされました。実行環境が対応していない等、本番環境で利用されている方はまだ少数かと思います。一方、PHP8.1はリリースされてから1年以上を経て、新規開発で利用する方や既存システムをバージョンアップした方など、すでに多くの方に浸透しているのではないでしょうか?

PHP8.1は興味深い機能が盛りだくさんなのですが、今回は、その中でEnum型について取り上げます。

Enum型とは

Enum型とは列挙型とも呼ばれ、列挙型という名前が示す通り、とりうる値を列挙した型のことです。ここで型とはクラスを指します。他の言語ではおなじみの機構で、個人的にはPHPでも非常に待ち望んでいた機能の1つです。

コード例

PHPのEnum型は以下のように記述します。enumキーワードに列挙名が続き、caseキーワードで各列挙を定義します。以下は3色を表す列挙型を定義する例です。

<?php
namespace App\Example;

/**
  色を表す列挙です。
 */
enum Color {
    /**
      赤色。
     */    
    case Red;
    /**
      青色。
     */
    case Blue;
    /**
      黄色。
     */
    case Yellow;
}

ここで重要なことは、列挙型であるColorの取りうる値は、RedBlueYellowの3つだけだということです。

この列挙型を使って、信号機を表すクラスを実装してみます。
(この例では、コンストラクタのプロモーションと呼ばれる、PHP8.0で導入されたコンストラクタの書き方をしています。ご存じでない場合はドキュメントをご参照ください。)

<?php
namespace App\Example;

/**
  信号機を表すクラスです。
 */
class TrafficLight
{
    public function __construct(private Color $current)
    {
    }

    /**
      信号機の色を変更します。
      @param Color $color 色
     */
    public function changeTo(Color $color)
    {
        $this->current = $color;
    }

    /**
      信号機の現在の色を返します。
      @return Color 現在の色
     */
    public function color(): Color
    {
        return $this->current;
    }
}

信号機クラス(TrafficLight)のコンストラクタやchangeToメソッドの引数は列挙型Colorとして型宣言されています。これにより、これらのメソッドにColor以外の値を指定できないことが保証されます。

信号機クラスと色列挙の使い方を示すために、PHPUnitを使ったテストコードを見てみましょう。

<?php
namespace App\Example;

use App\Example\Color;
use App\Example\TrafficLight;
use PHPUnit\Framework\TestCase;

class TrafficLightTest extends TestCase
{
    /**
      @test
     */
    public function 信号機を生成できる()
    {
        $sut = new TrafficLight(Color::Blue);
        $this->assertEquals($sut->color(), Color::Blue);
    }

    /**
      @test
     */
    public function 信号機の色を変更できる()
    {
        $sut = new TrafficLight(Color::Blue);
        $sut->changeTo(Color::Yellow);
        $this->assertEquals($sut->color(), Color::Yellow);
    }
}

上記の例の通り、列挙型の各値はColor::Blueのように参照できます。
このテストを実行すると、以下のように正常にテストが終了します。

$ ./vendor/bin/phpunit --filter=TrafficLightTest --colors --testdox
PHPUnit 9.5.27 by Sebastian Bergmann and contributors.

Traffic Light (App\Example\TrafficLight)
 ✔ 信号機を生成できる
 ✔ 信号機の色を変更できる

Time: 00:00.056, Memory: 8.00 MB

OK (2 tests, 2 assertions)

では、上記のテストクラスを以下のように書き換えてみます。これは間違ったコード例です。どこが間違っているかわかりますか?

<?php
namespace App\Example;

use App\Example\Color;
use App\Example\TrafficLight;
use PHPUnit\Framework\TestCase;

class TrafficLightTest extends TestCase
{
    /**
      @test
     */
    public function 信号機を生成できる()
    {
        $sut = new TrafficLight(Color::Green);
        $this->assertEquals($sut->color(), Color::Green);
    }

    /**
      @test
     */
    public function 信号機の色を変更できる()
    {
        $sut = new TrafficLight(Color::Blue);
        $sut->changeTo('Yellow');
        $this->assertEquals($sut->color(), Color::Yellow);
    }
}

このテストクラスを実行すると失敗してエラーメッセージが表示されます。

$ ./vendor/bin/phpunit --filter=TrafficLightTest --colors --testdox
PHPUnit 9.5.27 by Sebastian Bergmann and contributors.

Traffic Light (App\Example\TrafficLight)
 ✘ 信号機を生成できる
   ┐
   ├ Error: Undefined constant App\Example\Color::Green
   │
   ╵ /var/task/tests/Unit/App/Example/TrafficLightTest.php:16
   ┴

 ✘ 信号機の色を変更できる
   ┐
   ├ TypeError: App\Example\TrafficLight::changeTo(): Argument #1 ($color) must be of type App\Example\Color, string given, called in /var/task/tests/Unit/App/Example/TrafficLightTest.php on line 26
   │
   ╵ /var/task/app/Example/TrafficLight.php:18
   ╵ /var/task/tests/Unit/App/Example/TrafficLightTest.php:26
   ┴

Time: 00:00.101, Memory: 10.00 MB

ERRORS!
Tests: 2, Assertions: 0, Errors: 2.

エラーメッセージが示すように、以下の2点がエラーとなっていました。

  • 1つ目のテスト(信号機を生成できる)で使った、Color::Greenという色はないこと
  • 2つ目のテスト(信号機の色を変更できる)で文字列を色として指定したこと

Enum型の嬉しいところ

Enum型を利用することにより、事前に既知の少数の値からなる型を定義することができるようになりました。これにより、上記のコード例で見た通り、可読性が高く、より型安全なプログラムを記述できるようになりました。(PHPは動的型付け言語ですので、実際には実行時に型エラーを検出してくれます。Javaなどの静的型付けの言語では、プログラムの実行前、コンパイル時に静的にエラーを検出します)

PHP8.0以前はどうしていたか?

私のような他言語でEnum型を享受していたプログラマは、PHPでもEnum(もどき?)を利用するためにいろいろな工夫をする必要がありました。例えば、1つの例を以下に示してみます。
(1つ補足しておくと、このようなオレオレEnumもどきを実装する他にも、第三者が提供しているパッケージを利用する方法もありました。たとえば、こんなパッケージ

<?php

namespace App\Example;

/**
  色を表す列挙です。
 */
class Color
{
    private function __construct(private string $code)
    {
    }

    /**
      赤色です。
      @return Color 色
     */
    public static function Red(): Color
    {
        return new static('red');
    }

    /**
      青色です。
      @return Color 色
     */
    public static function Blue(): Color
    {
        return new static('blue');
    }

    /**
      黄色です。
      @return Color 色
     */
    public static function Yellow(): Color
    {
        return new static('yellow');
    }
}

この例では、クラスのstaticメソッドで列挙型を表しています。この列挙型もどきのColorを使った信号機クラスは列挙型を使ったバージョンと何も変わりません。変わるのはその使い方のみです。これをテストコードで見てみましょう。

<?php

namespace App\Example;

use App\Example\Color;
use App\Example\TrafficLight;
use PHPUnit\Framework\TestCase;

class TrafficLightTest extends TestCase
{
    /**
      @test
     */
    public function 信号機を生成できる()
    {
        $sut = new TrafficLight(Color::Blue());
        $this->assertEquals($sut->color(), Color::Blue());
    }

    /**
      @test
     */
    public function 信号機の色を変更できる()
    {
        $sut = new TrafficLight(Color::Blue());
        $sut->changeTo(Color::Yellow());
        $this->assertEquals($sut->color(), Color::Yellow());
    }
}

このように、列挙型であればColor::Blueだったところが、Color::Blue()のようにstaticなメソッド呼び出しになっています。

このように、PHP8.0以前では、自前で列挙型もどきのクラスを定義する必要がありました。この例でも分かるように、Enum型の簡潔さには到底及びません。また、上記の例では毎回Colorクラスのインスタンスが生成されるのに比べ、Enum型の値は1つのみ(シングルトン)であるという利点もあります。

まとめ

今回は、PHP8.1で導入されたEnum型について簡単に紹介しました。Enum型を使うとこで列挙型を簡潔に定義できるようになりました。また、これにより、可読性や安全性が高いプログラムを記述することができるようになりました。

PHP8.0以前は外部パッケージを用いるか、オレオレEnumもどきを実装する必要がありましたが、PHP8.1で導入されたエレガントなEnum型には到底及びません。

Enum型にはBacked Enumと呼ばれる派生した列挙型や、Emum型にメソッドを定義することも可能です。これらに関しては、また別途ご紹介できればと思います。詳細についてはドキュメントをご参照ください。

Discussion