オーバーロードされたプロパティと emtpy() 〜 nagoya.php vol.2 での話

http://blog.ohgaki.net/php-5-4-accessor-php にある trait を使ったアクセサのサンプルコード(微妙にいじってますが、ロジックは全く同じです)。

<?php
// Example code how to eliminate getter and setter methods
// with traits introduced from PHP 5.4

trait Accessors
{
  public function __get($name)
  {
    if ($this->r_property[$name]) {
      return $this->$name;
    } else {
      trigger_error("Access to read protected property");
    }
  }

  public function __set($name, $value)
  {
    if ($this->w_property[$name]) {
      $this->$name = $value;
    } else {
      trigger_error("Access to write protected property");
    }
  }
}

class OrderLine
{
  use Accessors;

  private $r_property = array('price' => 1, 'amount' => 1);
  private $w_property = array('price' => 0, 'amount' => 1);

  protected $price = 100;
  private $amount;
  private $tax = 1.15; // property without getter/setter

  public function getTotal()
  {
    return $this->price * $this->amount * $this->tax;
  }
}

$line = new OrderLine;

//$line->price = 20;  // Notice error
$line->amount = 3;

echo "Total cost: ".$line->getTotal(), PHP_EOL;

で、上記のようなコードで、オーバーロードされたプロパティを emtpy() したら true が返ってハマったというのが、nagoya.php での話でした。

var_dump(empty($line->amount));  // true

これは、PHP の仕様です。

PHP マニュアルの「注意」

ここで、以下のような注意が PHP にマニュアルにありました。

注意:

オーバーロードされたプロパティを、 isset() 以外の言語構造の中で使うことはできません。 つまり、オーバーロードされたプロパティに対して empty() がコールされたとしても、オーバーロードされたメソッドはコールされないということです。

この制約を回避するには、オーバーロードされたプロパティを そのスコープのローカル変数にコピーしてから empty() に渡さなければなりません。
http://www.php.net/manual/ja/language.oop5.overloading.php

これ、何なんでしょう?

オーバーロードされたプロパティを、 isset() 以外の言語構造の中で使うことはできません」→ isset() なら使えるの?使えないですよね。

var_dump(isset($line->amount));  // false

empty() と isset() どちらも同じようにオーバーロードされたプロパティでは機能しません。

ということで、この「注意」はちょっとよくわかりません。ただの間違い?

あと、「この制約を回避するには、オーバーロードされたプロパティを そのスコープのローカル変数にコピーしてから empty() に渡さなければなりません」→ つまり、empty() 使いたかったら、こうしろと。

$amount = $line->amount;
var_dump(empty($amount));  // false

しかし、そんなことを強制するのは、正直、無理っぽいですよね。

どうすればいいのか?

__isset() を定義すればいいと思うよ。

<?php
// Example code how to eliminate getter and setter methods
// with traits introduced from PHP 5.4

trait Accessors
{
  public function __get($name)
  {
    echo '__get():';
    if ($this->r_property[$name]) {
      return $this->$name;
    } else {
      trigger_error("Access to read protected property");
    }
  }

  public function __set($name, $value)
  {
    echo '__set():';
    if ($this->w_property[$name]) {
      $this->$name = $value;
    } else { 
      trigger_error("Access to write protected property");
    }
  }

  public function __isset($name)  // これです
  {
    echo '__isset():';
    if ($this->r_property[$name] === 1) {
      return true;
    } else {
      return false;
    }
  }
}

class OrderLine
{
  use Accessors;

  private $r_property = array('price' => 1, 'amount' => 1);
  private $w_property = array('price' => 0, 'amount' => 1);

  protected $price = 100;
  private $amount;
  private $tax = 1.15; // property without getter/setter

  public function getTotal() {
    return $this->price * $this->amount * $this->tax;
  }
}

$line = new OrderLine;

//$line->price = 20;  // Notice error
$line->amount = 3;

echo "Total cost: ".$line->getTotal(), PHP_EOL;

var_dump(isset($line->amount));
var_dump(empty($line->amount));

そうすると、結果は、以下のようになります。

__set():Total cost: 345
__isset():bool(true)
__isset():__get():bool(false)

ちなみに、この話、trait は関係ありません。