【FuelPHP】のモデルインスタンスでis_null($user->hoge)===isset($user->hoge)===trueが成立する

  • 2019年12月24日
  • 2019年12月24日
  • PHP, FuelPHP

 次の様なデータとモデルクラスがあったとして、

データ
id name email 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)); 

PHP: PHP 型の比較表 – Manual

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG