Skip to content

Commit 7e56434

Browse files
committed
Implement generic collections and enhance type validation; add tests for Shopware entity hierarchy and generic placeholder binding
1 parent e2e95af commit 7e56434

12 files changed

Lines changed: 332 additions & 6 deletions

src/Internal/Checker/ParamChecker.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,13 @@ private static function resolveTemplateParam(
549549
return null;
550550
}
551551

552+
if ($expectedTypeNode instanceof IdentifierTypeNode && $expectedTypeNode->name === $templateName) {
553+
$inferredType = TemplateManager::inferTypeFromValue($val);
554+
TemplateManager::bindTemplate($function, $targetObj, $templateName, $inferredType);
555+
556+
return null;
557+
}
558+
552559
if ($isVariadic && \is_array($val)) {
553560
foreach ($val as $idx => $item) {
554561
$err = $registry->validate($item, $expectedTypeNode, $function . '(): Argument $' . $paramName . '[' . $idx . '] (template ' . $templateName . ' = ' . $expectedTypeNode . ')');

src/Resolver/SpecialTypeResolver.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
use TypePHP\Internal\TypeFormatter;
3535

3636
/**
37-
* @internal Resolves special type identifiers (self, static, parent, FQCNs) against Reflection or file contexts.
37+
* Resolves special type identifiers (self, static, parent, FQCNs) against Reflection or file contexts.
38+
*
39+
* @internal
3840
*/
3941
final class SpecialTypeResolver
4042
{
@@ -980,7 +982,7 @@ public static function resolveFqcnForFile(string $name, string $file): string
980982
/**
981983
* Checks if a type name is a built-in PHP or PHPDoc type keyword.
982984
*/
983-
private static function isBuiltInTypeKeyword(string $name): bool
985+
public static function isBuiltInTypeKeyword(string $name): bool
984986
{
985987
return isset(self::BUILTIN_TYPE_KEYWORDS[strtolower($name)]);
986988
}

src/Resolver/TemplateManager.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,21 @@ private static function bindSingleTemplateArgument(
352352
return null;
353353
}
354354

355+
$expectedTypeNode = $typeNode->genericTypes[$index];
356+
357+
if ($expectedTypeNode instanceof IdentifierTypeNode) {
358+
$isBuiltIn = SpecialTypeResolver::isBuiltInTypeKeyword($expectedTypeNode->name);
359+
$isRealType = class_exists($expectedTypeNode->name) || interface_exists($expectedTypeNode->name) || enum_exists($expectedTypeNode->name) || trait_exists($expectedTypeNode->name);
360+
361+
if (! $isBuiltIn && ! $isRealType) {
362+
return null;
363+
}
364+
}
365+
355366
if (self::$instanceTemplateBindings === null) {
356367
self::$instanceTemplateBindings = new WeakMap();
357368
}
358369

359-
$expectedTypeNode = $typeNode->genericTypes[$index];
360370
$usageVariance = $typeNode->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT;
361371
$declaredVariance = $classVariances[$templateTag->name] ?? GenericTypeNode::VARIANCE_INVARIANT;
362372

@@ -505,8 +515,17 @@ private static function bindInheritedGenericTag(
505515
if (isset($genericTypeNode->genericTypes[$idx])) {
506516
$resolved = self::resolveTypeNodeAst($genericTypeNode->genericTypes[$idx], $hierClass);
507517

508-
if ($resolved instanceof IdentifierTypeNode && isset($declaredTemplateNames[$resolved->name])) {
509-
continue;
518+
if ($resolved instanceof IdentifierTypeNode) {
519+
$isBuiltIn = SpecialTypeResolver::isBuiltInTypeKeyword($resolved->name);
520+
$isRealType = class_exists($resolved->name) || interface_exists($resolved->name) || enum_exists($resolved->name) || trait_exists($resolved->name);
521+
522+
if (! $isBuiltIn && ! $isRealType) {
523+
continue;
524+
}
525+
526+
if (isset($declaredTemplateNames[$resolved->name])) {
527+
continue;
528+
}
510529
}
511530

512531
$bindings[$templateName] = $resolved;
@@ -837,4 +856,4 @@ private static function getTypeParserComponents(): array
837856

838857
return [$typeParser, $lexer];
839858
}
840-
}
859+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Collections;
6+
7+
use Closure;
8+
use Countable;
9+
use IteratorAggregate;
10+
use Traversable;
11+
12+
/**
13+
* @template TElement
14+
* @template TKey of array-key = array-key
15+
*
16+
* @implements IteratorAggregate<TKey, TElement>
17+
*/
18+
abstract class ShopwareCollection implements IteratorAggregate, Countable
19+
{
20+
/**
21+
* @var array<TKey, TElement>
22+
*/
23+
protected array $elements = [];
24+
25+
/**
26+
* Directly populates elements without method contract inference
27+
*/
28+
public function setElementsDirectly(array $elements): void
29+
{
30+
$this->elements = $elements;
31+
}
32+
33+
/**
34+
* @param TElement $element
35+
*/
36+
protected function validateType(mixed $element): void
37+
{
38+
}
39+
40+
/**
41+
* @param Closure(TElement): bool $closure
42+
*/
43+
public function filter(Closure $closure): static
44+
{
45+
$filtered = array_filter($this->elements, $closure);
46+
47+
$instance = new static();
48+
$instance->setElementsDirectly($filtered);
49+
50+
return $instance;
51+
}
52+
53+
/**
54+
* @return Traversable<TElement>
55+
*/
56+
public function getIterator(): Traversable
57+
{
58+
yield from $this->elements;
59+
}
60+
61+
public function count(): int
62+
{
63+
return \count($this->elements);
64+
}
65+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Collections;
6+
7+
/**
8+
* @template TElement of object
9+
*
10+
* @extends ShopwareCollection<TElement, string>
11+
*/
12+
class ShopwareEntityCollection extends ShopwareCollection
13+
{
14+
/**
15+
* @param iterable<TElement> $elements
16+
*/
17+
public function __construct(iterable $elements = [])
18+
{
19+
foreach ($elements as $key => $element) {
20+
$this->validateType($element);
21+
$keyStr = \is_string($key) ? $key : ('id_' . $key);
22+
$this->elements[$keyStr] = $element;
23+
}
24+
}
25+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Collections;
6+
7+
/**
8+
* @template TElement of object
9+
*
10+
* @extends ShopwareEntityCollection<TElement>
11+
*/
12+
class ShopwareEntitySearchResult extends ShopwareEntityCollection
13+
{
14+
public function __construct(?ShopwareEntityCollection $entities = null)
15+
{
16+
parent::__construct($entities ?? []);
17+
}
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Collections;
6+
7+
use TypePHP\Tests\Fixtures\Domain\Dog;
8+
9+
/**
10+
* Concrete Leaf Class binding TElement to Dog
11+
*
12+
* @extends ShopwareEntitySearchResult<Dog>
13+
*/
14+
class SpecificDogSearchResult extends ShopwareEntitySearchResult
15+
{
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Generics;
6+
7+
/**
8+
* Child class passing generic placeholder TElement up to MidGenericBag<TElement>
9+
*
10+
* @template TElement of object
11+
*
12+
* @extends MidGenericBag<TElement>
13+
*/
14+
class ChildGenericBag extends MidGenericBag
15+
{
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Generics;
6+
7+
/**
8+
* Middle class passing generic placeholder TElement up to RootGenericBag<TElement>
9+
*
10+
* @template TElement of object
11+
*
12+
* @extends RootGenericBag<TElement>
13+
*/
14+
abstract class MidGenericBag extends RootGenericBag
15+
{
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Generics;
6+
7+
/**
8+
* @template TElement of object
9+
*/
10+
abstract class RootGenericBag
11+
{
12+
/**
13+
* @param TElement $element
14+
*/
15+
public function addItem(object $element): bool
16+
{
17+
return true;
18+
}
19+
}

0 commit comments

Comments
 (0)