My project has many "collections" where we loop over an iterable and I'd like for the item type to be inferred properly. The collection class implements Traversable and I have a getIterator method using template generics in the vardocs. This works if I use the direct instantiation for the collection instance or if I use getIterator() in the foreach loop, but not both at once.
Here is a full minimal reproduction example:
.phpstorm.meta.php:
<?php
namespace PHPSTORM_META {
override(\Mage::getSingleton(), map([
'movement_collector' => \Model_Movement_Collector::class,
'delivery_collection' => \Delivery_Collection::class,
'delivery_collection2' => '\Delivery_Collection<\Model_Delivery>',
]));
}
index.php:
<?php
final class Mage
{
/**
* @param string $modelClass
* @param array $arguments
* @return object|false
*/
public static function getSingleton($modelClass = '', array $arguments = [])
{
switch ($modelClass) {
case 'movement_collector':
$className = \Model_Movement_Collector::class;
break;
case 'delivery_collection':
$className = \Delivery_Collection::class;
break;
default:
return false;
}
return new $className();
}
}
#[\AllowDynamicProperties]
class Varien_Object { }
abstract class Model_Abstract extends Varien_Object { }
/**
* @template-covariant T of Varien_Object
* @template-implements IteratorAggregate<string|int,T>
*/
class Varien_Data_Collection implements IteratorAggregate
{
/**
* @return Traversable<string|int,T>
*/
public function getIterator(): Traversable
{
$className = get_class($this).'_Testing';
return new ArrayIterator([
'item1' => new $className(),
'item2' => new $className(),
]);
}
/**
* @param string|int $idValue
* @return T|null
*/
public function getItemById($idValue)
{
$className = get_class($this).'_Testing';
return new $className();
}
}
/**
* @template T of Model_Delivery
* @extends Varien_Data_Collection<Model_Delivery>
*/
class Delivery_Collection extends Varien_Data_Collection
{
public function addFilter(string $field, mixed $value, string $conditionType = 'eq'): self
{
return $this;
}
}
class Model_Delivery extends Model_Abstract
{
public function doSomething(string $thing): string
{
return "Doing $thing with delivery";
}
}
// Loop without getIterator() returns unknown type
$collection = Mage::getSingleton('delivery_collection');
$collection->addFilter('foo', 'bar');
foreach ($collection as $delivery1) { // <- unknown type
echo $delivery1->doSomething('something');
}
// Works when calling getIterator()
foreach ($collection->getIterator() as $delivery2) { // <- Model_Delivery
echo $delivery2->doSomething('something');
}
// Works without calling getIterator() when using direct class instantiation
$collection2 = new Delivery_Collection();
$collection2->addFilter('foo', 'bar');
foreach ($collection2 as $delivery3) { // <- Model_Delivery
echo $delivery3->doSomething('something');
}
// Testing override as '\Delivery_Collection<\Model_Delivery>' - does not work
$collection3 = Mage::getSingleton('delivery_collection2');
$collection3->addFilter('foo', 'bar');
foreach ($collection3 as $delivery4) { // <- unknown type
echo $delivery4->doSomething('something');
}
