Search code examples
phpclassoopphp-8php-8.1

Can a PHP class return an array of classes without calling methods or variables?


Within PHP, I'd like to be able to iterate over a collection of classes to help with settings, inserting, and validating values. Using a class as a type in method args would make the code more strict which would help avoiding bugs.

I am able to access the collection but only through a public array or method ($values->array or $values->get()). I would like to be able to use $values directly for cleaner code. For example, to access a reference, I'd need to use $values->array[0] or $values->get()[0] instead of $values[0]. How can this be achieved with PHP?

Expected usage:

$values = new Values(
    new Value('foo', 'bar'),
    new Value('foo2', 'bar2'),
);

function handleValues(Values $exampleValues): void
{
    foreach ($exampleValues as $exampleValue) {
        //do something with $exampleValue->field, $exampleValue->value
    }
}

handleValues($values);

Classes:

class Values
{
    public array $array;

    public function __construct(Value... $value){
        $this->array = $value;
    }
}

class Value
{
    public string $field;
    public mixed $value;

    public function __construct(string $field, mixed $value)
    {
        $this->field = $field;
        $this->value = $value;
    }
}

Solution

  • It sounds like what you really want is a typed array, but there is no such thing in PHP.

    There is support for documenting typed arrays in a lot of static analysis tools and IDEs, using "PHPDoc syntax" like this:

    /** @param Value[] $values */
    function foo(array $values) {}
    

    If you want an object that can be looped with foreach, the simplest way is to implement the IteratorAggregate interface, and use it to wrap the internal array in an ArrayIterator object:

    class Values implements IteratorAggregate
    {
        private array $array;
    
        public function __construct(Value... $value){
            $this->array = $value;
        }
        
        public function getIterator(): Iterator {
            return new ArrayIterator($this->array);
        }
    }
    
    $values = new Values(
        new Value('foo', 'bar'),
        new Value('foo2', 'bar2'),
    );
    
    foreach ( $values as $value ) {
        var_dump($value);
    }
    

    If you want an object that can be referenced into with [...] syntax, implement the ArrayAccess interface. There are four methods, but each is trivial to implement for this case, and there's an example in the manual.


    There's also a built-in ArrayObject class that implements both these interfaces (and a few more), which you can extend to get a lot of array-like behaviour in one go.


    On the other hand, if all you want is to validate that the array contains only a specific type, then just do that. A one-line version would be:

    $valid = array_reduce($values, fn($valid, $next) => $valid && $next instanceof Value, true);
    

    Or a slightly more efficient version for large arrays (because it stops looping completely when it finds an invalid item):

    $valid = true;
    foreach ( $values as $next ) {
        if ( ! $next instanceof Value ) {
             $valid = false;
             break;
        }
    }