php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #34934 offsetExists not called in derived class
Submitted: 2005-10-20 18:52 UTC Modified: 2005-10-25 05:38 UTC
From: adove at booyahnetworks dot com Assigned: dmitry (profile)
Status: Not a bug Package: SPL related
PHP Version: 5.0.5 OS: WinXP, Linux
Private report: No CVE-ID: None
 [2005-10-20 18:52 UTC] adove at booyahnetworks dot com
Description:
------------
I've searched the bug list and found only one bug that hints arond the same issue (http://bugs.php.net/bug.php?id=34849) BUT it appears the closing statement in the bug "ArrayAccess objects don't work in array_*() functions. You may want to
turn this report into a feature request?" seems incorrect per the code below. 

I have an class (MyArray) I derived from ArrayObject and have overridden the ArrayAccess::offset* methods to allow me to make the array addressable via an XPath-styled key path, e.g., /key1/key2, etc.). All of the offset* methods get called just fine and work like a champ EXCEPT offsetExists... 

Based on the bug ref'd above, offsetExists is just an adjunct function on the ArrayAccess interface and not "hooked up" to any language constructs. HOWEVER, based on emperical information, this appears incorrect/inconsistent. 

The code submited illustrates the point. If I take a raw ArrayObject, set it to a multi-dim'd array and test for the root key via array_key_exists, I get true, which is correct. Additionally, if I test for any other key I get false, which is also correct. Based on the above bug I would expect to get a warning about only arrays being passed to the method blah blah. So, somehow it would *appear* that the ArrayObject class is somehow having its offsetExists method.

When I do the same tests with my derived array object (MyArray), I get the same results. HOWEVER, my overidden offsetExists method is never called. This is evident via debuging (I never step into it) and in the last test assigned to $bShouldWorkButDoesNot. This path actually does exist and would have  been confirmed to via my offsetExists.

So, I am prepared to be given the same answer as the other bug. ;-) BUT, I think this is inconsistent behavior so it seems a bug to me... And, if it is not a bug, then I will 100% submit a feature request. 

Reproduce code:
---------------
$a = array(
    "test" => array(
        "one" => "dunno",
        "two" => array(
            "peekabo" => "do you see me?",
            "anyone" => array("there")
            )
        )
    );
$oArray = new ArrayObject($a);
$oMyArray = new MyArray($a);

$bWorks1 = array_key_exists("test", $oArray);
$bWorks2 = array_key_exists("test", $oMyArray);

$bShouldWorkButDoesNot = array_key_exists("test/two/peekabo", $oBooyahArray);


Expected result:
----------------
true === $bWorks1
true === $bWorks2
true === $bShouldWorkButDoesNot

Actual result:
--------------
true === $bWorks1
true === $bWorks2
false === $bShouldWorkButDoesNot

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2005-10-20 18:57 UTC] tony2001@php.net
Please try using this CVS snapshot:

  http://snaps.php.net/php5-latest.tar.gz
 
For Windows:
 
  http://snaps.php.net/win32/php5-win32-latest.zip


 [2005-10-20 19:28 UTC] adove at booyahnetworks dot com
Same behavior in both the latest 5.0.5 and 5.1...
 [2005-10-20 19:30 UTC] tony2001@php.net
Provide a short but complete reproduce script then.
The code you've pasted in the report is not complete - there is no declaration of MyArray class.
 [2005-10-21 07:42 UTC] adove at booyahnetworks dot com
Here's a full code sample to replicate. Note I stripped out the method comments. If you want them I can repost.

<?php
class MyArray extends ArrayObject
{
    function __construct(
        $mValue = array()
        )
    {
        // Convert our value.        
        if(is_array($mValue))
        {
            $mValue = &$this->recurs($mValue);
        }
        elseif(is_object($mValue) && ($mValue instanceof ArrayObject))
        {
            // Get the raw array from our ArrayObject for handling.
            $mValue = &$this->recurs($mValue->getArrayCopy(), $constFlags);
        }
        elseif(null == $mValue)
        {
            $mValue = array();
        }
        elseif(!is_object($mValue))
        {
            // Turn primative into an indexed array.
            $mValue = array($mValue);
        }
        // else - it's an object, enum its public properites.
        
        parent::__construct($mValue);
    }
    
    protected function explodeKeyPath($mKeyPath)
    {
        return explode("/", trim($mKeyPath, "/"));
    }
    
    function offsetSet($mKey, $mValue) 
    {
        $mKey = trim($mKey, "/");
        if(stristr($mKey, "/"))
        {
            // A path was requested. Start out pointing to ourself.
	        $mRetval = $this;
	        
	        $aKeyPathParts = $this->explodeKeyPath($mKey);
                
            // Need to take the LAST path off. That's what we assign our value too. 
            $strLastPathPart = array_pop($aKeyPathParts);
	            
	        foreach($aKeyPathParts as $strPathPart)
	        {
	            // We know we our data are either a primative type or an ArrayAccess. 
	            if(($mRetval instanceof ArrayAccess) && 
	                array_key_exists($strPathPart, $mRetval)
	                )
	            {
	                $mRetval = $mRetval[$strPathPart];
	            }
	            else
	            {
                    // Need a new empty one of us and then point to it!
                    $mRetval[$strPathPart] = 
                        $this->newArrayObject($strPathPart, array(), $this->getFlags());
	                $mRetval = $mRetval[$strPathPart];
	            }
	        }
            
            // Now put our value!
            $mRetval[$strLastPathPart] = $mValue;
        }
        else
        {
	        if(is_array($mValue))
	        {
	            // Need to create a new instance of us!
	            $mValue = $this->newArrayObject($mKey, $mValue, $this->getFlags());
	        }
	        
            parent::offsetSet($mKey, $mValue);
        }
    }

    function offsetGet($mKey) 
    { 
        $mRetval = null;
        
        $mKey = trim($mKey, "/");
        if(stristr($mKey, "/"))
        {
            // A path was requested. Start out pointing to ourself.
	        $mRetval = $this;
	        
	        $aKeyPathParts = $this->explodeKeyPath($mKey);
	            
	        foreach($aKeyPathParts as $strPathPart)
	        {
	            // We know we our data are either a primative type or an ArrayAccess. 
	            if(($mRetval instanceof ArrayAccess) && 
	                array_key_exists($strPathPart, $mRetval)
	                )
	            {
	                $mRetval = $mRetval[$strPathPart];
	            }
	            else
	            {
                    $mRetval = null;
	                
	                break;
	            }
	        }
        }
        else
        {
            $mRetval = parent::offsetGet($mKey);
        }
        
        return $mRetval; 
    } 
    
    function offsetUnset($mKey) 
    { 
        $mKey = trim($mKey, "/");
        if(stristr($mKey, "/"))
        {
            // A path was requested. Start out pointing to ourself.
	        $oArray = $this;
	        
	        $aKeyPathParts = $this->explodeKeyPath($mKey);
                
            // Need to take the LAST path off. That's what we unset. 
            $strLastPathPart = array_pop($aKeyPathParts);
	            
	        foreach($aKeyPathParts as $strPathPart)
	        {
	            // We know we our data are either a primative type or an ArrayAccess. 
	            if(($oArray instanceof ArrayAccess) && 
	                array_key_exists($strPathPart, $oArray)
	                )
	            {
	                $oArray = $oArray[$strPathPart];
	            }
	            else
	            {
                    // Hmmmm... bad path... Die. Nothing will happen below.
                    break;
	            }
	        }
            
            // Now unset our value!
            if(($oArray instanceof ArrayAccess) && 
                array_key_exists($strLastPathPart, $oArray)
                )
            {
                unset($oArray[$strLastPathPart]);
            }
            // else - Bad path.. do nothing...
        }
        else
        {
            parent::offsetUnset($mKey);
        }
    } 

    function offsetExists($mKey) 
    { 
        $bRetval = false;
        
        $mKey = trim($mKey, "/");
        if(stristr($mKey, "/"))
        {
            // A path was asked for. Start at our root.
            $mPathData = $this;
            
	        $aKeyPathParts = $this->explodeKeyPath($mKey);
	            
	        foreach($aKeyPathParts as $strPathPart)
	        {
	            // We know we our data are either a primative type (non-array) 
	            // or an ArrayAccess. 
	            if(($mPathData instanceof ArrayAccess) && 
	                array_key_exists($strPathPart, $mPathData)
	                )
	            {
                    // So far so good... True will remain unless a latter path 
                    // part is invalid. 
	                $bRetval = true;
	                
	                // Set for next loop.
	                $mPathData = $mPathData[$strPathPart];
	            }
	            else
	            {
	                // Nope, bad path.
	                $bRetval = false;
	                break;
	            }
	        }
        }
        else
        {
            $bRetval = parent::offsetExists($mKey);
        }
        
        return $bRetval;
    } 
    
    protected function &recurs(
        $aData
        )
    {
        $aRetval = $aData;
        
        foreach($aRetval as $mKey => &$mValue)
        {
            // If we have a primative array or MyArray-based instance 
            // convert them to us... Buhahahahahah!!!  
            if(is_array($mValue) || 
                (is_object($mValue) && ($mValue instanceof ArrayObject))
                )
            {
                $aRetval[$mKey] = new MyArray($mValue);
            }
            // else - leave it alone!
        }
        
        return $aRetval;
    }
}

$a = array(
    "test" => array(
        "one" => "dunno",
        "two" => array(
            "peekabo" => "do you see me?",
            "anyone" => array("there")
            )
        )
    );
$oArray = new ArrayObject($a);
$oMyArray = new MyArray($a);

$bWorks1 = array_key_exists("test", $oArray);
$bWorks2 = array_key_exists("test", $oMyArray);

$bShouldWorkButDoesNot = array_key_exists("test/two/peekabo", $oMyArray);
$bDirectWorks = $oMyArray->offsetExists("test/two/peekabo");
$bDirectBadPathWorksToo = $oMyArray->offsetExists("test/two/notfound");
?>
 [2005-10-21 17:21 UTC] dmitry@php.net
Fixed in CVS HEAD and PHP_5_1.
 [2005-10-24 09:01 UTC] dmitry@php.net
After discussion with Marcus and Andi, this fix is reverted and bug marked as bogus. array_... functions were not designed to work with ArrayAccess interface, ao array_key_exists() shouldn't call ofssetExists() at all.
You should use isset() instead of it.
 [2005-10-25 05:38 UTC] adove at booyahnetworks dot com
Ok, isset works as expected. IMHO, the behavior should be made consistent across the one array_key_exists example that works.
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Sun Nov 19 01:31:42 2017 UTC