Skip to content

Commit b9f00e6

Browse files
authored
Internal improvements 9 (#34)
* Add higher-order generic array and list transformers to GenericCallableService and enhance tests for type validation * Refactor and cleanup classes and make it more maitanable * Enhance tests for ParamChecker and ReturnChecker with additional scenarios and validations * Enhance GeneratorChecker and InlineChecker with additional validation methods and improve unit tests for better coverage * Refactor PropertyHookInjector and FunctionContractInjector for improved handling of hooks and docblock annotations - Simplified the process of skipping injection for properties with no hooks or specific ignore tags in PropertyHookInjector. - Extracted logic for processing 'get' and 'set' hooks into separate methods for better readability and maintainability. - Enhanced tests for PropertyHookInjector to cover various scenarios including handling of ignore tags and custom parameter names. - Updated FunctionContractInjector tests to ensure proper injection of checks based on docblock annotations, including support for generator functions and lifecycle methods. - Improved test structure for clarity and consistency across different scenarios. * Refactor ContractParserTest to enhance test coverage and organization for function, property, and magic method parsing * Refactor tests for improved readability and structure - Updated ContractParserTest to enhance readability by adding line breaks for better separation of assertions. - Enhanced SpecialTypeResolverTest with additional test cases for handling $this identity checks and resolving special identifier keywords. - Improved TemplateManagerTest by restructuring tests into descriptive groups, adding tests for inherited template resolution, and validating variance rules. - Expanded TemplateSubstitutorTest to include substitutions for CallableTypeNode, ConditionalTypeNode, and ObjectShapeNode, ensuring comprehensive coverage of template placeholder substitutions. * Enhance TemplateSubstitutorTest by adding a line break for improved readability in the callable type substitution test * Improve generics documentations * Update test suites * Fix php 8.2 ci failures due to low pestphp version not currenly supporting modern testing utilities * fix remaining failing test * Reorganize test suites
1 parent 7de5e49 commit b9f00e6

42 files changed

Lines changed: 5054 additions & 2604 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/.vitepress/config.mts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineConfig({
99
nav: [
1010
{ text: 'Home', link: '/' },
1111
{ text: 'Documentation', link: '/getting-started/installation' },
12-
{ text: 'Generics', link: '/generics/generics-and-bounds' },
12+
{ text: 'Generics', link: '/generics/basics-and-bounds' },
1313
{ text: 'CLI', link: '/getting-started/cli-commands' },
1414
{ text: 'FAQ', link: '/troubleshooting' },
1515
{ text: 'GitHub', link: 'https://github.com/typephp-php/typephp' }
@@ -24,6 +24,16 @@ export default defineConfig({
2424
{ text: 'CLI Commands', link: '/getting-started/cli-commands' },
2525
]
2626
},
27+
{
28+
text: 'Runtime Generics',
29+
items: [
30+
{ text: 'Basics & Bounds', link: '/generics/basics-and-bounds' },
31+
{ text: 'Inheritance & Traits', link: '/generics/inheritance-and-traits' },
32+
{ text: 'Reified Generics & State', link: '/generics/reified-generics' },
33+
{ text: 'Advanced Types & Callables', link: '/generics/advanced-generics' },
34+
{ text: 'Demystifying Variance', link: '/generics/variance' },
35+
]
36+
},
2737
{
2838
text: 'Enforcement Boundaries',
2939
items: [
@@ -44,12 +54,6 @@ export default defineConfig({
4454
{ text: 'Type Aliases', link: '/supported-types/type-aliases' },
4555
]
4656
},
47-
{
48-
text: 'Runtime Generics',
49-
items: [
50-
{ text: 'Generics & Bounds', link: '/generics/generics-and-bounds' },
51-
]
52-
},
5357
{
5458
text: 'Advanced & Architecture',
5559
items: [

docs/.vitepress/theme/custom.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.VPDocAsideOutline .outline-link,
2+
.VPDocOutlineItem .outline-link {
3+
white-space: normal !important;
4+
line-height: 1.5 !important;
5+
word-break: break-word !important;
6+
padding-top: 6px !important;
7+
padding-bottom: 6px !important;
8+
display: block !important;
9+
}
10+
11+
.VPDocAsideOutline .outline-item,
12+
.VPDocOutlineItem,
13+
.VPDocAsideOutline ul > li {
14+
margin-top: 6px !important;
15+
margin-bottom: 8px !important;
16+
}
17+
18+
.VPDocAsideOutline .nested {
19+
padding-left: 14px !important;
20+
margin-top: 4px !important;
21+
margin-bottom: 4px !important;
22+
}
23+
24+
.VPSidebarItem.is-link .text {
25+
white-space: normal !important;
26+
line-height: 1.4 !important;
27+
word-break: break-word !important;
28+
}
29+
30+
@media (min-width: 1280px) {
31+
.VPDoc.has-aside .aside-container {
32+
width: 270px !important;
33+
}
34+
}

docs/.vitepress/theme/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import DefaultTheme from 'vitepress/theme'
2+
import type { Theme } from 'vitepress'
3+
import './custom.css'
4+
5+
export default {
6+
extends: DefaultTheme,
7+
} satisfies Theme

docs/generics/advanced-generics.md

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# Advanced Types & Callables
2+
3+
TypePHP allows combining generic templates (`T`, `K`, `V`) with high-level type algebra, including Higher-Order Callables, Lazy Iterables, Generators, Conditionals, Unions, Intersections, and Deeply Nested Containers.
4+
5+
---
6+
7+
## Generic Callables with Template Substitution
8+
9+
When a function accepts a generic callback (`@param callable(T): T $transformer`), TypePHP dynamically substitutes `T` with the inferred concrete type before callback invocation:
10+
11+
```php
12+
/**
13+
* Generic transformer function
14+
*
15+
* @template T
16+
*
17+
* @param callable(T): T $transformer
18+
* @param T $input
19+
*
20+
* @return T
21+
*/
22+
function transformValue(callable $transformer, mixed $input): mixed
23+
{
24+
return $transformer($input);
25+
}
26+
27+
// 1. Valid Call: Infers T = int, validates callback argument (int) and return (int)
28+
$double = fn (int $x): int => $x * 2;
29+
transformValue($double, 21); // Returns 42
30+
31+
// 2. Invalid Callback Return: T is inferred as int (from 10), but callback returns string ('invalid')
32+
$badReturn = fn (int $x): string => 'invalid';
33+
transformValue($badReturn, 10);
34+
// Throws: TypeError: transformValue(): Return value must be of type int, string 'invalid' returned
35+
```
36+
37+
---
38+
39+
## Higher-Order Generic Transformers (`array<K, V>` & `callable(V): V2`)
40+
41+
TypePHP pre-infers template parameters across multiple arguments simultaneously:
42+
43+
```php
44+
/**
45+
* Higher-order array mapper with 3 generic parameters
46+
*
47+
* @template K of array-key
48+
* @template V
49+
* @template V2
50+
*
51+
* @param callable(V): V2 $callback
52+
* @param array<K, V> $array
53+
*
54+
* @return array<K, V2>
55+
*/
56+
function mapArray(callable $callback, array $array): array
57+
{
58+
$result = [];
59+
foreach ($array as $key => $value) {
60+
$result[$key] = $callback($value);
61+
}
62+
63+
return $result;
64+
}
65+
66+
$stringify = fn (int $n): string => "val_{$n}";
67+
68+
// 1. Valid Call: Infers K = string, V = int, V2 = string
69+
$res = mapArray($stringify, ['a' => 10, 'b' => 20]);
70+
// Returns: ['a' => 'val_10', 'b' => 'val_20']
71+
72+
// 2. Invalid Call: 'invalid_string' violates inferred V = int on function entry!
73+
mapArray($stringify, ['item1' => 10, 'item2' => 'invalid_string']);
74+
// Throws: TypeError: mapArray(): Argument $array['item2'] must be of type int, string 'invalid_string' given
75+
```
76+
77+
---
78+
79+
## Generic Iterables & Generators (`iterable<T>` & `Generator<K, V>`)
80+
81+
TypePHP substitutes template parameters into iterators, validating yielded items, keys, and generator inputs (`$gen->send()`) lazily during execution:
82+
83+
```php
84+
/**
85+
* @template T
86+
*
87+
* @param iterable<T> $stream
88+
* @param T $sample
89+
*
90+
* @return list<T>
91+
*/
92+
function collectStream(iterable $stream, mixed $sample): array
93+
{
94+
$collected = [];
95+
foreach ($stream as $item) {
96+
$collected[] = $item;
97+
}
98+
return $collected;
99+
}
100+
101+
// Infers T = int from $sample (1)
102+
$iterator = new ArrayIterator([10, 'invalid', 30]);
103+
collectStream($iterator, 1);
104+
// Throws: TypeError: Iterator $stream value must be of type int, string 'invalid' given
105+
```
106+
107+
### Generic Interactive Generators (`Generator<int, T, T, void>` / `TSend`)
108+
109+
When `TSend` uses a generic template `T`, `$gen->send()` is dynamically validated against the bound generic type:
110+
111+
```php
112+
/**
113+
* @template T
114+
*
115+
* @param T $initial
116+
*
117+
* @return Generator<int, T, T, void>
118+
*/
119+
function streamInteractive(mixed $initial): Generator
120+
{
121+
$current = $initial;
122+
for ($i = 0; $i < 3; $i++) {
123+
$input = yield $i => $current;
124+
if ($input !== null) {
125+
$current = $input;
126+
}
127+
}
128+
}
129+
130+
// Initial value 10 locks T = int
131+
$gen = streamInteractive(10);
132+
$gen->current();
133+
134+
$gen->send(20); // Valid (20 is int)
135+
136+
$gen->send('invalid'); // Invalid: string violates T = int!
137+
// Throws: TypeError: streamInteractive(): Generator sent value (TSend) must be of type int, string 'invalid' given
138+
```
139+
140+
---
141+
142+
## Conditional Return Types with Generics (`(T is Dog ? A : B)`)
143+
144+
TypePHP dynamically evaluates conditional return types based on generic templates:
145+
146+
```php
147+
/**
148+
* @template T
149+
*
150+
* @param T $input
151+
* @param mixed $output
152+
*
153+
* @return (T is Dog ? positive-int : non-empty-string)
154+
*/
155+
function processInput(mixed $input, mixed $output): mixed
156+
{
157+
return $output;
158+
}
159+
160+
// 1. T is inferred as Dog -> Evaluates return contract as positive-int
161+
processInput(new Dog(), 100); // Valid
162+
163+
// 2. T is inferred as Cat -> Evaluates return contract as non-empty-string
164+
processInput(new Cat(), 'valid_string'); // Valid
165+
166+
processInput(new Cat(), ''); // Invalid: empty string violates non-empty-string
167+
// Throws: TypeError: processInput(): Return value must be of type non-empty-string
168+
```
169+
170+
### Negated Generic Conditionals (`(T is not Dog ? A : B)`)
171+
172+
```php
173+
/**
174+
* @template T
175+
*
176+
* @param T $input
177+
* @param mixed $result
178+
*
179+
* @return (T is not Dog ? non-empty-string : positive-int)
180+
*/
181+
function processNegated(mixed $input, mixed $result): mixed
182+
{
183+
return $result;
184+
}
185+
186+
processNegated(new Cat(), 'valid_text'); // Valid (Cat is not Dog -> non-empty-string)
187+
processNegated(new Dog(), 42); // Valid (Dog is Dog -> positive-int)
188+
```
189+
190+
---
191+
192+
## Generics with Unions and Intersections
193+
194+
TypePHP fully supports combining generic structures with Union (`|`) and Intersection (`&`) types:
195+
196+
### Generic Containers Holding Unions (`Collection<Dog|Cat>`)
197+
198+
```php
199+
/** @var Collection<Dog|Cat> $animals */
200+
$animals = new Collection();
201+
202+
$animals->add(new Dog()); // Valid
203+
$animals->add(new Cat()); // Valid
204+
205+
$animals->add(new Car()); // Invalid: Car is neither Dog nor Cat
206+
// Throws: TypeError: Collection::add(): Argument $item (template T = Dog|Cat) must be of type (Dog | Cat)
207+
```
208+
209+
### Unions of Generic Containers (`Producer<Dog> | Producer<Cat>`)
210+
211+
```php
212+
/**
213+
* @param Producer<Dog>|Producer<Cat> $producer
214+
*/
215+
function handleAnimalProducer(Producer $producer): void
216+
{
217+
// ...
218+
}
219+
220+
handleAnimalProducer(new Producer(new Dog())); // Valid
221+
handleAnimalProducer(new Producer(new Cat())); // Valid
222+
223+
handleAnimalProducer(new Producer(new Car())); // Invalid
224+
// Throws: TypeError: Argument $producer must be of type Producer<Dog>|Producer<Cat>
225+
```
226+
227+
### Generic Containers Holding Intersections (`Collection<Countable & ArrayAccess>`)
228+
229+
Enforce that generic items must implement multiple interfaces simultaneously:
230+
231+
```php
232+
/** @var Collection<Countable&ArrayAccess> $collections */
233+
$collections = new Collection();
234+
235+
$collections->add(new CountableArrayAccess()); // Valid (Implements both)
236+
237+
$collections->add(new CountableOnly()); // Invalid (Fails ArrayAccess interface)
238+
// Throws: TypeError: Argument $item must be of type Countable&ArrayAccess
239+
```
240+
241+
### Complex Unions of Intersections in Generics
242+
243+
You can combine parenthesized unions and intersections inside generic parameters:
244+
245+
```php
246+
/** @var Collection<(Countable&ArrayAccess)|(Iterator&Countable)> $payload */
247+
$payload = new Collection();
248+
249+
$payload->add(new CountableArrayAccess()); // Valid
250+
$payload->add(new ArrayIterator([1, 2])); // Valid
251+
```
252+
253+
---
254+
255+
## Deeply Nested Generics (`Collection<Producer<Dog>>`)
256+
257+
TypePHP recursively evaluates deeply nested generic structures down to any depth:
258+
259+
```php
260+
/** @var Collection<Producer<Dog>> $producers */
261+
$producers = new Collection();
262+
263+
// Valid Addition
264+
$producers->add(new Producer(new Dog()));
265+
266+
// Invalid Addition (Producer holding Car instead of Dog)
267+
$producers->add(new Producer(new Car()));
268+
// Throws: TypeError: Argument $item must be an instance of Producer<Dog>
269+
```

0 commit comments

Comments
 (0)