PHPはよくweb系の案件に使われます。webではよく決まった選択肢を扱います。例えば都道府県です。都道府県は47の限られた選択肢から選ばれます(海外を考慮してその他込みの48や案件特有で説明しがたいものになる時もあります)。これを正しく早く取り扱うためには、いくらかの工夫された方法が必要になります。この記事ではその方法のいくつかを紹介します。ちなみにどの方法を採用してもベタ書きでマジックナンバーまみれのコードを生み出すより、ずっとずっとましです。
方法の評価方針は次です。
- コーディング時、どこに定義されていて、どうなっているかすぐにわかる
- コーディング時、望む定義の呼び出しがすぐにできる
- 実行速度が速い
- 管理上で重複、欠落がない
DBにマスターテーブルを作る
DB上にマスターテーブルを作る方法です。必要な際には以下の様なSQLを実行します。
SELECT * from pref_master WHERE id = 13; // 東京都を得る SELECT * from pref_master WHERE name = '東京都'; // 13を得る SELECT Users.*, pref_master.name FROM users INNER JOIN pref_master ON users.pref_id = pref_master.id WHERE users.id = 1; // ユーザとユーザの所属する都道府県の名前を得る
評価は次になります。
- コーディング時、どこに定義されていて、どうなっているかすぐにわかる
- 良好。テーブルの重複ぐらいの異常事態でなければ一貫できる
- コーディング時、望む定義の呼び出しがすぐにできる
- 良好。フレームワークのORMは優秀で、SQLを簡単に作れる
- 実行速度が速い
- 微妙。SQL発行自体のオーバーヘッドがよろしくないことになる事態も
- 管理上で重複、欠落がない
- マスタ自身にとっては良好。プロジェクト全体としては悪い
- マスターテーブルが乱立すると見通しが悪くなり、他の部分で事故を起こしやすい
この方法では外部制約によってユーザの保存する値が絶対に都道府県の範囲に収まることを保証できます。
設定ファイルにkey=>value, value=>keyの組み合わせを全部書く
設定ファイルで完結する方法です。例えば、次の様に定義、呼び出しをします(Laravel想定)。
// 設定ファイル return [ 'pref_key_to_val' => [ 1 => '北海道', 2 => '青森県' ... // 省略 ], 'pref_val_to_key' => [ // 日本語キー '北海道' => 1, '青森県' => 2, ... // あるいはアルファベットキー 'HOKAIDO' => 1, 'AOMORI' => 2, ... ], ]; // 呼び出し config('const.pref_key_to_val')[13]; // 東京都を得る config('const.pref_val_to_key')['東京都']; // 13を得る config('const.pref_val_to_key')['TOKYO']; // 13を得る class User { /** * 既にインスタンス化されているユーザの所属する都道府県の名前を返す * @return string */ public function get_pref_name(){ return config('const.pref_key_to_val')[$this->pref_id]; } }
評価は次になります。
- コーディング時、どこに定義されていて、どうなっているかすぐにわかる
- 程々。どのファイル群の中にあるかはすぐわかるがgrepが必須
- コーディング時、望む定義の呼び出しがすぐにできる
- やや良い。長いキーと複数同時呼び出しが必要な時には惨事になる
- 実行速度が速い
- 良好。定数のユースケースではACIDの内の永続性以外を捨て去ってよく、無駄のない形
- 管理上で重複、欠落がない
- 微妙。設定すべき項目が増えるにつれ、視認性の悪さ、制約の緩さから異常を埋め込みやすくなる
ちなみに日本語キーの場合、ファイルの文字コードの取り違え次第で事件が起きます(相当雑に管理しない限りは起きませんが)。
設定ファイルにkey=>valueのみを書いてarray_flipする
設定ファイルを簡潔にして呼び出し側でarray_flipすることによってvalue=>keyを実現します。例えば、次の様に定義、呼び出しをします(Laravel想定)。
// 設定ファイル return [ 'pref' => [ 1 => '北海道', 2 => '青森県' ... // 省略 ], ]; // 呼び出し config('const.pref'[13]); // 東京都を得る array_flip(config('const.pref'))['東京都']; // 13を得る class User { /** * 既にインスタンス化されているユーザの所属する都道府県の名前を返す * @return string */ public function get_pref_name(){ return config('const.pref')[$this->pref_id]; } }
設定のキーが短くなりました。ユースケース的に最も多くなる”キーから値を取得する”が簡潔になります。一方で都度array_flipを打ち込む分、値からキーを取る方は長くなりました。評価は次になります。
- コーディング時、どこに定義されていて、どうなっているかすぐにわかる
- やや良い。値=>キー込みの1/2の量のため検索困難になるまでの限界が遠くなる
- コーディング時、望む定義の呼び出しがすぐにできる
- やや良いが不安定。よくあるケースではキーが短く済むため複数条件でもなかなか爆発しない。値=>キー時は配列操作を駆使した方がよい
- 実行速度が速い
- 良好。定数のユースケースではACIDの内の永続性以外を捨て去ってよく、無駄のない形
- 管理上で重複、欠落がない
- 程々。視認性の悪さ、制約の緩さから異常を埋め込みやすくなるが限界が遠め
無視できない問題点に array_flip を知らないプログラマが参戦した時、マジックナンバーが乱立するという点があります。
ORMのconstに記述し、テーブルをまたぐ場合のみ設定ファイルに書く
Enum的な定数による数値と値のマッピングがなぜ必要になるかというと、テーブル中のint型カラムに状態を格納するからです。int型で格納された状態を表現する値に意味を持たせるのがEnum定数によるマッピングです。このことを考えて、ソースコードの中でテーブルに最も近い部分であるORMのconstに設定ファイルの様な記述を行います。例えば、次の様に定義、呼び出しをします。
// 都道府県の様な複数テーブルにまたがる場合が考えられるものは設定ファイル行き // ORMファイル class Delivery extends Model { const STATE = [ 10 => '配送処理中', 20 => '発送済み', 30 => '未発送', 40 => 'キャンセル中', 50 => 'キャンセル済み', 60 => '保留', ]; } // 呼び出し Delivery::STATE[40]; // キャンセル中を得る array_flip(Delivery::STATE)['保留'] // 60を得る class User { /** * 既にインスタンス化されているユーザに関する配送の状態を返す * @return string */ public function get_delivery_state_name(){ return Delivery::STATE[$this->deliveries->first()->state]; } }
配置がわかりやすくなりました。個々が簡潔なので重複も起きにくいでしょう。
- コーディング時、どこに定義されていて、どうなっているかすぐにわかる
- 良い。ORM、共通設定定数の順で探せばまず見つかる
- コーディング時、望む定義の呼び出しがすぐにできる
- やや良いが不安定。設定ファイル同様によくあるケースではキーが短く済むため複数条件でもなかなか爆発しない。値=>キー時は配列操作を駆使した方がよい
- 実行速度が速い
- 良好。定数のユースケースではACIDの内の永続性以外を捨て去ってよく、無駄のない形
- 管理上で重複、欠落がない
- 良好。分散により、視認性が良くなる。
この方法の問題点は、どの値がどのテーブルで表現されているかわからないと定数を発見しづらくなる点にあります。プロジェクトが巨大化した時、新規参入者になんとなくで定数を使うことを許しません。許すのが良いか悪いかは案件次第、取り違えの可能性をどこまで許容するか次第です。私的にはこれが今、一番好みのやり方です。
Enumパッケージを使う
いろいろ列挙型を使うパッケージが公開されています。タイプヒンティングが効きにくい、オブジェクト呼び出しが2行以上喰らいだす、辺りが不満点ですが、機能は十二分にそろっているのでうまく使うと上記の小技よりよっぽど役に立つでしょう。
myclabs/php-enum – Packagist
marc-mabe/php-enum – Packagist
bensampo/laravel-enum – Packagist