Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is ArrayIterator subclass's constructor never called?

Tags:

iterator

php

I'm baffled why a subclass to ArrayIterator is never getting its __construct method called. Consider this example:

<?php

class ConstructorException extends Exception {}

class Foo extends ArrayObject {
    function __construct( $arr = array(), $flags = 0, $iterator = 'ArrayIterator' ) {
        $iterator = 'FooIterator';
        parent::__construct( $arr, $flags, $iterator );
    }
}

class FooIterator extends ArrayIterator {
    function __construct( $array = array(), $flags = 0 ) {
        throw new ConstructorException( 'HELLO WORLD' ); // I AM NEVER CALLED.
        parent::__construct( $array, $flags );
    }
}

try {
    $f = new Foo( array( 1, 2, 3 ) );
    $it = $f->getIterator();
    if ( get_class( $it ) !== 'FooIterator' ) {
        throw new Exception( 'Expected iterator to be FooIterator.' );
    }
    die( "FAIL\n" );
} catch ( ConstructorException $e ) {
    die( "PASS\n" );
} catch ( \Exception $e ) {
    die( sprintf( "ERROR: %s\n", $e->getMessage() ) );
}

In both PHP 5.4 an 5.5, the result is FAIL. Why?

like image 217
Weston Ruter Avatar asked May 22 '15 23:05

Weston Ruter


2 Answers

Method __construct was called, as usually, on creating new instance:

$it = new FooIterator();

So, it take some time and I have a solution: override method getIterator() in your class Foo (subclass of ArrayObject) in your example to next:

class Foo extends ArrayObject {

    public function __construct( $arr = array(), $flags = 0, $iterator = 'FooIterator' ) {
        parent::__construct( $arr, $flags, $iterator );
    }

    /**
     * @return ArrayIterator
     */
    public function getIterator()
    {
        $class = $this->getIteratorClass();
        return new $class($this);
    }
}

With this correction your code will 'PASS'.

Result of changed code from question: http://3v4l.org/HnFQm

Previous code without exception, but showing that iterator work well with changes of method getIterator() in class Foo (adding by index and unsetting): http://3v4l.org/R8PHr

like image 58
Nicholas Vasilaki Avatar answered Oct 03 '22 02:10

Nicholas Vasilaki


@Leggendario is right to say the problem lies in the spl_array_object_new_ex method. However, if it's a bug i'm not sure. It is, however, not really standard what is going on here.

The iteratorClass variable from the constructor (or set through setIteratorClass) would suggest that this class gets instantiated whenever we retrieve the iterator from the ArrayObject. But it does not do regular "instantiation", as this is not possible.

It will just initialize the iterator (allocate memory etc), but not call the constructor. It makes sense to not call the constructor, as the constructor of an ArrayIterator takes two arguments ($array and $flags), and your class might have changed its signature, maybe even adding more (mandatory values).

Normally the ArrayIterator (or RecursiveArrayIterator), are internal classes and have a internal structure attached to them (basically like its own internal set of properties which you cannot get to directly from PHP userland). The spl_array_object_new_ex will set these internal values directly (most notably, the ce_get_iterator and ar_flags). So basically it takes over the work of the ArrayIterator constructor.

Because PHP checks if the given class is an ArrayIterator, or if one of the parent classes is one. When so, it means that it ultimately it can use these internal values and thus sets them directly, bypassing any need for a constructor. As a downside, anything that you would like to construct yourself, will not be called.

like image 25
JayTaph Avatar answered Oct 03 '22 03:10

JayTaph