2012-11-16 70 views
34

我想用註冊表來存儲一些對象。這是一個簡單的註冊表類實現。獲得「超載財產的間接改變沒有效果」通知

<?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 

我該怎麼做?

回答

18

此行爲已報告爲錯誤幾次:

目前尚不清楚對我有什麼討論的結果是,雖然它似乎有與值「通過價值」和「通過參考」傳遞值有關。我在some similar code找到了解決辦法做了這樣的事情:

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; 
     } 
    } 
} 

通知他們將值使用& $value時如何使用&__get&__set和也。我認爲這是完成這項工作的方法。

+0

我發掘了一個情況,即使這樣也不能解決問題:使用'ifsetor'實現可能會在測試存在時創建虛假的NULL值條目,請參見[此PHP.net RFC被拒絕](https:// wiki.php.net/rfc/ifsetor#userland_2)。不用感謝PHP核心開發人員讓我的生活更加艱難,我在工作中耗費更多的加班時間,而且由於沒有實施正確的'ifsetor'操作員,我的代碼更容易出錯。 – mirabilos

+1

使用'&__ get'替代'&__ get'來防止「間接修改重載屬性無效」警告,但是'&__ set'觸發了另一個錯誤(「WARNING:只有變量引用應該被引用返回。 「),所以我把它作爲'__set'。 – Cragmonkey

+0

'__set'永遠不會返回任何內容。你只需要爲'__get'做到這一點 – bg17aw

93

我知道,這是現在一個很老的話題,但它是我今天遇到了自己的第一次,我想,如果我擴大在什麼上面用我自己的調查結果說,這可能是幫助他人。

據我所知,這是不是在PHP的錯誤。事實上,我懷疑PHP解釋器必須特別努力檢測並報告此問題。它涉及你訪問「foo」變量的方式。

Registry::getInstance()->foo 

當PHP看到這部分語句時,首先檢查對象實例是否有一個名爲「foo」的可公開訪問的變量。在這種情況下,它不會,所以下一步是調用其中一個魔術方法,無論是__set()(如果您試圖替換當前值「foo」)或__get()(如果您試圖訪問該值)。

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

在這份聲明中,你正試圖取代 「富」 與數組中的值(1,2,3),那麼PHP調用您的__set()方法$關鍵= 「foo」 和$ value = array(1,2,3),並且一切正常。

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

然而,在此聲明,你是檢索「foo」的值,這樣就可以(通過將其視爲一個陣列和追加一個新的元件在這種情況下)修改它。該代碼意味着要修改的「富」的實例保存的值,但在現實中你是實際上修改由__get(返回FOO的臨時副本)等PHP發出警告(類似如果通過引用而不是按值將Registry :: getInstance() - > foo傳遞給函數,就會出現這種情況。

您有解決此問題的工作幾個選項。

方法1

你可以「富」的值寫入到一個變量,修改變量,然後寫回,即

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

功能,但可怕的冗長等不建議。

方法2

有引用您的__get()函數的返回,由cillosis的建議(也沒有必要有引用您的__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); 

將上面的代碼(至少「)陣列​​(1 PHP的一些版本中)輸出像{[」 foo 「的] => & NULL}」,意思是「$ VAR2 = & $ var1 ['foo'];「聲明可能會影響雙方的表達。不過,我認爲,這是從根本上壞的,允許一個變量的內容被改由操作,因爲這可能會導致一些嚴重的討厭的錯誤(因此我覺得上面的陣列行爲一個PHP BUG)。

例如,讓我們假設你永遠只能將存儲在您的註冊表對象,您修改__set()函數,如果$值不是一個對象時拋出異常。存儲在註冊表中的任何對象都必須符合特殊的「RegistryEntry」接口,該接口聲明必須定義「someMethod()」方法。因此,註冊表類的文檔聲明調用者可以嘗試訪問註冊表中的任何值,並且結果將檢索有效的「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()函數知道它是不是安全,從註冊表中讀取任意項,因爲它們可能會或可能不存在,所以它做了檢查「空」的情況下,並相應的行爲。一切都很好。相比之下,循環「知道」任何編寫操作註冊表將失敗,除非寫入的值是符合「RegistryEntry」接口的對象,所以它不費心去檢查以確保$條目確實是爲了節省不必要的開銷。現在讓我們假設在嘗試讀取任何尚不存在的註冊表項之後的某個時間,有一個非常罕見的情況。砰!

在上述場景中,循環會生成一個致命錯誤「調用成員函數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()是當你想給呼叫者只讀到某些私人/保護性訪問,在這種情況下,你不想通過引用返回任何東西。

我希望這有助於。 :)

+1

這是最好的答案。它值得多點擊 – DavidLin

+0

感謝您對問題的全面解釋!現在對我來說更有意義。 – Scott

+0

謝謝@ indigo866非常感謝! –

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,因爲這個函數應該什麼也不返回,所以沒有什麼可以參考的。