再考:列挙型
2017/08/30
第117回 PHP勉強会@東京
do_aki
@do_aki
@do_aki
http://do-aki.net/
列挙型
http://qiita.com/Hiraku/items/71e385b56dcaa37629fe
// こんなクラスを用意しておけば
final class GameDifficulty extends Enum {
const HARD = 'hard';
const NORMAL = 'normal';
const EASY = 'easy';
}
// こんな感じにインスタンスの生成ができて
$difficulty = GameDifficulty::NORMAL();
// 必要とするところでは、 難易度のいずれかであることが保証される
function hoge(GameDifficulty $difficulty) {
// 値は value メソッド で取り出せる
if ($difficulty->value() === GameDifficulty::NORMAL) {
...
}
}
改善したい項目
1. IDE 等で補完したい
2. 同じ定数値であっても異なる列挙型なら
ば別物としたい
1.IDE 等で補完したい
• Enum の 生成をする static メソッドは
__callStatic
• __callStatic は静的解析できない
• IDE で `GameDifficulty::NORMAL()`
が補完されない
• 人間の想像力による解決
– `GameDifficulty::NORMAL` が補完される
からいいじゃん -> IDE 問題は未解決
• DocComment による解決
– `@method static GameDifficulty NORMAL()`
まぁ、それでもいいか
同じ定数値であっても異なる列挙
型ならば別物としたい
• GameDifficulty::HARD と
CheeseType::HARD は区別したい
// 現状の列挙型だと……
final class CheeseType extends Enum {
const HARD = 'hard';
....
}
$hard = new GameDifficulty(GameDifficulty ::HARD);
var_dump($hard->value() === CheeseType::HARD); // bool(true)
• オブジェクトの緩い比較による解決
• 比較メソッドによる解決
$hard = new GameDifficulty(GameDifficulty ::HARD);
var_dump($hard == CheeseType::HARD()); // bool(false)
public function is(Enum $lhs) {
return get_class($this) === ($lhs)
&& $this->value() === $lhs->value();
}
var_dump($d->is(GameDifficulty::NORMAL()));
なんかイマイチ……
と、いろいろ考えてみたので、
せっかくならと
ぼくのかんがえたさいきょうの
列挙型を作ってみた
trait EnumTrait
{
private $value;
private function __construct($value)
{
$ref = new ReflectionObject($this);
$cons = $ref->getConstants();
if (!in_array($value, $cons, true)) {
throw new InvalidArgumentException("invalid constant value");
}
$this->value = $value;
}
final public function value()
{
return $this->value;
}
final public function is(self $lhs)
{
return $this === $lhs;
}
private static $instance;
/**
* @return self
*/
private static function constant()
{
static $name = null;
if ($name === null) {
$name = debug_backtrace(
DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['function'];
}
if (!isset(self::$instance[$name])) {
$cls = __CLASS__;
self::$instance[$name] = new $cls(constant("{$cls}::{$name}"));
}
return self::$instance[$name];
}
}
final class GameDifficulty {
const HARD = 'hard';
const NORMAL = 'normal';
const EASY = 'easy';
use EnumTrait {
constant as public HARD;
constant as public NORMAL;
constant as public EASY;
}
}
// ↓ DocComment なしに補完できる!
$difficulty = GameDifficulty::NORMAL();
var_dump($difficulty->is(GameDifficulty::NORMAL())); // bool(true)
var_dump($difficulty->is(CheeseType::HARD())); // Type Error
// ↑ PhpStorm ならば警告表示される!
Point
• trait を利用することで、`self` タイ
プヒンティング(型宣言)が、use され
たクラスを表すことになる (is)
• use … as でメソッドを複数指定するこ
とで、メソッドが複製され、 static 変
数がメソッド分用意される (constant)
• Singleton にすることで、オブジェクト
自身の比較が定数値の比較と同じことに
なる
private static $instance; // use したクラスごとに個別に存在
/**
* @return self
*/
private static function constant()
{
static $name = null; // use … as したメソッドごとに個別に存在
if ($name === null) {
$name = debug_backtrace(
DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0][‘function’];
}
if (!isset(self::$instance[$name])) {
$cls = __CLASS__; // これは use した クラス名に置き換わる
self::$instance[$name] = new $cls(constant("{$cls}::{$name}"));
}
return self::$instance[$name];
}
}
trait EnumTrait
{
private $value;
public function __construct($value)
{
$ref = new ReflectionObject($this);
$cons = $ref->getConstants();
if (!in_array($value, $cons, true)) {
throw new InvalidArgumentException(“invalid constant value”);
}
$this->value = $value;
}
final public function value()
{
return $this->value;
}
final public function is(self $lhs) // この self は use したクラスを表す
{
return $this === $lhs;
}
PhpStorm のバグ
• https://youtrack.jetbrains.com/iss
ue/WI-34277
• use … as で生成したメソッドから利用
個所を追跡(find usage)できない
• 2013年から存在するバグ(解消されず)
• 仕方ないので、 @method も併用してる
以上
• 自分なりに考えた より良い列挙型を作って
みた
• 意見もらえると嬉しいです
• (php なんだからもっとゆるふわでいー
じゃんという気もする)
• EnumTrait の完全版は
https://gist.github.com/do-aki/61fe688b512bf9255a67da390d9b2030

再考:列挙型

Editor's Notes

  • #5 t_wada さんの PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 / PHP Conference 2016 でも紹介されていた、型による制約の一つ