Troubleshooting “The (subclass) instance wasn't initialized properly” in custom RecursiveIteratorIterator

StackOverflow https://stackoverflow.com/questions/11833360

Question

I have created a custom iterator that extends RecursiveIteratorIterator which I use to iterate over a Doctrine_Collection from a table that uses the NestedSet behavior (e.g., so that I apply custom sorting to records at each level in the hierarchy).

There are a couple of models in my project that leverage this iterator, so I have created a base class that looks like the following:

/** Base functionality for iterators designed to iterate over nested set
 *    structures.
 */
abstract class BaseHierarchyIterator
  extends RecursiveIteratorIterator
{
  /** Returns the component name that the iterator is designed to work with.
   *
   * @return string
   */
  abstract public function getComponentName(  );

  /** Inits the class instance.
   *
   * @param $objects  Doctrine_Collection Assumed to already be sorted by `lft`.
   *
   * @throws LogicException If $objects is a collection from the wrong table.
   */
  public function __construct( Doctrine_Collection $objects )
  {
    /** @kludge Initialization will fail horribly if we invoke a subclass method
     *    before we have initialized the inner iterator.
     */
    parent::__construct(new RecursiveArrayIterator(array()));

    /* Make sure we have the correct collection type. */
    $component = $this->getComponentName();
    if( $objects->getTable()->getComponentName() != $component )
    {
      throw new LogicException(sprintf(
        '%s can only iterate over %s collections.'
          , get_class($this)
          , $component
      ));
    }

    /* Build the array for the inner iterator. */
    $top = array();
    /** @var $object Doctrine_Record|Doctrine_Node_NestedSet */
    foreach( $objects as $object )
    {
      // ... magic happens here ...
    }

    parent::__construct(
        new RecursiveArrayIterator($top)
      , RecursiveIteratorIterator::SELF_FIRST
    );
  }

  ...
}

A subclass might look something like this:

/** Iterates hierarchically through a collection of User objects.
 */
class UserHierarchyIterator
  extends BaseHierarchyIterator
{
  /** Returns the component name that the iterator is designed to work with.
   *
   * @return string
   */
  public function getComponentName()
  {
    return UserTable::getInstance()->getComponentName();
  }

  ...
}

Note the @kludge at the top of the constructor in the base class:

/** @kludge Initialization will fail horribly if we invoke a subclass method
 *    before we have initialized the inner iterator.
 */
parent::__construct(new RecursiveArrayIterator(array()));

As long as I keep that extra initialization line at the top of the base class' constructor, everything works as expected.

However, if I remove/comment that line, I get the following error as soon as script execution gets to $component = $this->getComponentName():

Fatal error: BaseHierarchyIterator::__construct(): The UserHierarchyIterator instance wasn't initialized properly in /path/to/BaseHierarchyIterator.class.php on line 21.

Alternatively, if I remove the code that calls $this->getComponentName() (and the subsequent conditional block), the constructor still operates as expected (minus the check to make sure the component name is correct).

What is the root cause of this error? Is there a better workaround for this issue?

PHP version info:

PHP 5.3.3 (cli) (built: Jul  3 2012 16:40:30) 
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies
    with Suhosin v0.9.29, Copyright (c) 2007, by SektionEins GmbH
Was it helpful?

Solution

Fatal error: BaseHierarchyIterator::__construct(): The UserHierarchyIterator instance wasn't initialized properly in /path/to/BaseHierarchyIterator.class.php on line 21.

What is the root cause of this error?

Your class extends from RecursiveIteratorIterator. To have an object of this type or a subtype thereof working, PHP needs to have it initialized properly before you call any other method or access a property of it. Properly means here that it's parent constructor has been called, the initialization is done then.

But you call a method before initialization (RecursiveIteratorIterator::__construct() has not been called yet). The method call in your case is:

$this->getComponentName();

You also have realized that so far. If you initialize before that call, you don't have this fatal error. In your case the initialization is:

parent::__construct(new RecursiveArrayIterator(array()));

The strict check by PHP reserves PHP to do some special kind of recursive iterators that can delegate better to underlying data-structures. Recursive traversal is just a bit more complex as well, needs initialization of a stack and such which is all hidden by PHP. So that check is also done for safety reasons.

If you compare that with IteratorIterator, you see that such a fatal error does not exists.

Is there a better workaround for this issue?

I won't call it a workround. You actually implemented something similar comparable for what IteratorAggregate is for. But you just did too much at once, see your constructor, it is doing too much. This is a Flaw: Constructor does Real Work.

So this calls for a cleaner separation of concerns and actually a more basic understanding of iterators could be helpful, too.

The solution is rather straight forward: The only thing you need to do here is to move the data-handling logic from the constructor into an implementation of IteratorAggregate:

abstract class BaseHierarchyIterator implements IteratorAggregate
{
    private $objects;

    abstract public function getComponentName();

    public function __construct(Doctrine_Collection $objects) {
        $this->setObjects($objects);
    }

    public function getIterator() {

        /* Build the array for the inner iterator. */
        $top = array();
        /** @var $object Doctrine_Record|Doctrine_Node_NestedSet */
        foreach ($this->objects as $object) {
            // ... magic happens here ...
        }

        return new RecursiveArrayIterator($top);
    }

    private function setObjects($objects) {

        /* Make sure we have the correct collection type. */
        $component = $this->getComponentName();

        if ($objects->getTable()->getComponentName() != $component) {
            throw new LogicException(sprintf(
                '%s can only iterate over %s collections.'
                , get_class($this)
                , $component
            ));
        }

        $this->objects = $objects;
    }
}

You then can just do recursive Iteration straight away:

$users = new UserHierarchyIterator($objects);
$it    = new RecursiveIteratorIterator(
                 $users, RecursiveIteratorIterator::SELF_FIRST
             );

As you can see, the only thing you need to solve your concrete problem is to separate the concerns a bit more. This will help you anyway, so you should be fine with it in the first run:

[Iterator]  ---- performs traversal --->  [Container]

The container has an interface the iterator can work on now. This by the way is exactly what an iterator object is. You actually used it wrong. If you follow that trail and as it's common with IteratorAggregate, you can go from there to a true own iterator interface for your containers. Right now you iterate over the doctrine collection before you create the iterator object.

OTHER TIPS

http://www.php.net/manual/en/oop4.constructor.php

PHP doesn't call constructors of the base class automatically from a constructor of a derived class. It is your responsibility to propagate the call to constructors upstream where appropriate.

which is to say, you have to explicitly call the constructor when extending classes, otherwise PHP doesn't know how to initialize the base class.

PHP could try to call the base class constructor with NULL params, but that would almost certainly be wrong and would lead to unexpected results (in this case - you'd probably get a bunch of 'param X expected an array, NULL given' errors if PHP decided to try to pass NULL to the base constructor instead of throwing an error)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top