15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PHP Enum(列挙型)基礎 対話篇

Last updated at Posted at 2022-12-10

Enum(列挙型)とは?

PHP: 列挙型の概要 - Manual

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

つまり、どう使う?

  • システム開発会社A社では、血液型占いのWebアプリを開発する事となった。
  • A社での実績が多いことから、言語にはPHP、フレームワークにはLaravelが選択された。
  • このWebアプリの会員登録部分の実装を、「後輩エンジニアくん」が担当することとなった。
  • ユーザーは、会員登録の際、血液型を登録する。

先輩エンジニアくん(先)「進捗いかが?」
後輩エンジニアくん(後)「なんとなく実装のイメージはついてるんですが、方式で迷っている部分がありまして…」

先「どこ?」
後「血液型の部分はラジオボタンなのですが、選択肢をどう管理したものかと…」

先「configで管理しちゃえば?」
後「うーん、それだとちょっと自由度が高すぎる気がしています。それに、関数も追加したいんですよ」

先「なるほど。過去のLaravelの案件ならLaravel Enumとか使ってたよね」
後「それはそうなんですが、PHP8.1からEnum型が登場したので、できればこっちを使ってみたいなと」

先「そうだったわ完全に忘れてた。で?使えそう?」
後「それがわからないから悩んでいるんですよ...」

そこで、一通りの機能を確かめてみることにしました。

Enumの定義方法

先「たとえばconfigでオプション(選択肢)を、配列で定義するならこんな感じ」

config/blood_type.php
return [
    'A',
    'B',
    'AB',
    'O',
];

先「これをEnumにすると、こういう感じになるはずだね」

Enum/BloodType.php
<?php
enum BloodType
{
    case A;
    case B;
    case AB;
    case O;
}

後「ですね。caseか...switchのイメージが強いですが、確かにcaseっちゃcaseですね」

各caseを文字列として取得

先「それで、どういう使い方をする予定?」
後「まずは、ユーザーテーブルのレコードに文字列として入れるんで、ちゃんと文字列になるか見たいですね」

echo BloodType::B->name;

# ↓
# B

先「なるほど、 name プロパティを持つオブジェクトみたいな感じなのか」
後「わざわざnameを見なくても、勝手に変換してくれたりしないですかね?」
先「試そうか」

echo BloodType::B;

# ↓

# PHP Fatal error:  Uncaught Error: Object of class BloodType could not be converted to string

先「ふぅん、そこまで親切ではないか」
後「ひと手間必要な感じですけど、まあ余計な事はしない方針ってことですね」

引数の型指定

後「他には、関数の引数にもなるので、型のチェックはしたいですね」

enum BloodType
{
    case A;
    case B;
    case AB;
    case O;
}

function ja(BloodType $bloodType)
{
  return "{$bloodType->name}型";
}

echo ja(BloodType::B);

# ↓
# B型

後「お、ちゃんと型チェックできましたね」
先「これは、文字列を引数にしたら場合どうなるんだろ? 'B' とかみたいに」

echo ja("C");
# ↓
# PHP Fatal error:  Uncaught TypeError: ja(): Argument #1 ($bloodType) must be of type BloodType,..

echo ja("B");
# ↓
# PHP Fatal error:  Uncaught TypeError: ja(): Argument #1 ($bloodType) must be of type BloodType,..

先「ほう、仕様としてはOKな文字列でも、型チェックでは弾かれるな」
後「こっちも、変に気を回してきたりはしないっぽいですね」
先「PHP、気を回した仕様が今になって負債になってるのもあったと思うし、正解かも」

Backed Enum

先「うーん、しかし、そもそもこのjaって関数は必要かな...?」
後「といいますと?」

先「いや、例えば連想配列的に、Key-Valueみたいにできればいいなと思って。それから、選択肢に空文字入れたい時はどうすればいいか、謎だな...」
後「連想配列っぽいのもできそうですよ。ええと、caseに値を突っ込めばいいのかな」

  enum BloodType
  {
      case A = 'A型';
      case B = 'B型';
      case AB = 'AB型🍤';
      case O = 'O型';
  }

  echo Blood::AB->value;

# ↓
# PHP Fatal error:  Case A of non-backed enum ..(略).., try adding ": string" to the enum declaration

先「だめじゃん…。あ、型を指定する必要があるのか」
後「つまり、こうですか」

x  enum BloodType
o  enum BloodType :string

先「ほなもっかいやるで」

enum BloodType :string
{
    case A = 'A型';
    case B = 'B型';
    case AB = 'AB型🍤';
    case O = 'O型';
}

echo BloodType::AB->value;

# ↓
# AB型🍤

先「よし、動いたね。しかしPHPは『動的型付け言語』としての何かを捨ててない? 考えすぎ?」
後「…どうでしょうね。この連想配列っぽい形式は、 Backed Enum って言うらしいですよ」
先「Enumについて会話してるとき、そこにBacked Enumが含まれるかどうかで混乱しそう」
後「そういう場合は Pure Enum ってワードがあるみたいです」

(注:Laravel使ってるなら、同じ機能を実現するならlocale使ったほうが良い場合があります)

文字列からenumに

先「Pure EnumでもBacked Enumでも、文字列からEnumに変換するケースは絶対あるよね」
後「例えば、リクエストから取得できるのは'B'だけど、どうやってBloodType::Bに変換するかってことですね」
後「Pure Enumの場合は…ちょっと無理やりな感じですけどこうみたいです」

$bloodTypeString = 'B';

$bloodTypeCase = constant("BloodType::$bloodTypeString");

先「ちょっとキツイな…w」

後「Backed Enumの方は、from()って関数が用意されてるみたいです」

$bloodTypeCase = BloodType::from('B型');
var_dump($bloodTypeCase);

# ↓
# enum(BloodType::B)

先「これは、valueが一致するcaseが無いとどうなるんだろ」

$bloodTypeCase = BloodType::from('C型');
var_dump($bloodTypeCase);

# ↓
# PHP Fatal error:  Uncaught ValueError: "C型" is...

後「ちゃんとエラーがでますね。いい感じです。」
先「エラー出したくない場合はtry-catch?ちょっとデフォルト値いれたいだけでも記述量かなり増えるよこれ」
後「そうならないように、tryFrom()ってのもあるみたいです。一致するものがなければnullが帰ってくるらしいです。」

$bloodTypeCase = BloodType::tryFrom('C型');
var_dump($bloodTypeCase);

# ↓
# NULL

先「なるほど。基本Backed Enumで運用するほうがよさそうね。」

Enumへのクラス関数を追加

先「それで、Enumに関数を実装したいんだっけ?」
後「はい、抗体の種類を返却する関数なんですけど...」
後「A型はB抗体を、B型はA抗体を持っています。O型は両方の抗体を持っていて、AB型は両方持っていないです。」

先「それ、占いで本当にいるの?…とりあえずコードにするとどうなるのかな」
後「はい。AB型以外のリストを返す関数を追加するなら、こんな感じです」

enum BloodType :string
{
    case A = 'A型';
    case B = 'B型';
    case AB = 'AB型🍤';
    case O = 'O型';

    static function listHavingAntibodies()
    {
        return [
            self::A,
            self::B,
            self::O,
        ];
    }

var_dump( BloodType::listHavingAntibodies());

# ↓
# array(3) {
#   [0]=>
#   enum(BloodType::A)
#   [1]=>
#   enum(BloodType::B)
#   [2]=>
#   enum(BloodType::O)
# }

先「そのまんまだね」

Enumの各caseに関数を追加する

先「なるほど。たとえば、BloodType::B みたいに、caseに対して関数を追加はできないの?」
後「できますよ。その場合は、static(クラス関数)抜いて定義するみたいですよ」

enum BloodType :string
{
    case A = 'A型';
    case B = 'B型';
    case AB = 'AB型🍤';
    case O = 'O型';

    public function hasAntibody()
    {
        return $this !== self::AB;
    }    
}

var_dump( BloodType::AB->hasAntibody());

# ↓
# bool(false)

後「staticじゃない関数はcaseに実装される感じなんですよね」
先「$this 自身(case)を参照できるのか。」
後「ですね。関数でmatchとか switch 構文使って、各caseにプロパティを増やすような実装も出来るみたいです。」

nameとvalue以外は基本持てないし、valueはint型かstring型である必要がある

先「なるほd...あれ、プロパティ増やすってのはできないの?」
後「出来る気がしますよね。今はstringの Backed Enum ですけど、これをobjectの Backed Enum にするとか?」
先「なるほど、 BloodType::A->value->antibodies みたいな感じで抗体の配列とってくるとかね。こうか?」

enum BloodType :object
  {
      case A = (object)['name' => 'A型', 'antibodies' => ['A']];
      // (略)

# ↓
# Enum backing type must be int or string, object given

先「えっ、intかstring以外無理って怒られたじゃん...」
後「制約つよつよですね」

LaravelでEnumを使ったバリデーション

先「あ、そういえば、リクエストをLaravelのValidatorでバリデーションするときはどうする予定なんや?」
後「実はLaravelのバリデーションルールはEnumに対応済みなんですよ」
先「ほんとに」

use App\Enums\ServerStatus;
use Illuminate\Validation\Rules\Enum;
 
$request->validate([
    'status' => [new Enum(ServerStatus::class)],
]);

後「実は最初は、casesで配列化して、inルールとか使おうと考えてたんですけど..不要でしたね」
先「あ、たしかに、Enumをオプションとして扱いたいことはあるし、配列化できるのは便利かも」

Enumを配列化する

後「cases()って関数で配列に変換できるんですよ」

↑から引用
enum BloodType :string
{
    case A = 'A型';
    case B = 'B型';
    case AB = 'AB型🍤';
    case O = 'O型';
}

var_dump(BloodType::cases());

# ↓
# array(4) {
#   [0]=>
#   enum(BloodType::A)
#   [1]=>
#   enum(BloodType::B)
#   [2]=>
#   enum(BloodType::AB)
#   [3]=>
#   enum(BloodType::O)
# }

先「...ちょっと微妙かも?」
後「各caseはobjectみたいに扱えましたよね。なら、ちょっと小技つかえばオプションとして利用しやすい連想配列に変換できるかも...」


$options = array_combine(array_column(BloodType::cases(), 'value'), array_column(BloodType::cases(), 'name'));
var_dump($options);

# ↓
# array(4) {
#   ["A"]=>
#   string(4) "A型"
#   ["B"]=>
#   string(4) "B型"
#   ["AB"]=>
#   string(9) "AB型🍤"
#   ["O"]=>
#   string(4) "O型"
# }

先「コレ毎回書くのはキツくない?」
後「確かに...」

そして

先「まあ導入しても、なんとかなりそうだけどさぁ、正直もうちょっといい感じになるといいよね」
後「うーん、ヘルパーでも自作しますかねえ。その前にいい感じのパッケージないか探します..」

       「(archtechx/enums) Enum Helper の使い方 対話篇」に続く

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?