1

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;
    }
}
MCSharp
  • 1,068
  • 1
  • 14
  • 37
  • I think I understand the question, and I have a couple of ideas, but I'm not sure of the best way to answer because I'm having trouble seeing what it's for exactly. Can you add a small example of how a theoretical solution would be used? – Don't Panic Feb 22 '22 at 16:55
  • @MCSharp: If I understand correctly you would like to have a typed array. This does not exist in PHP. I would recommend to extend the class _Values_ from _ArrayIterator_ (but I second _Don't Panic_ that an example would be nice to better understand your issue). – lukas.j Feb 22 '22 at 17:00
  • Added an example. Having access directly to the array would prevent creating additional variables or calling `->array` every time I'd need to access the objects. – MCSharp Feb 22 '22 at 17:11
  • Look at the iterator interface which `Values` would implement: https://www.php.net/manual/en/class.iterator.php (for the iterating example). – Computable Feb 22 '22 at 17:18
  • Yes, I was also thinking of suggesting implementing the Iterator interface, as well as the ArrayAccess interface. You might find it more trouble than it's worth just to avoid using `->array`, (it will take quite a bit of extra code to implement these interfaces) but that would let you effectively use your Values objects like arrays. – Don't Panic Feb 22 '22 at 17:21
  • I do think the examples helped to clarify your goal. Thanks for adding them. – Don't Panic Feb 22 '22 at 17:28
  • Does this answer your question? [Using foreach over an object implementing ArrayAccess and Iterator](https://stackoverflow.com/q/9973080/1426539) – yivi Feb 22 '22 at 18:12
  • Thanks everyone for the suggestions. I think implementing iterators are overkill. I'm just looking for a way to return the same output as `$values = [new Value()]` when using `$values = new Values(new Value())`. I thought this might be possible with magic methods but not getting success. – MCSharp Feb 22 '22 at 18:43
  • If part of the reason you want this is to enforce types in methods that take multiple values, arrays can still be used. if you use a variadic function such as your Values constructor, you can pass an array of values of that type with argument unpacking. For example: https://3v4l.org/aL8J0 (Apologies if you already know this.) – Don't Panic Feb 22 '22 at 19:29
  • @Don'tPanic thanks for providing the example. That solution might work if there is only one arg but not in this case: https://3v4l.org/fR6pX – MCSharp Feb 22 '22 at 20:05

1 Answers1

0

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;
    }
}
IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • This is the functionality I am looking for. Thanks for providing the examples. One note, `getIterator(): Iterator` should be `getIterator(): ArrayIterator` – MCSharp Feb 22 '22 at 20:03
  • @MCSharp That's up to you - `: Iterator` promises that this class, and any child classes, return some descendant of `Iterator`, which is true. It could also say `Traversable` (the return value required by the interface), or `ArrayIterator`, since those are both also true. – IMSoP Feb 22 '22 at 20:30