次の様なデータとモデルクラスがあったとして、
データ
| id | name | deleted_at | |
|---|---|---|---|
| 1 | 太郎 | tarou@example.com | null |
// モデル
class Model_User extends \Orm\Model{
protected static $_table_name = 'users';
}
次の様にコードを書くと、コメントの通りの結果になります。
$user = Model_User::query()->get_one(); var_dump(isset($user->deleted_at)); // true var_dump(is_null($user->deleted_at)); // true var_dump(isset($user['deleted_at'])); // true var_dump(is_null($user['deleted_at'])); // true
思わずどっちだよ、と突っ込みたくなる結果です。一方で素のPHPは次のようになります。
class Users {
public $deleted_at = null;
}
var_dump(isset((new Users)->deleted_at)); // false
var_dump(is_null((new Users)->deleted_at)); // true
$user = [
'deleted_at' => null
];
var_dump(isset($user['deleted_at'])); // false
var_dump(is_null($user['deleted_at'])); // true
この違いは次のように定義されているマジックメソッドによって起こります。
// fuel/packages/orm/classes/model.phpより
/**
* Check whether a property exists, only return true for table columns, relations, eav and custom data
*
* @param string $property
* @return bool
*/
public function __isset($property)
{
if (array_key_exists($property, $this->_data))
{
// これに引っかかる。keyの中身がnullでもkeyが存在するからtrue
// FuelPHPのモデルは$this->_dataに連想配列としてDB中の値を持つ
return true;
}
elseif (static::relations($property))
{
return true;
}
elseif ($this->_get_eav($property, true))
{
return true;
}
elseif (array_key_exists($property, $this->_custom_data))
{
return true;
}
return false;
}
public function offsetExists($offset)
{
// 上記の__issetを読んでいる
return $this->__isset($offset);
}
__issetはインスタンスに存在しないプロパティを参照するときにしばしば用いられるメソッドです。isset($tgt->key)としてkeyの名のプロパティを持たない場合、isset($tgt->key)と$tgt->__isset(‘key’)は等しいです。
PHP: オーバーロード – Manual#__isset
上記コードではコメント部にある様に、DB中にカラムが存在するならばそのカラムと同名のプロパティに関するissetは常にtrueを返します。
offsetExistsはArrayとして扱うことのできる条件implements \ArrayAccessを満たしているクラスの持つメソッドです。PHPはこのクラスを配列で扱うとき、しばしばこのoffsetExistを使います。isset($tgt[‘offset’])は$tgt->offsetExists($offset)と等しいです。
PHP: ArrayAccess::offsetExists – Manual
上記コードのコメントにある様に__issetを呼ぶのみなので__issetと同様の問題が起きます。
これらによって中身がnullのプロパティを参照した時、次のようになります。
$user = Model_User::query()->get_one(); var_dump(isset($user->deleted_at)); // true var_dump(is_null($user->deleted_at)); // true var_dump(isset($user['deleted_at'])); // true var_dump(is_null($user['deleted_at'])); // true
対策は次のように記述することです。
$user = Model_User::query()->get_one(); var_dump(isset($user->deleted_at) && $user->deleted_at !== null); // keyの存在確認後、中身がnullでないと確認 // プロパティが参照できると確信できるなら次も大丈夫。参照できないとOutOfBoundsExceptionと例外が飛びます // PHP 型の比較表にある様に!is_null($hoge) === isset($hoge)なので var_dump(!is_null($user->deleted_at));