[PSR-5][PSR-19] Is there a way to annotate interface that acts as iterator over some type?

147 views
Skip to first unread message

Alan Gabeiel Bem

unread,
Oct 13, 2018, 6:06:26 PM10/13/18
to PHP Framework Interoperability Group
It is easy if such interface already extends some kind of iterator...

interface Stream extends IteratorAggregate
{
   
/**
     * @return Item[]
     */

   
public function getIterator();
}

...or... 

interface Stream extends Iterator
{
   
/**
     * @return Item
     */

   
public function current();
}

...so IDEs have no problems with inferring actual type that variable of type `Stream` iterates over...

/* @var $stream Stream */
foreach ($stream as $item) {
    $item
->... // works, Item class methods are suggested
}

...but what if we want to delay the decision of iteration mechanism? Like...

interface Stream extends Traversable
{}

...so later on we can do this:

class InMemoryStream implements Stream, IteratorAggregate
{
   
public function __construct(Item ...$items)
   
{
        $this
->items = $items;
   
}

   
// rest of the code goes here

   
/**
     * @return Item[]
     */

   
public function getIterator()
   
{
       
return new ArrayIterator($this->items);
   
}
}

class DatabaseStream implements Stream, Iterator
{
   
// rest of the code goes here

   
public function rewind()
   
{
        $this
->statement = $this->connection->prepare('SELECT * FROM items');
        $this
->statement->execute();
        $this
->current = $this->statement->fetch(\PDO::FETCH_ASSOC);
        $this
->key = 0;
   
}

   
public function next()
   
{
        $this
->current = $this->statement->fetch(\PDO::FETCH_ASSOC);
        $this
->key++;
   
}
   
   
public function key()
   
{
       
return $this->key;
   
}
   
   
public function valid()
   
{
       
return false !== $this->current;
   
}

   
/**
     * @return Item
     */

   
public function current()
   
{
       
return new Item($this->current);
   
}
}

Unfortunately IDEs, as can be expected, can't handle this.

/* @var $stream Stream */
foreach ($stream as $item) {
    $item
->... // not enough information for IDEs to suggest type that $stream iterates over.
}

AFAIK at this moment the only solution for such case is to annotate variables in user-land... every time.


/* @var $stream Stream|A[] */
foreach ($stream as $item) {
    $item
->... // works
}

// or worse

/* @var $stream DatabaseStream */
foreach ($stream as $item) {
    $item
->... // works but annotation specifies actual implementation which violates Design by Contract principle
}



It bugs me personally, but I would like to hear your opinion on this and possible solution.

Chuck Burgess

unread,
Oct 16, 2018, 1:30:23 PM10/16/18
to PHP Framework Interoperability Group
I'm tempted to argue that you're *not* sticking to Design By Contract here, because your two `Stream` implementations do not themselves give the same contract... at least, the parts of the contract that you're wanting to be derived by the IDE.

Since it's already necessary to use a `/* @var */` comment block to typehint your local variable, and you don't want to hint specifically on your two classes, then perhaps you can do this instead:

/* @var $stream Stream|Iterator|IteratorAggregate */

foreach ($stream as $item) {
    $item
->...
}

That leaves you only hinting on interfaces... 
CRB

Alan Bem

unread,
Oct 16, 2018, 3:02:21 PM10/16/18
to php...@googlegroups.com
I think you missed my point - What I want to achieve is to annotate somehow that Stream is iterable over Item object and do it in one place instead of every place I'm using `Stream` implementation. 

As I mentioned I could enforce using Iterator or IteratorAggregate (as it opens the way to specify the Item as an iterated type... which IDEs understand), but I don't want to do that because actual iteration method should depend on how it fits actual implementation. Traversable is enough in this case because, simply speaking, it is marking Stream as an object you can foreach on, but not telling how. 

--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/77014da1-8ab9-4042-af9d-eb26deaeaf97%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.


--
Pozdrawiam,
Alan Bem

Woody Gilk

unread,
Oct 17, 2018, 2:26:32 PM10/17/18
to PHP Framework Interoperability Group
Wouldn't the simple solution be:

/** @var $stream Item[] */
foreach ($stream as $item) {
    /* ... */

Chuck Burgess

unread,
Nov 8, 2018, 2:18:49 PM11/8/18
to PHP Framework Interoperability Group
Revisiting this, I'm leaning towards there *not* being a way to accomplish this in the "one place" manner that you are after.
CRB

Larry Garfield

unread,
Nov 9, 2018, 1:10:36 PM11/9/18
to php...@googlegroups.com
"This variable/return is an iterable of Foo objects" seems like an entirely
reasonable thing to do. We already have the de facto standard of Foo[] to
mean "an array of Foo objects", but that doesn't technically cover any
iterable. (I'm really big on iterables these days, as anyone who follows my
blog has noticed.)

--Larry Garfield

On Thursday, November 8, 2018 1:18:49 PM CST Chuck Burgess wrote:
> Revisiting this, I'm leaning towards there *not* being a way to accomplish
> this in the "one place" manner that you are after.
> CRB
>
> On Wednesday, October 17, 2018 at 1:26:32 PM UTC-5, Woody Gilk wrote:
> > Wouldn't the simple solution be:
> >
> > /** @var $stream Item[] */
> > foreach ($stream as $item) {
> >
> > /* ... */
> >
> > }
> >
> > On Tuesday, October 16, 2018 at 2:02:21 PM UTC-5, Alan Gabriel Bem wrote:
> >> I think you missed my point - What I want to achieve is to *annotate*
> >> somehow that *Stream* is *iterable* over *Item* object and do it in *one
> >> place* instead of every place I'm using `Stream` implementation.
> >>
> >> As I mentioned I could enforce using *Iterator* or *IteratorAggregate*
> >> (as
> >> it opens the way to specify the *Item* as an iterated type... which IDEs
> >> understand), but I don't want to do that because actual iteration method
> >> should depend on how it fits actual implementation. *Traversable* is
> >> enough in this case because, simply speaking, it is marking *Stream* as
> >> an object you can *foreach* on, but not telling how.
> >>>> $item->... // works, *Item* class methods are suggested
signature.asc

Chuck Burgess

unread,
Nov 9, 2018, 1:33:18 PM11/9/18
to php...@googlegroups.com
I don't immediately see a way to accomplish this with tags.

The closest thing to an idea I can come up with here will be dictating a rule to IDEs:  for any variable representing a collection used by a foreach, the IDE must do deep class hierarchy mining to find *any and all potential return mechanisms* in order to enumerate all possible return types based on what's found.  In the OP's case,

/* @var Stream $stream */
foreach ($stream as $item)

that means the IDE must take the Stream interface, find all implementations of it, find all known array/interable return methods, compile a list of defined return types, and treat that list as what potential types that $item might be.

Is there any similar IDE behavior as a precedent for such a rule / expectation?


Stepping back a moment... that /* @var */ usage... seems like I've only ever seen that used to typehint the $item portion of the example, not the $stream portion... $stream should have been figured out earlier when it was first populated, either by its own /* @var */ doc or by the return type of whatever function/method actually populated it.



--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.

Alan Gabriel Bem

unread,
Nov 9, 2018, 3:24:46 PM11/9/18
to PHP Framework Interoperability Group
How about allowing something like

/**
 * @var Item[]
 */

interface Stream extends Traversable
{}

because I am sure this

/**
 * @iterable Item
 */

interface Stream extends Traversable
{}

won't cut it as it is to much of an edge case to be pulled into standard.

@Chuck I will outline your feature idea on Jetbrains' bug tracker after I come up with some compelling examples :)

Larry Garfield

unread,
Nov 10, 2018, 12:02:42 PM11/10/18
to php...@googlegroups.com
Hm, that's not quite what I mean. IDEs would only need to do that level of
deep inspection to generate a doc; I don't need them to generate it in this
case if it's hard.

I'm thinking more like:

/**
* @param Things[] $things
*/
function do_stuff(iterable $things) {
// ...
}

The docblock here technically says "$things is an array of Thing objects",
whereas the type declaration says "$things is an iterable that produces Thing
objects".

What should the @param statement be so that it matches the actual code?

--Larry Garfield

On Friday, November 9, 2018 12:33:00 PM CST Chuck Burgess wrote:
> I don't immediately see a way to accomplish this with tags.
>
> The closest thing to an idea I can come up with here will be dictating a
> rule to IDEs: for any variable representing a collection used by a
> foreach, the IDE must do deep class hierarchy mining to find *any and all
> potential return mechanisms* in order to enumerate all possible return
> types based on what's found. In the OP's case,
>
> /* @var Stream $stream */
> foreach ($stream as $item)
>
> that means the IDE must take the Stream interface, find all implementations
> of it, find all known array/interable return methods, compile a list of
> defined return types, and treat that list as what potential types that
> $item might be.
>
> Is there any similar IDE behavior as a precedent for such a rule /
> expectation?
>
>
> Stepping back a moment... that /* @var */ usage... seems like I've only
> ever seen that used to typehint the $item portion of the example, not the
> $stream portion... $stream should have been figured out earlier when it was
> first populated, either by its own /* @var */ doc or by the return type of
> whatever function/method actually populated it.
>
> CRB
> *about.me/ashnazg <http://about.me/ashnazg>*
signature.asc

Alexey Gopachenko

unread,
Nov 12, 2018, 10:48:47 AM11/12/18
to PHP Framework Interoperability Group
Gents, in PhpStorm we had to specifically implement support for `Stream` implementing some magic interfaces

public final static Map<String, String> ARRAY_VALUE_PROVIDERS;
static{
ARRAY_VALUE_PROVIDERS = new LinkedHashMap<>();
ARRAY_VALUE_PROVIDERS.put("\\Iterator", "#M#C*.current");
ARRAY_VALUE_PROVIDERS.put("\\Traversable", "#M#C*.__iterator");
ARRAY_VALUE_PROVIDERS.put("\\IteratorAggregate", "#E#M#C*.getIterator");
ARRAY_VALUE_PROVIDERS.put("\\ArrayAccess", "#M#C*.offsetGet");
}

public final static Map<String, String> ARRAY_KEY_PROVIDERS;
static{
ARRAY_KEY_PROVIDERS = new LinkedHashMap<>();
ARRAY_KEY_PROVIDERS.put("\\Iterator", "#M#C*.key");
ARRAY_KEY_PROVIDERS.put("\\IteratorAggregate", "#Y#M#C*.getIterator");
ARRAY_KEY_PROVIDERS.put("\\ArrayAccess", "#M#C*.offsetGet");
}


"Scan all Stream supers; if any matches eg Iterator than in foreach value and key TYPES shall be deduced from return types of methods current and key"

this shall work transparently. While this stuff is still "nailed" there as of now, its ok to expect to have some meta defining such behaviors.

Basically, this is one of the dynamic cases I was talking to some of you about - this is an important user story and has to be handled. 

Chuck Burgess

unread,
Nov 12, 2018, 11:01:53 AM11/12/18
to php...@googlegroups.com
In Larry's example, I could envision the usage of the same @param with the code signatures of (array $things) as well as (iterable $things), and both be acceptable.  Both seem to say "a collection Things", with the difference being the expected retrieval mechanism in the code signature typehint (array vs interable).

Granted, this doesn't help us much with the OP's use case, which centers on @returns rather than @params.


Chuck Burgess

unread,
Nov 12, 2018, 11:06:27 AM11/12/18
to php...@googlegroups.com
So here we do indeed have a special IDE implementation to try to deal with the OP's kind of use case.

Again, I can't envision a more standardized way to solve this with tags themselves.


I'll mention this again, with regard to this use case though:
Stepping back a moment... that /* @var */ usage... seems like I've only ever seen that used to typehint the $item portion of the example, not the $stream portion... $stream should have been figured out earlier when it was first populated, either by its own /* @var */ doc or by the return type of whatever function/method actually populated it.

Would using the /* @var */ in this manner not solve this use case?

Larry Garfield

unread,
Nov 12, 2018, 12:34:57 PM11/12/18
to php...@googlegroups.com
On Monday, November 12, 2018 10:06:08 AM CST Chuck Burgess wrote:
> So here we do indeed have a special IDE implementation to try to deal with
> the OP's kind of use case.
>
> Again, I can't envision a more standardized way to solve this with tags
> themselves.
>
>
> I'll mention this again, with regard to this use case though:
> Stepping back a moment... that /* @var */ usage... seems like I've only
> ever seen that used to typehint the $item portion of the example, not the
> $stream portion... $stream should have been figured out earlier when it was
> first populated, either by its own /* @var */ doc or by the return type of
> whatever function/method actually populated it.
>
> Would using the /* @var */ in this manner not solve this use case?
> CRB
> *about.me/ashnazg <http://about.me/ashnazg>*

I agree that, when putting a @var over a foreach, only the $item is really
relevant to document.

My use case may be tangential/related, as I'm talking more about how to
specify "iterable of X" in "the return type of whatever function/method
actually populated it".

While it would be lovely to say that [] implies any "collection", I don't
think PHP will let us do that. Arrays and iterables are not interchangeable,
as there's a few dozen functions that work on arrays but not iterables.

To wit:

/**
* @return int[]
*/
function foo() : iterable {

}

int[] implies "Array of ints", which means calling array_* functions on it is
a totally legit thing to do. But what's returned is an iterable, which means
there's no guarantee that it's an array; it could return any Traversable, or
foo() could be a generator itself. In those cases, the docblock is now
materially wrong and misleading.

I'm not sure what the best solution here is, but I do think we need one.

(And that's before we get into the excitement of an iterable that returns non-
numeric keys, which is also completely valid, so we have to think about
something like Go maps, yeaaaaaahhhh!)

--Larry Garfield
signature.asc

Woody Gilk

unread,
Nov 12, 2018, 12:38:46 PM11/12/18
to php...@googlegroups.com
Could we use:

@return iterable<int>

?

Also generators can return key => value:

@return iterable<int,User>

Would imply:

yield $key => $user;


--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.

Chuck Burgess

unread,
Dec 8, 2018, 3:44:14 PM12/8/18
to php...@googlegroups.com
I'm thinking this use case will probably need to rely on some kind of collection/generics syntax.  Do we know if PhpStorm / PHPStan / etc have looked at syntaxes for this use case?


Message has been deleted

Alessandro Lai

unread,
Dec 11, 2018, 3:17:20 AM12/11/18
to PHP Framework Interoperability Group
This is the issue traking the implementation on PHPStan: https://github.com/phpstan/phpstan/issues/652
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+unsubscribe@googlegroups.com.

To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/39547303.NTl641kpUQ%40vulcan.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+unsubscribe@googlegroups.com.

Chuck Burgess

unread,
Dec 11, 2018, 8:52:31 AM12/11/18
to php...@googlegroups.com
Yes, out of scope, at least here at the beginning.  Once we get a baseline draft covering the minimal "already in use old stuff" covered, I'm expecting us to open up topics on "new stuff" to add, generics included.  Not that I'm convinced we'll get that one nailed down to enough agreement that we can for sure include it in this round of PSRs... my point is only that I wish to not let such a future/forward looking discussion derail the rest of the PSR content.


On Mon, Dec 10, 2018 at 9:41 PM Jan Tvrdík <php...@jantvrdik.com> wrote:
Yes, it needs generics syntax, making it currently intentionally out-of-scope.
If you don't mind ugly hacks with unclear semantics that may stop working in the future, you can also use `iterable|User[]`.
Reply all
Reply to author
Forward
0 new messages