🤖

PHP8.1が出たのでmyclabs/php-enumから列挙型(Enum)に置き換えが出来るか試してみた

2021/12/09に公開約7,400字2件のコメント

https://qiita.com/advent-calendar/2021/php
この記事は、PHP Advent Calendar 2021の10日目の記事です。

はじめに

2021年11月25日にPHP8.1がリリースされましたー

https://www.php.net/releases/8.1/en.php

新機能の中には、待望?の列挙型(Enum)が含まれています。

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

PHPでは、これまで列挙型を作るときにはmyclabs/php-enumなどのパッケージを使っていることが多かったかと思いますが、今回PHPでサポートされるようになったので、myclabs/php-enumから置き換えができるのか試してみます。

環境

  • PHP 8.1.0
  • myclabs/php-enum 1.8.3

myclabs/php-enumを使って作った下記のサンプルコードを列挙型(Enum)に置き換えれるか試してみたいと思います。

declare(strict_types=1);

use MyCLabs\Enum\Enum;

class Cardsuit extends Enum
{
    public const CLUBS = 1;
    public const DIAMONDS = 2;
    public const HEARTS = 3;
    public const SPADES = 4;

    public function label(): string
    {
        return match($this->value) {
            self::CLUBS => 'クラブ',
            self::DIAMONDS => 'ダイヤモンド',
            self::HEARTS => 'ハート',
            self::SPADES => 'スペード',
        };
    }
}

置き換えを試してみた

置き換えを試してみるのは次の通りです。

  • 定義
  • 使い方
  • staticメソッド
  • メソッド

定義

サンプルコードを列挙型で定義した場合は次のように書くことが出来そうです。

declare(strict_types=1);

enum Cardsuit: int
{
    case CLUBS = 1;
    case DIAMONDS = 2;
    case HEARTS = 3;
    case SPADES = 4;

    public function label(): string
    {
        return match($this) {
            self::CLUBS => 'クラブ',
            self::DIAMONDS => 'ダイヤモンド',
            self::HEARTS => 'ハート',
            self::SPADES => 'スペード',
        };
    }
}

列挙型にはスカラー値を持つもの(Backed Enum)と持たないもの(Pure Enum)があります。

https://www.php.net/manual/ja/language.enumerations.basics.php
https://www.php.net/manual/ja/language.enumerations.backed.php

置き換えをする場合はスカラー値を持つ列挙型(Backed Enum)を使うことになりそうです。

また、メソッド、staticメソッドを作成することが出来るようなので独自に作ったメソッドも置き換えは可能かと思われますが、テストは必須ですね。

https://www.php.net/manual/ja/language.enumerations.methods.php
https://www.php.net/manual/ja/language.enumerations.static-methods.php

さらに定数を定義することもできるようです。

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

が、定数を使うなら列挙型に置き換える必要もない気がしますね。

定義に関することで最後に列挙型はtraitも使うことが出来るようです。

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

使い方

// myclabs/php-enum
$cardsuit = Cardsuit::DIAMONDS();
echo $cardsuit->label(); // クラブ

// 列挙型
$cardsuit = Cardsuit::CLUBS;
echo $cardsuit->label(); // クラブ

メソッドではないので()が不要になりますね。
また、これまで定数として使っていた場合に、置き換えたあとだと、列挙型のインスタンスに変わるので注意が必要ですね。

インスタンスの生成後は定義したメソッドの実行も出来ました。

staticメソッド

myclabs/php-enumで使えるstaticメソッドは下記になります。

  • from
  • keys
  • values
  • toArray
  • isValid
  • isValidKey
  • assertValidValue
  • search
  • __callStatic

from

列挙型のBacked Enumの場合は、同じ名前のfromメソッドがあります。

https://www.php.net/manual/ja/backedenum.from.php
// myclabs/php-enum
$cardsuit =  Cardsuit::from(4);
var_dump($cardsuit);
// object(Cardsuit)#3 (2) {
//  ["value":protected]=>
//  int(4)
//  ["key":"MyCLabs\Enum\Enum":private]=>
//  string(6) "SPADES"
}

// 列挙型
$cardsuit =  Cardsuit::from(4);
var_dump($cardsuit);
// enum(Cardsuit::SPADES)

インスタンスを返しているので、同じ動作になりそうです。

keys、values、toArray

列挙型にはこの3つと同様のメソッドはなさそうですが、
値のリストを取得するメソッドのcasesが使えるようなので、独自に定義すれば対応することが出来そうです。

https://www.php.net/manual/ja/language.enumerations.listing.php
var_dump(Cardsuit::cases());
// array(4) {
//   [0]=>
//   enum(Cardsuit::CLUBS)
//   [1]=>
//   enum(Cardsuit::DIAMONDS)
//   [2]=>
//   enum(Cardsuit::HEARTS)
//   [3]=>
//   enum(Cardsuit::SPADES)
//}

isValid、isValidKey、assertValidValue、search

列挙型には同様のメソッドはなさそうです。
独自に定義すれば対応することが出来そうです。

__callStatic

列挙型は全てのマジックメソッドが使えるわけではないようです

https://www.php.net/manual/ja/language.enumerations.object-differences.php

以下に示すもの以外の マジックメソッド は許可されていません。
__call, __callStatic, __invoke

__callStaticは使えるようなので独自に実装すれば、そのままで置き換えが出来そうな感じがします。

メソッド

myclabs/php-enumで使えるメソッドは下記になります。

  • __construct
  • __wakeup
  • __toString
  • getValue
  • getKey
  • equals
  • jsonSerialize

__construct、__wakeup、__toString

staticメソッドのところで書いた通り、この3つは列挙型では使えないマジックメソッドのようです。

__constructが使えなくなったので、staticメソッドのfromメソッドに置き換える必要があります。

$cardsuit = new Cardsuit(1); // Fatal error: Uncaught Error: Cannot instantiate enum Cardsuit
$cardsuit = Cardsuit::from(1);

また、__toStringも使えなくなったので、出力している箇所はvalueプロパティを使うように書き換える必要があります。
尚、valueプロパティはBacked Enumの場合だけ、定義されているみたいです。

$cardsuit = Cardsuit::CLUBS;
echo $cardsuit; // Fatal error: Uncaught Error: Object of class Cardsuit could not be converted to string
echo $cardsuit->value; // 1

getValue、getKey

列挙型にはメソッドではなく、専用プロパティvaluenameが定義されていて、そのプロパティを使うことで置き換えは出来そうです。

$cardsuit = Cardsuit::CLUBS;
echo $cardsuit->value; // 1
echo $cardsuit->name;  // CLUBS

equals

列挙型には同様のメソッドはなさそうです。
独自に定義すれば対応することが出来そうです。

jsonSerialize

列挙型もシリアライズは可能ですが、違う形でシリアライズされるようです。

https://www.php.net/manual/ja/language.enumerations.serialization.php
// myclabs/php-enum
$cardsuit =  Cardsuit::CLUBS();
echo serialize($cardsuit);
// O:8:"Cardsuit":2:{s:8:"*value";i:1;s:22:"MyCLabs\Enum\Enumkey";s:5:"CLUBS";}

// 列挙型
$cardsuit = Cardsuit::CLUBS;
echo serialize($cardsuit) . PHP_EOL;
// E:14:"Cardsuit:CLUBS";

独自にメソッドを定義してみよう

今回、独自にメソッドを定義すれば対応することが出来そうなので実際に作ってみました。
汎用的なものとして作ろうと思うので、今回はtraitを使います。
尚、列挙型はクラスではないので継承は使えないです。

declare(strict_types=1);

trait EnumTrait
{
    public function getValue(): mixed
    {
        return $this->value;
    }

    public function getKey(): string
    {
        return $this->name;
    }

    public function equals($enum): bool
    {
        return $enum instanceof self
        && $this === $enum
        && static::class === \get_class($enum);
    }

    public static function keys(): array
    {
        return \array_keys(static::toArray());
    }

    public static function values()
    {
        $cases = static::cases();
        $values = [];
        foreach ($cases as $enum) {
            $values[$enum->name] = $enum;
        }

        return $values;
    }

    public static function toArray(): array
    {
        $cases = static::cases();
        $array = [];
        foreach ($cases as $enum) {
            $array[$enum->name] = $enum->value;
        }

        return $array;
    }

    public static function isValid($value): bool
    {
        return \in_array($value, static::toArray(), true);
    }

    public static function isValidKey($key): bool
    {
        $array = static::toArray();

        return isset($array[$key]) || \array_key_exists($key, $array);
    }

    public static function assertValidValue($value): void
    {
        if (static::search($value) === false) {
            throw new \UnexpectedValueException();
        }
    }

    public static function search($value)
    {
        return \array_search($value, static::toArray(), true);
    }

    public static function __callStatic($name, $arguments)
    {
        $cases = static::cases();
        foreach ($cases as $case) {
            if ($name === $case->name) {
                return $case;
            }
        }
    }
}

作ってみましたが、myclabs/php-enumとほぼ一緒でしたね。

まとめ

まとめると次のような感じです。

  • (当たり前ですが)全てを置き換えることは無理でした。
  • 独自にメソッドを追加することで置き換えが可能なものもあります。
  • 定数として使っている箇所は列挙型のインスタンスに変わるので修正が必要です。

はじめに書いた通り、この記事は列挙型を触るための半分?趣味的な内容でした。

新規で列挙型を使う分にはいいかもしれないですが、依存パッケージを減らしたいなどの理由がない限り既にあるものは無理に置き換える必要はない気がしますね。

仮にmyclabs/php-enumから置き換えを検討している人がいるなら、この結果が何かの参考になればと思います。

Discussion

rector というツールで単純なパターンは自動置き換えもできたりします https://github.com/rectorphp/rector-src/pull/30

ありがとうございます。
すでにRectorに置き換えルールがあったんですね。

確認してみます

ログインするとコメントできます