Skip to content

Commit e398bbb

Browse files
committed
Add generics support to PHP
Implement reified generics with type parameter declarations on classes, interfaces, traits, and functions. Includes type constraint enforcement, variance checking, type inference, reflection API support, and comprehensive test suite.
1 parent 2fd3433 commit e398bbb

59 files changed

Lines changed: 25328 additions & 15596 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Generic class: basic declaration and instantiation
3+
--FILE--
4+
<?php
5+
class Box<T> {
6+
private $value;
7+
public function __construct(T $value) { $this->value = $value; }
8+
public function get(): T { return $this->value; }
9+
}
10+
11+
$box = new Box(42);
12+
echo $box->get() . "\n";
13+
14+
$box2 = new Box("hello");
15+
echo $box2->get() . "\n";
16+
?>
17+
--EXPECT--
18+
42
19+
hello
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Generic class: constrained type parameter with upper bound
3+
--FILE--
4+
<?php
5+
class TypedCollection<T: Countable> {
6+
private array $items = [];
7+
public function add(T $item): void {
8+
$this->items[] = $item;
9+
}
10+
public function count(): int {
11+
return count($this->items);
12+
}
13+
}
14+
15+
echo "TypedCollection declared\n";
16+
17+
$c = new TypedCollection();
18+
echo "Instantiated\n";
19+
?>
20+
--EXPECT--
21+
TypedCollection declared
22+
Instantiated
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
Generic class: inheritance with bound type arguments
3+
--FILE--
4+
<?php
5+
class Box<T> {
6+
private $value;
7+
public function __construct(T $value) { $this->value = $value; }
8+
public function get(): T { return $this->value; }
9+
}
10+
11+
class IntBox extends Box<int> {}
12+
13+
$ib = new IntBox(99);
14+
echo $ib->get() . "\n";
15+
16+
try {
17+
$bad = new IntBox("not an int");
18+
} catch (TypeError $e) {
19+
echo "TypeError caught\n";
20+
}
21+
?>
22+
--EXPECT--
23+
99
24+
TypeError caught
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Generic class: multiple type parameters
3+
--FILE--
4+
<?php
5+
class Pair<A, B> {
6+
private $first;
7+
private $second;
8+
public function __construct(A $first, B $second) {
9+
$this->first = $first;
10+
$this->second = $second;
11+
}
12+
public function getFirst(): A { return $this->first; }
13+
public function getSecond(): B { return $this->second; }
14+
}
15+
16+
$pair = new Pair<string, int>("hello", 42);
17+
echo $pair->getFirst() . "\n";
18+
echo $pair->getSecond() . "\n";
19+
?>
20+
--EXPECT--
21+
hello
22+
42
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Generic class: new with explicit type arguments
3+
--FILE--
4+
<?php
5+
class Box<T> {
6+
private $value;
7+
public function __construct(T $value) { $this->value = $value; }
8+
public function get(): T { return $this->value; }
9+
}
10+
11+
$intBox = new Box<int>(42);
12+
echo $intBox->get() . "\n";
13+
14+
$strBox = new Box<string>("hello");
15+
echo $strBox->get() . "\n";
16+
?>
17+
--EXPECT--
18+
42
19+
hello
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Generic class: type enforcement on constructor parameter
3+
--FILE--
4+
<?php
5+
class Box<T> {
6+
private $value;
7+
public function __construct(T $value) { $this->value = $value; }
8+
public function get(): T { return $this->value; }
9+
}
10+
11+
try {
12+
$box = new Box<int>("not an int");
13+
} catch (TypeError $e) {
14+
echo "TypeError: " . $e->getMessage() . "\n";
15+
}
16+
?>
17+
--EXPECTF--
18+
TypeError: Box::__construct(): Argument #1 ($value) must be of type int, string given, called in %s on line %d
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Generic class: clone preserves generic type arguments
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
class Box<T> {
8+
public T $value;
9+
public function __construct(T $value) { $this->value = $value; }
10+
}
11+
12+
$a = new Box<int>(42);
13+
$b = clone $a;
14+
15+
echo $b->value . "\n";
16+
17+
// Clone should preserve generic args — assigning wrong type should fail
18+
try {
19+
$b->value = "hello";
20+
} catch (TypeError $e) {
21+
echo $e->getMessage() . "\n";
22+
}
23+
24+
echo "OK\n";
25+
?>
26+
--EXPECTF--
27+
42
28+
Cannot assign string to property Box::$value of type int
29+
OK
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
Generic closures and arrow functions
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
// Generic closure
8+
$identity = function<T>(T $value): T {
9+
return $value;
10+
};
11+
12+
echo $identity(42) . "\n"; // 42
13+
echo $identity("hello") . "\n"; // hello
14+
15+
// Generic arrow function
16+
$wrap = fn<T>(T $x): array => [$x];
17+
var_dump($wrap(42)); // array(1) { [0]=> int(42) }
18+
var_dump($wrap("hello")); // array(1) { [0]=> string(5) "hello" }
19+
20+
// Generic closure with constraint
21+
$count = function<T: Countable>(T $item): int {
22+
return count($item);
23+
};
24+
25+
echo $count(new ArrayObject([1, 2, 3])) . "\n"; // 3
26+
27+
// Multiple type params
28+
$pair = fn<A, B>(A $a, B $b): array => [$a, $b];
29+
var_dump($pair(1, "two")); // array(2) { [0]=> int(1) [1]=> string(3) "two" }
30+
31+
// Reflection on generic closures
32+
$rf = new ReflectionFunction($identity);
33+
var_dump($rf->isGeneric()); // true
34+
$params = $rf->getGenericParameters();
35+
echo "identity generic params: " . count($params) . "\n";
36+
echo " T: " . $params[0]->getName() . "\n";
37+
38+
// Reflection on generic arrow function
39+
$rf2 = new ReflectionFunction($wrap);
40+
var_dump($rf2->isGeneric()); // true
41+
42+
// Non-generic closure for comparison
43+
$plain = function(int $x): int { return $x * 2; };
44+
$rf3 = new ReflectionFunction($plain);
45+
var_dump($rf3->isGeneric()); // false
46+
47+
echo "OK\n";
48+
?>
49+
--EXPECT--
50+
42
51+
hello
52+
array(1) {
53+
[0]=>
54+
int(42)
55+
}
56+
array(1) {
57+
[0]=>
58+
string(5) "hello"
59+
}
60+
3
61+
array(2) {
62+
[0]=>
63+
int(1)
64+
[1]=>
65+
string(3) "two"
66+
}
67+
bool(true)
68+
identity generic params: 1
69+
T: T
70+
bool(true)
71+
bool(false)
72+
OK
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Generic class: constraint enforcement at instantiation
3+
--FILE--
4+
<?php
5+
class TypedCollection<T: Countable> {
6+
private array $items = [];
7+
public function add(T $item): void {
8+
$this->items[] = $item;
9+
}
10+
}
11+
12+
// ArrayObject implements Countable — should work
13+
$c = new TypedCollection<ArrayObject>();
14+
echo "ArrayObject accepted\n";
15+
16+
// string does NOT implement Countable — should fatal error
17+
$c2 = new TypedCollection<string>();
18+
echo "Should not reach here\n";
19+
?>
20+
--EXPECTF--
21+
ArrayObject accepted
22+
23+
Fatal error: Generic type argument #1 must implement Countable, string given in %s on line %d
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
Generic class: contravariance (in) compile error when used in return position
3+
--FILE--
4+
<?php
5+
// in T should not be allowed in method return position
6+
class BadConsumer<in T> {
7+
public function get(): T { return null; }
8+
}
9+
?>
10+
--EXPECTF--
11+
Fatal error: Contravariant type parameter T of class BadConsumer cannot be used in return type of method BadConsumer::get() (only parameter types allowed) in %s on line %d

0 commit comments

Comments
 (0)