Iterable objects and array type hinting?

PhpIteratorType Hinting

Php Problem Overview


I have a lot of functions that either have type hinting for arrays or use is_array() to check the array-ness of a variable.

Now I'm starting to use objects that are iterable. They implement Iterator or IteratorAggregate. Will these be accepted as arrays if they pass through type hinting, or undergo is_array()?

If I have to modify my code, is there a generic sort of is_iterable(), or must I do something like:

if ( is_array($var) OR $var instance_of Iterable OR $var instanceof IteratorAggregate ) { ... }

What other iterable interfaces are out there?

Php Solutions


Solution 1 - Php

I think you mean instanceof Iterator, PHP doesn't have an Iterable interface. It does have a Traversable interface though. Iterator and IteratorAggregate both extend Traversable (and AFAIK they are the only ones to do so).

But no, objects implementing Traversable won't pass the is_array() check, nor there is a built-in is_iterable() function. A check you could use is

function is_iterable($var) {
    return (is_array($var) || $var instanceof Traversable);
}

To be clear, all php objects can be iterated with foreach, but only some of them implement Traversable. The presented is_iterable function will therefore not detect all things that foreach can handle.

Solution 2 - Php

PHP 7.1.0 has introduced the iterable pseudo-type and the is_iterable() function, which is specially designed for such a purpose:

> This […] proposes a new iterable pseudo-type. This type is analogous to callable, accepting multiple types instead of one single type. > > iterable accepts any array or object implementing Traversable. Both of these types are iterable using foreach and can be used with yield from within a generator.

function foo(iterable $iterable) {
    foreach ($iterable as $value) {
        // ...
    }
}

> This […] also adds a function is_iterable() that returns a boolean: true if a value is iterable and will be accepted by the iterable pseudo-type, false for other values.

var_dump(is_iterable([1, 2, 3])); // bool(true)
var_dump(is_iterable(new ArrayIterator([1, 2, 3]))); // bool(true)
var_dump(is_iterable((function () { yield 1; })())); // bool(true)
var_dump(is_iterable(1)); // bool(false)
var_dump(is_iterable(new stdClass())); // bool(false)

Solution 3 - Php

I actually had to add a check for stdClass, as instances of stdClass do work in foreach loops, but stdClass does not implement Traversable:

function is_iterable($var) {
    return (is_array($var) || $var instanceof Traversable || $var instanceof stdClass);
}

Solution 4 - Php

I use a simple (and maybe a little hackish) way to test for "iterability".

function is_iterable($var) {
    set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext)
    {
        throw new \ErrorException($errstr, null, $errno, $errfile, $errline);
    });
    
    try {
        foreach ($var as $v) {
            break;
        }
    } catch (\ErrorException $e) {
        restore_error_handler();
        return false;
    }
    restore_error_handler();
    return true;
}

When you try to loop a non iterable variable, PHP throws a warning. By setting a custom error handler prior the attempt to iterate, you can transform an error into an exception thus enabling you to use a try/catch block. Afterwards you restore the previous error handler to not disrupt the program flow.

Here's a small test case (tested in PHP 5.3.15):

class Foo {
    public $a = 'one';
    public $b = 'two';
}

$foo = new Foo();
$bar = array('d','e','f');
$baz = 'string';
$bazinga = 1;
$boo = new StdClass();    

var_dump(is_iterable($foo)); //boolean true
var_dump(is_iterable($bar)); //boolean true
var_dump(is_iterable($baz)); //boolean false
var_dump(is_iterable($bazinga)); //bolean false
var_dump(is_iterable($boo)); //bolean true

Solution 5 - Php

Unfortunately you won't be able to use type hints for this and will have to do the is_array($var) or $var instanceof ArrayAccess stuff. This is a known issue but afaik it is still not resolved. At least it doesn't work with PHP 5.3.2 which I just tested.

Solution 6 - Php

You CAN use type hinting if you switch to using iterable objects.

protected function doSomethingWithIterableObject(Iterator $iterableObject) {}

or

protected function doSomethingWithIterableObject(Traversable $iterableObject) {}

However, this can not be used to accept iterable objects and arrays at the same time. If you really want to do that could try building a wrapper function something like this:

// generic function (use name of original function) for old code
// (new code may call the appropriate function directly)
public function doSomethingIterable($iterable)
{
	if (is_array($iterable)) {
		return $this->doSomethingIterableWithArray($iterable);
	}
	if ($iterable instanceof Traversable) {
		return $this->doSomethingIterableWithObject($iterable);
	}
	return null;
}
public function doSomethingIterableWithArray(array $iterable)
{
	return $this->myIterableFunction($iterable);
}
public function doSomethingIterableWithObject(Iterator $iterable)
{
	return $this->myIterableFunction($iterable);
}
protected function myIterableFunction($iterable)
{
	// no type checking here
	$result = null;
	foreach ($iterable as $item)
	{
		// do stuff
	}
	return $result;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser151841View Question on Stackoverflow
Solution 1 - PhpNullUserExceptionView Answer on Stackoverflow
Solution 2 - PhpBlackholeView Answer on Stackoverflow
Solution 3 - PhpIsaacView Answer on Stackoverflow
Solution 4 - PhpTivieView Answer on Stackoverflow
Solution 5 - PhpRaoul DukeView Answer on Stackoverflow
Solution 6 - PhpJon GilbertView Answer on Stackoverflow