PHPのenum(列挙型)とは!?ちゃんと理解して新たなデータの持ち方の選択肢とする

PHP基礎 enum(列挙型)解説 PHP
※当サイトはアフィリエイト広告を掲載しています。

PHP8.1から、enum(列挙型)が使えるようになりました。私は日々の案件で忙殺されており、知ってはいましたが、基本形だけの利用に留まっていました。

Laravelでもenumを使うことがあるので、もうちょっとちゃんと理解しておきたいな……。ということで、この度改めてマニュアルをチェックすることに。

結果的に、思ったよりも柔軟で、いろいろできることを知りました。……が同時に、下手をするとごちゃつくなという印象も持ちました。

とは言えenumは、うまく使えば従来の定数・配列・クラスから置き換えて便利に使えるシーンも多そう。有用な機能であることは間違いありません。

本ページでは、そんなenumの理解を深めるために調べたことをまとめてみました。少しでも参考にしていただければ幸いです。

enumとは

enumは「enumeration」の省略で、直訳すると「列挙」や「一覧」を意味します。

PHPに限ったものではなく、プログラミングの文脈では「列挙型」というデータ型を指します。

特徴として、事前に定義(列挙)した固定の値しか取ることができないというものです。

読みは!?

私は勝手にイーナムと(心の中で)呼んでいましたが、どうやらこれが最も多数派な模様!?

こういったデータは統計が取れるものではないため、適当にググって出てきた読みを調べた結果です。

他にエナム 、イナム、イニューム、イヌームなどとも読めるという意見もあります。

英語やその語源的な正しさは別として、日本語読みではイーナムで通用する……という認識で良いのではないでしょうか。

Lara
Lara

本来の読みと異なっていても、用語としての定着は別なことがありますね。

何が便利なの!?

さきほど「事前に定義した固定の値しか取ることができない」と書きました。

この制約があることにより、以下のようなメリットがあります。

  • 型安全である
    値はあらかじめ定義する型に制限さるため、無効な型の値を割り当てることを防止します。
  • 間違った値を防ぐ
    列挙した値のみを使用できるため、ミスを回避できます。
  • コードが読みやすくなる
    コードが明瞭&短く記述することができるため、開発者がコードを読みやすくなります。

使用できるPHPバージョン

先にも述べましたが、PHP8.1~の機能です。

PHPとして比較的新しい機能ですので、まだ実務では使えていない環境にある方もいらっしゃることでしょう(執筆時点ではPHP8.2が最新)。

そろそろPHP7で動いているシステムも減ってきたので、今が覚え時と言えるかもしれませんね。

是非この機会に、覚えて使ってみてはいかがでしょうか。

Lara
Lara

PHPを長年使ってきた私としても、定数、配列、クラスの代替として使ってみようとワクワクしてきます!

enumの基本

enumの基本について見ていきましょう。enumは、大きく分けると以下の2つに分かれます。

  • 列挙型(Pure Enum)
  • 値に依存した列挙型(Backed Enum)

順番に説明していきます。

列挙型(Pure Enum)

最もシンプルな使い方が、Pure Enumです。以下の様な書き方で定義します。

enum 名称 {
    case ケース名;
}

ポイントとしてはcaseですが、これを必要なだけ複数並べていきます。

名称・ケース名に大文字・小文字の指定はありませんでしたが、本ページではPHP公式マニュアルに準じ、供にPascalCase(先頭だけ大文字)にしています。

実際のコードを見た方が分かりやすいので、以下に挙げます。

<?php
enum Days {
    case Monday;
    case Tuesday;
    case Wednesday;
    case Thursday;
    case Friday;
    case Saturday;
    case Sunday;
}

function planActivity(Days $day): string {
    switch ($day) {
        case Days::Monday:
            return "週の始まりはミーティング";
        case Days::Friday:
            return "週を終えてタスクを確認";
        default:
            return "タスクに取り組む……";
    }
}
$day = Days::Monday;

// 出力: 週の始まりはミーティング
echo planActivity($day); 

// 出力: Monday
echo $day->name;

ここではenum Days で、週の7日を表す列挙型を定義しています(2~10行目)。これにより、このDays型の変数はこれら7つのケースのみを取ることができます。

そしてplanActivity()関数は、Days enumの値を引数として受け取り、その日に基づいた活動計画を返します。引数の型が限定されているところがポイントです。

不正な値(例:”Days::Manday”や”Monday”等)を関数に渡すとエラーになるので、すぐに問題に気づくことが可能です。

注意点として、ここでのDays::Mondayはオブジェクトだということです。enumのオブジェクトにはnameというプロパティがあり、Days::Monday->nameでアクセス可能です。このコード例では28行目ですね。

値に依存した列挙型(Backed Enum)

値に依存した列挙型(Backed Enum)とは、各列挙のケースに特定のスカラー値(文字列または整数のみ)を関連付けることができるenumです。

以下の様に、型と値を指定します。

enum 名称: 型 {
    case ケース名 = スカラー値(文字列または整数のみ);
}

これも見た方が早いですね。1行目で型をstring指定している点と、caseの後の値に注目してください。

<?php
enum Days: string {
    case Monday = '月曜日';
    case Tuesday = '火曜日';
    case Wednesday = '水曜日';
    case Thursday = '木曜日';
    case Friday = '金曜日';
    case Saturday = '土曜日';
    case Sunday = '日曜日';
}

function getShortDayName(Days $day): string {
    return match ($day) {
        Days::Monday => "月",
        Days::Tuesday => "火",
        Days::Wednesday => "水",
        Days::Thursday => "木",
        Days::Friday => "金",
        Days::Saturday => "土",
        Days::Sunday => "日",
    };
}

$day = Days::Monday;

echo getShortDayName($day);  // 出力: 月
echo $day->name; // 出力: Monday
echo $day->value; // 出力: 月曜日

クラス定数(const)のような感じで指定することが可能、というわけですね。ここでは個々のケースが日本語の曜日名に関連付けられています。

注目したいのは、2行目にstringと、型を指定しているところです。この型しか値に指定できないため、型安全が担保されるというわけです。

int|stringなどのような指定はできず、必ず単一の型となります。また、boolやfloatなどは指定できません。

後のコードは同じようなものですが、Days::Monday->valueというプロパティにアクセスできることが異なります。

このプロパティは、値に依存した列挙型(Backed Enum)のみアクセス可能です。

応用編

これだけでも使えますが、さらにいろいろな使い方・機能があります。

まあこんなことができるんだなぁ程度に覚えておき、必要な際に改めて調べれば良いかなと思います。

cases()メソッド

enumにはcases()という静的メソッドが用意されています。このcases()メソッドを使うことで、enumで定義されているすべてのケースを取得することができます。

<?php
enum Days: string {
    case Monday = '月曜日';
    case Tuesday = '火曜日';
    case Wednesday = '水曜日';
    case Thursday = '木曜日';
    case Friday = '金曜日';
    case Saturday = '土曜日';
    case Sunday = '日曜日';
}

$allDays = Days::cases();

foreach ($allDays as $key => $day) {
    echo $key . ":" . $day->value . PHP_EOL; 
}
// 出力: 
// 0:月曜日
// 1:火曜日
// 2:水曜日
// 3:木曜日
// 4:金曜日
// 5:土曜日
// 6:日曜日

返されるリストは、enumのオブジェクトが入った配列となります。そのため15行目でvalueプロパティにアクセスできるというわけですね。

enumのケースを繰り返し処理する際などに有用です。

ここではforeach$keyも含めてループさせていますが、もちろん省略してもOKです。

from(), tryFrom()

特定のケースを取得したい……ということはよくあると思います。

それを実現するために、値に依存した列挙型(Backed Enum)ではfrom(), tryFrom()メソッドを用意しています。

<?php
enum Color: string {
    case Red = '赤';
    case Blue = '青';
    case Green = '緑';
}

// 正常なケース
$color = Color::from('赤');
var_dump($color); // Color::Red

// 存在しないケース (例外がスローされる)
// $colorInvalid = Color::from('黄色'); // Exception

// tryFromを使用して安全にケースを取得
$colorSafe = Color::tryFrom('yellow');
var_dump($colorSafe); // null

from()は対象が無かった場合に例外が発生します。対してtryFrom()はその場合にnullを返す違いがあります。

以下のような感じで使い分けるとよさそうです。

  • from()
    入力値が信用できる場合。存在しない場合はアプリケーションを停止させたい場合。
  • tryFrom()
    入力値が信用できない場合(フォームからのリクエストなど)。自分でエラー時の動作を実装したい場合。

定数が定義できる

enumには定数を宣言することが可能です。

<?php
enum Size
{
    case Small;
    case Medium;
    case Large;
    public const Moge = 'もげ';
}

//  出力: もげ
echo Size::Moge;

これ単体だと、あまり使い道が思い浮かびません。

しかしながら、以下の様に任意のクラスの定数式の値として使うことも可能です。

<?php
enum Fruit
{
    case Apple;
    case Banana;
    case Cherry;
}

class Juice
{
    // enumのケースをクラスの定数の値として使用
    const DEFAULT_FRUIT = Fruit::Apple;

    public static function describe(): string {
        return 'デフォルトのジュースは' . self::DEFAULT_FRUIT->name . 'でできています。';
    }
}

// 出力: デフォルトのジュースはAppleでできています。
echo Juice::describe();

うまく使えば有用かもしれませんね。

独自メソッドが持てる

enumには、独自にメソッドを作ることが可能です。

以下はコード例です。

<?php
enum Days: string {
    case Monday = '月曜日';
    case Tuesday = '火曜日';
    case Wednesday = '水曜日';
    case Thursday = '木曜日';
    case Friday = '金曜日';
    case Saturday = '土曜日';
    case Sunday = '日曜日';
    
    public function mood() {
        return match ($this) {
            Days::Monday => "また月曜日...",
            Days::Saturday, Days::Sunday => "週末楽しもう!",
            default => "普通の日",
        };
    }
}

// 出力: また月曜日...
echo Days::Monday->mood();

// 出力: 週末楽しもう!
echo Days::Sunday->mood();

// 出力: 普通の日
echo Days::Wednesday->mood();

Daysenumのオブジェクトからメソッドを呼ぶことができ、ここではmood()メソッドを呼んでいます。

その中でmatch()式により、メッセージを分岐させています。

match()式とは!?

PHP8.0.0から導入された式で、switch()文と近い感じで使うことが可能です。この例では、順番に上から評価していき、あてはまった値を返しています。

上手く使えば、便利に使えそうですよね。ただ、enumがクラス化してしまうような気がするので、使い方が難しそうです。

そもそも内部的にはクラスとして扱われるような文言をPHP公式マニュアルで見つけました。なのでそういうように使えて当然なのかもしれません。

インターフェースが実装できる

なんとインターフェースも実装できます。これも使いこなせれば有用そうです。

<?php
interface Describable {
    public function getDescription(): string;
}

enum Fruit: string implements Describable {
    case Apple = 'りんご';
    case Banana = 'バナナ';
    case Cherry = 'さくらんぼ';

    public function getDescription(): string {
        return "これは" . $this->value . "です。";
    }
}

// 出力: これはりんごです。
echo Fruit::Apple->getDescription();

トレイトを利用できる

なんとトレイトまで利用できちゃいます。ほんと、ほぼクラスですね……。

<?php
trait HasSound
{
    public function repeatSound($str): string {
        return $str . $str;
    }
}

enum Animal
{
    use HasSound;

    case Dog;
    case Cat;

    public function toCry(): string {
        $cry = match($this) {
            Animal::Dog => 'ワン!',
            Animal::Cat => 'ニャー!',
        };
        return $this->repeatSound($cry);
    }
}

// 出力: ワン!ワン!
echo Animal::Dog->toCry();        

// 出力: ニャー!ニャー!
echo Animal::Cat->toCry();
Lara
Lara

このようにクラスに近くも使えますが、私は当面定義をメインに利用するつもりです。

定数・配列・クラスから置き換えて使ってみる

いろいろな使い方をご紹介してきました。

しかしながら、現実的には最初から複雑な利用をすることはそうそうないと思います。上手く使わないと逆にメンテナンスしづらくなりそうですしね。

手始めとしては、定数・配列・クラスで値を定義していたシステムをenumに置き換えるのが現実的ではないか、と私は思いました。

ここではそれぞれの置き換えについてコード例を元に考えてみたいと思います。

定数をenumに置き換え

最近は見なくなりましたが、以前のWebアプリケーションの定義には、定数が頻繁に使われていました。

こんな感じです。

<?php
// 定数をdefineで定義
define('FRUIT_APPLE', 'りんご');
define('FRUIT_BANANA', 'ばなな');
define('FRUIT_CHERRY', 'さくらんぼ');

function getFruitPrice($fruit) {
    switch ($fruit) {
        case FRUIT_APPLE:
            return 100;
        case FRUIT_BANANA:
            return 80;
        case FRUIT_CHERRY:
            return 50;
        default:
            throw new Exception('無効なフルーツ');
    }
}

// 出力: りんごの価格: 100円
echo FRUIT_APPLE . "の価格: " . getFruitPrice(FRUIT_APPLE) . "円\n";

これをenumを使って書き換えてみましょう。

<?php
enum Fruit {
    case Apple;
    case Banana;
    case Cherry;

    public function getPrice(): int {
        return match($this) {
            Fruit::Apple => 100,
            Fruit::Banana => 80,
            Fruit::Cherry => 50,
        };
    }

    public function getJapaneseName(): string {
        return match($this) {
            Fruit::Apple => 'りんご',
            Fruit::Banana => 'ばなな',
            Fruit::Cherry => 'さくらんぼ',
        };
    }
}

// 出力: りんごの価格: 100円
echo Fruit::Apple->getJapaneseName() . "の価格: " . Fruit::Apple->getPrice() . "円\n";

列挙型(Pure Enum)で仕上げてみました。少なくともdefineよりは分かりやすいです。

値に依存した列挙型(Backed Enum)でも考えてみましょう。

<?php
enum Fruit: string {
    case Apple = 'りんご';
    case Banana = 'ばなな';
    case Cherry = 'さくらんぼ';

    public function getPrice(): int {
        return match($this) {
            Fruit::Apple => 100,
            Fruit::Banana => 80,
            Fruit::Cherry => 50,
        };
    }
}

// 出力: りんごの価格: 100円
echo Fruit::Apple->value . "の価格: " . Fruit::Apple->getPrice() . "円\n";

例えばAPPLEに対してのりんご、という関係がはっきりとしていますね。

DBに挿入するなどの場合は、この方が良いかもしれません。

配列をenumに置き換え

キーとバリューの関係で情報を定義することは非常に多くあります。それも多くはenumに置き換えることが可能と思われます。

例えば以下は、ローカル変数として配列でカテゴリーを定義しています。

<?php
$categories = [
    'Fruit' => '果物',
    'Vegetable' => '野菜',
    'Meat' => '肉'
];

function describeFood($category) {
    global $categories;

    $key = array_search($category, $categories);

    if ($key === false) {
        return "未知のカテゴリです。";
    }

    return "{$category}は{$key}カテゴリに属しています。";
}

echo describeFood('果物');  // 出力: 果物はFruitカテゴリに属しています。

小さなお問い合せフォームのパーツ等なら、これでも問題ありません。しかしシステムの規模が大きくなってくると、管理するのが困難になることもありました。

これが以下の様に、すっきりと書くことができます。

<?php
enum Category: string {
    case Fruit = '果物';
    case Vegetable = '野菜';
    case Meat = '肉';
}

function describeFood(Category $category): string {
    return "{$category->value}は{$category->name}カテゴリに属しています。";
}

// 出力: 果物はFruitカテゴリに属しています。
echo describeFood(Category::Fruit); 

実際には別ファイルに分けておくなどすれば、再利用性もよくなりますね。

クラスをenumに置き換え

クラスを使えば、合理的にデータを管理できそうです。しかし、以下の様に定数で持ってしまうとちょっとやりにくいシーンもあります。

<?php
class Region {
    const KANTO = '関東';
    const KANSAI = '関西';
    const CHUBU = '中部';
    const KYUSHU = '九州';

    public static function isRegionExist($regionName) {
        $regions = [self::KANTO, self::KANSAI, self::CHUBU, self::KYUSHU];
        return in_array($regionName, $regions);
    }
}

// 出力: 九州は存在します。
if (Region::isRegionExist('九州')) {
    echo "九州は存在します。";
} else {
    echo "九州は存在しません。";
}

上記のような実装は、項目数が増えるとメンテナンスが難しくなります。

その問題を避けるため、クラス内に静的な配列としてデータを定義するケースも多かったのではないでしょうか。私もよくやっていました。

それでもありと思いますが、今ならenumに置き換えることも一案でしょう。

<?php
enum RegionEnum: string {
    case Kanto = '関東';
    case Kansai = '関西';
    case Chubu = '中部';
    case Kyushu = '九州';

    public static function isRegionExist($regionName): bool {
        foreach (self::cases() as $case) {
            if ($case->value === $regionName) {
                return true;
            }
        }
        return false;
    }
}

// 出力: 九州は存在します。
if (RegionEnum::isRegionExist('九州')) {
    echo "九州は存在します。";
} else {
    echo "九州は存在しません。";
}

これは使いやすいですね。

配列と異なり、直接in_array()で分岐はできませんが、10-12行目のように、ちょっと書き足せば似たようなことは可能です。

まとめ

以上、enumについてまとめました。

いやー、クラス同様、かなりのことが出来そうですね。普通に今までクラスで実装していたものをリプレイスもできそうです。

逆に、設定値のみ定義するような使い型もできるでしょう。

人それぞれの使い方ができそうですね!この機会に私も実務でどんどん取り入れていこうと思います。

コメント

タイトルとURLをコピーしました