Skip to content

Instantly share code, notes, and snippets.

@Antnee
Last active September 3, 2023 23:40
Show Gist options
  • Save Antnee/fb2c41b0e3cf65eadf0c to your computer and use it in GitHub Desktop.
Save Antnee/fb2c41b0e3cf65eadf0c to your computer and use it in GitHub Desktop.
PHP 7 Generator Return Type
<?php
class Item {
private $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function id() : int
{
return $this->id;
}
}
class Collection {
private $items = [];
public function __construct(Item ...$items)
{
$this->items = $items;
}
public function item() : Generator
{
yield from $this->items;
}
}
$items = [];
for ($i=0; $i<10; $i++) {
$items[] = new Item($i);
}
$collection = new Collection(...$items);
$generator = $collection->item();
printf("\nThe Collection::item() method returns a type of %s, as expected\n", get_class($generator));
printf("However, it yields the following:\n\n");
foreach ($generator as $item) {
printf("\tClass: %s. Calling id() method returns %d (%s)\n", get_class($item), $item->id(), gettype($item->id()));
}
printf("\n\nIt doesn't appear to be possible to hint the yielded type\n\n");
@Antnee
Copy link
Author

Antnee commented Jan 28, 2016

https://3v4l.org/YcKua

PHP 7 allows us to define a return type. In this case, Collection::item() doesn't appear to be returning anything specific (ie there's no return statement), but rather it's yielding an Item type. So I tried to set the return type in Collection::item() to Item, but this will error saying:

Fatal error: Generators may only declare a return type of Generator, Iterator or Traversable, Item is not permitted in /vagrant/public/test.php on line 24

So, I changed it to a Generator and the code runs fine, but I wanted to hint that I would be yielding Item objects, not returning a Generator. The function DOES return a Generator, so it's not wrong, but in every loop I get an Item object to work with. I can't help but feel that this isn't the best way to use return type hinting with generators

@gallna
Copy link

gallna commented Jan 28, 2016

I can't see nothing wrong here, yield keyword is presumably the only way to create Generator object so $collection->item() returns new Generator instance.
Generator::current() returns yielded value, so (assuming you can extend final Generator class) declare type on Generator's current method instead. Generator::current() : Item
In above example just use Iterator instead of Generator: https://3v4l.org/VSf6k

@Antnee
Copy link
Author

Antnee commented Jan 29, 2016

@gallna The point was that in PHP 7 it's not possible to type hint the value that is yielded from a generator. This is just an example :)

However...

What I'm looking for is a guarantee that I will definitely get the correct type back.

In your solution there's no guarantee that I even constructed my array with \Item objects.

My example would stop runtime before it even instantiated the \Collection object, which is more desirable than instantiating it with duff data.

In yours I could put the odd \stdClass object in there, or maybe a \DateTime object, and the collection would be instantiated and potentially passed around the system assuming that everything is OK. At some point I would try to iterate over it and I'd get an error when I tried to call a method on the return value. In that case I may need to roll back a whole load of stuff that I've already done.

This would be resolved by not trusting the \ArrayIterator constructor and overloading it, mind you

If it's going to fail, it should fail fast!

As an aside, although in this case I constructed my collection with an array of objects, I may choose to construct it with a closure that fetches each item from a DB ONLY when required. It could be done with an \ArrayIterator, but it's a lot more hassle than doing it with a generator, which implements the \Iterator interface anyway, therefore is also \Traversable

Generators usually have a significantly smaller memory footprint than iterators.

The point still remains though that it's not possible to type hint when you will receive from a generator when it yields, and that's disappointing. I'd be OK with this syntax, which is similar to other languages which implemented generics:

class Collection {
    public function item() : Generator<Item>
    {
        yield from $this->items;
    }
}

@gallna
Copy link

gallna commented Jan 29, 2016

Sorry, I see the point now. I think we'll wait long to see implementation you expect.

@Antnee
Copy link
Author

Antnee commented Jan 29, 2016

Probably. I do know that some people are pushing for generics. That should also bring the ability to create an "array of types" as well. Hopefully something like $foo = []bar; to create an array of bar objects

@gallna
Copy link

gallna commented Feb 13, 2016

Here is working solution, where $items = []Item: https://3v4l.org/OH18b

@gallna
Copy link

gallna commented Feb 13, 2016

Or better one: https://3v4l.org/YMQer

@Antnee
Copy link
Author

Antnee commented Feb 19, 2016

@gallna - It's not the prettiest or most obvious piece of code I've ever seen, but I like that last solution. It does what I need it to :) It's a shame that it's such a PITA to get it to do what we're trying to do, but it certainly does it here. Nice one!

@rhift
Copy link

rhift commented Feb 12, 2021

hey @gallna - do you have another link for https://3v4l.org/YMQer by chance? That one produces a 500 and I am interested in your solution =) Thank you!

@Antnee
Copy link
Author

Antnee commented Feb 12, 2021

It works for me @rhift

<?php
class Item {
    private $id;
    public function __construct(int $id)
    {
        $this->id = $id;
    }
    public function id() : int
    {
        return $this->id;
    }
}
class Collection extends IteratorIterator
{
    public function __construct(Item ...$items)
    {
        parent::__construct(
            (function() use ($items) {
                yield from $items;
            })()
        );
    }
    public function current() : Item
    {
        return parent::current();
    }
}
$items = [];
for ($i=0; $i<10; $i++) {
    $items[] = new Item($i);
}
$collection = new Collection(...$items);

foreach ($collection as $item) {
    printf("\tClass: %s. Calling id() method returns %d (%s)\n", get_class($item), $item->id(), gettype($item->id()));
}

@rhift
Copy link

rhift commented Feb 12, 2021

oh weird, thanks for pasting it @Antnee!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment