46

レジストリを使用していくつかのオブジェクトを保存したいと思います。これが簡単なRegistryクラスの実装です。

<?php
  final class Registry
  {
    private $_registry;
    private static $_instance;

    private function __construct()
    {
      $this->_registry = array();
    }

    public function __get($key)
    {
      return
        (isset($this->_registry[$key]) == true) ?
        $this->_registry[$key] :
        null;
    }

    public function __set($key, $value)
    {
      $this->_registry[$key] = $value;
    }

    public function __isset($key)
    {
      return isset($this->_registry[$key]);
    }

    public static function getInstance()
    {
      if (self::$_instance == null) self::$_instance = new self();
      return self::$_instance;
    }
}

?>

このクラスにアクセスしようとすると、「オーバーロードされたプロパティの間接的な変更は効果がありません」という通知が表示されます。

Registry::getInstance()->foo   = array(1, 2, 3);   // Works
Registry::getInstance()->foo[] = 4;                // Does not work

私は何を間違えますか?

4

3 に答える 3

115

今ではかなり古い話題になっていることは承知しておりますが、本日初めて出会ったことであり、上記のことを自分の発見でさらに広げていくと、他の人にも役立つのではないかと思いました。

私の知る限り、これはPHPのバグではありません。実際、PHPインタープリターは、この問題を具体的に検出して報告するために特別な努力を払う必要があると思います。これは、「foo」変数にアクセスする方法に関連しています。

Registry::getInstance()->foo

PHPがステートメントのこの部分を見ると、最初に行うことは、オブジェクトインスタンスに「foo」と呼ばれる公的にアクセス可能な変数があるかどうかを確認することです。この場合はそうではないので、次のステップは、__ set()( "foo"の現在の値を置き換えようとしている場合)または__get()(ある場合)のいずれかのマジックメソッドを呼び出すことです。その値にアクセスしようとしています)。

Registry::getInstance()->foo   = array(1, 2, 3);

このステートメントでは、「foo」の値をarray(1、2、3)に置き換えようとしているため、PHPは__set()メソッドを$ key="foo"および$value= array(1、2、 3)、すべてが正常に機能します。

Registry::getInstance()->foo[] = 4;

ただし、このステートメントでは、「foo」の値を取得して変更できるようにしています(この場合は、配列として扱い、新しい要素を追加します)。このコードは、インスタンスが保持する「foo」の値を変更することを意味しますが、実際には__get()によって返されるfooの一時的なコピーを変更しているため、PHPは警告を発行します( Registry :: getInstance()-> fooを値ではなく参照によって関数に渡します)。

この問題を回避するためのいくつかのオプションがあります。

方法1

「foo」の値を変数に書き込み、その変数を変更してから、書き戻すことができます。

$var = Registry::getInstance()->foo;
$var[] = 4;
Registry::getInstance()->foo = $var;

機能的ですが、ひどく冗長であるため、お勧めしません。

方法2

cillosisで示唆されているように、__ get()関数を参照で返すようにします(値を返すことはまったく想定されていないため、__ set()関数を参照で返す必要はありません)。この場合、PHPは既存の変数への参照のみを返すことができ、この制約に違反すると通知を発行したり、奇妙な動作をしたりする可能性があることに注意する必要があります。クラスに適合したcillosisの__get()関数を見る場合(このルートを選択する場合は、以下で説明する理由により、この__get()の実装に固執し、読み取る前に存在チェックを忠実に実行してくださいレジストリから):

function &__get( $index )
{
    if( array_key_exists( $index, $this->_registry ) )
    {
        return $this->_registry[ $index ];
    }

    return;
}

これは、アプリケーションがレジストリにまだ存在していない値を取得しようとしない場合は問題ありませんが、取得した瞬間に「リターン」が表示されます。ステートメントを実行すると、「変数参照のみが参照によって返される必要があります」という警告が表示されます。フォールバック変数を作成して返すことでこれを修正することはできません。これにより、「オーバーロードされたプロパティの間接的な変更は効果がありません」という警告が表示されるためです。以前と同じ理由で再び。プログラムに警告を表示できない場合(および警告はエラーログを汚染し、他のバージョン/構成のPHPへのコードの移植性に影響を与える可能性があるため、警告は悪いことです)、__ get()メソッドはエントリを作成する必要がありますそれらを返す前に存在しない、すなわち

function &__get( $index )
{
    if (!array_key_exists( $index, $this->_registry ))
    {
        // Use whatever default value is appropriate here
        $this->_registry[ $index ] = null;
    }

    return $this->_registry[ $index ];
}

ちなみに、PHP自体は、配列を使用してこれと非常によく似た処理を実行しているようです。

$var1 = array();
$var2 =& $var1['foo'];
var_dump($var1);

上記のコードは(少なくとも一部のバージョンのPHPでは)「array(1){["foo"] =>&NULL}」のようなものを出力します。これは「$ var2 =&$var1['foo'];」を意味します。ステートメントは、式の両側に影響を与える可能性があります。ただし、読み取り操作によって変数の内容を変更できるようにすることは、深刻な厄介なバグにつながる可能性があるため、根本的に悪いと思います(したがって、上記の配列の動作PHPのバグであると感じます)。

たとえば、レジ​​ストリにオブジェクトを格納するだけで、$ valueがオブジェクトでない場合は、__ set()関数を変更して例外を発生させると仮定します。レジストリに格納されているオブジェクトはすべて、「someMethod()」メソッドを定義する必要があることを宣言する特別な「RegistryEntry」インターフェイスにも準拠している必要があります。したがって、レジストリクラスのドキュメントには、呼び出し元がレジストリ内の任意の値にアクセスを試みることができ、その結果、有効な「RegistryEntry」オブジェクトが取得されるか、そのオブジェクトが存在しない場合はnullになると記載されています。また、レジストリをさらに変更してIteratorインターフェイスを実装し、foreach構造を使用してすべてのレジストリエントリをループできるようにするとします。次のコードを想像してみてください。

function doSomethingToRegistryEntry($entryName)
{
    $entry = Registry::getInstance()->$entryName;
    if ($entry !== null)
    {
        // Do something
    }
}

...

foreach (Registry::getInstance() as $key => $entry)
{
    $entry->someMethod();
}

ここでの理論的根拠は、doSomethingToRegistryEntry()関数は、レジストリから任意のエントリを読み取るのは安全ではないことを認識しているため、「null」の場合をチェックし、それに応じて動作するということです。すべてうまくいっています。対照的に、ループは、書き込まれた値が「RegistryEntry」インターフェイスに準拠するオブジェクトでない限り、レジストリへの書き込み操作が失敗することを「認識」しているため、$entryが実際に存在することを確認する必要はありません。不要なオーバーヘッドを節約するためのそのようなオブジェクト。ここで、まだ存在していないレジストリエントリを読み取ろうとした後、このループに到達する非常にまれな状況があると仮定します。バン!

上記のシナリオでは、ループは致命的なエラー「非オブジェクトでのメンバー関数someMethod()の呼び出し」を生成します(警告が悪いことである場合、致命的なエラーは大惨事です)。これが実際には、先月の更新によって追加されたプログラムのどこかで、一見無害に見える読み取り操作が原因であることがわかります。これは簡単なことではありません。

個人的には、この方法も避けたいと思います。ほとんどの場合、うまく動作しているように見えますが、挑発された場合、本当に激しく噛む可能性があるからです。幸いなことに、はるかに簡単なソリューションが利用可能です。

方法3

__get()、__ set()、または__isset()を定義しないでください。次に、PHPは実行時にプロパティを作成し、それらをパブリックにアクセスできるようにします。これにより、必要なときにいつでも直接アクセスできるようになります。参照についてまったく心配する必要はありません。レジストリを反復可能にしたい場合でも、IteratorAggregateインターフェースを実装することでこれを行うことができます。あなたが最初の質問で与えた例を考えると、これが断然あなたの最良の選択肢であると私は信じています。

final class Registry implements IteratorAggregate
{
    private static $_instance;

    private function __construct() { }

    public static function getInstance()
    {
        if (self::$_instance == null) self::$_instance = new self();
        return self::$_instance;
    }

    public function getIterator()
    {
        // The ArrayIterator() class is provided by PHP
        return new ArrayIterator($this);
    }
}

__get()と__isset()を実装するときは、呼び出し元に特定のプライベート/保護されたプロパティへの読み取り専用アクセスを許可する場合です。この場合、参照によって何も返さないようにします。

これがお役に立てば幸いです。:)

于 2013-11-03T04:08:12.660 に答える
21

この動作は、バグとして数回報告されています。

値が「値で」「参照で」渡されることと関係があるように見えますが、議論の結果がどうだったかは私にはわかりません。私がいくつかの同様のコードで見つけた解決策は、次のようなことをしました:

function &__get( $index )
{
   if( array_key_exists( $index, self::$_array ) )
   {
      return self::$_array[ $index ];
   }
   return;
}

function &__set( $index, $value )
{
   if( !empty($index) )
   {
      if( is_object( $value ) || is_array( $value) )
      {
         self::$_array[ $index ] =& $value;
      }
      else
      {
         self::$_array[ $index ] =& $value;
      }
   }
}

&__getそれらがどのように使用されているか、また値useを&__set割り当てるときにも注意してください& $value。それがこの仕事をする方法だと思います。

于 2012-11-16T18:00:13.317 に答える
0

動作しない例では

Registry::getInstance()->foo[] = 4;                // Does not work

最初にを実行して__getから、戻り値を処理して配列に何かを追加します。__getしたがって、参照によって結果を渡す必要があります。

public function &__get($key)
{
  $value = NULL;
  if ($this->__isset($key)) {
    $value = $this->_registry[$key];
  }
  return $value;
}

$value参照で渡すことができるのは変数のみなので、使用する必要があります。この関数は何も返さないため、に&符号を追加する必要はありません。したがって、参照するものはありません。__set

于 2015-06-08T14:17:30.630 に答える