feat: support generics of PHP internal classes - #833
Merged
Conversation
A constructor declaring templates of its own, such as a factory returning `SomeCollection<T>` out of a `list<T>`, never had `T` bound to the generic of the type being mapped. The placeholder stayed in the parameters of the constructor and reached the mapper, which asserted on it, and took any value at all once assertions were disabled. The templates are now inferred from the type the class definition carries, the way a registered converter already has its own inferred from the type it converts to. A parameter typed `list<T>` becomes `list<SomeClass>` when `SomeCollection<SomeClass>` is mapped. A constructor marked with the `Constructor` attribute needed more: the templates a method declares for itself were collected nowhere, the function definition repository reading them from a docblock where the method definition builder never did. The very same method therefore worked when given to `MapperBuilder::registerConstructor()` and failed when marked, on `cannot parse unknown symbol T`. They are now resolved next to the generics of the class declaring the method, both being vacant types for that method alone.
The mapper learns about generics by reading the `@template` annotations
of a class docblock. Classes internal to PHP, or provided by an
extension, carry no such docblock and cannot be modified, so the type of
the values they hold could never be described.
Their signature is now known to the mapper, and the ones a source can be
mapped to without ambiguity are built with no configuration:
```php
final class Article
{
public function __construct(
public string $title,
/** @var \Ds\Set<string> */
public \Ds\Set $tags,
) {}
}
(new \CuyZ\Valinor\MapperBuilder())->mapper()->map(Article::class, [
'title' => 'Some article',
'tags' => ['php', 'valinor'],
]);
```
An array builds `ArrayObject`, `ArrayIterator` and `Ds\Map`, a list the
`Spl*` and `Ds\*` collections, a key and a value `Ds\Pair`. The others
are described but never built, until a constructor is registered for
them: `SplHeap` is abstract, `SplObjectStorage` is keyed by objects, the
priority queues need a priority for each value, `Generator`, `WeakMap`
and `WeakReference` have no meaningful source shape, and the interfaces
are given an implementation through `MapperBuilder::infer()`.
Every template declares a default, so that these classes stay usable
without generics, as they appear bare in countless existing signatures.
Mapping to one without them now reports that `mixed` is not allowed in
strict mode, where `map(SplStack::class, [])` previously returned an
empty stack.
`DateTime`, `DateTimeImmutable` and `DateTimeZone` each had a builder
factory of their own doing the same job; both are dropped, their
constructors joining the list. A built-in constructor applies when no
registered one takes a single argument, so customising the supported
date formats no longer adds one back when a registered constructor
already takes the source.
romm
force-pushed
the
feat/support-internal-class-generics
branch
from
August 10, 2026 21:48
16c706d to
d9d5f02
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The mapper learns about generics by reading the
@templateannotations of a class docblock. Classes internal to PHP, or provided by an extension, carry no such docblock and cannot be modified, so the type of the values they hold could never be described.Their signature is now known to the mapper, and the ones a source can be mapped to without ambiguity are built with no configuration:
An array builds
ArrayObject,ArrayIteratorandDs\Map, a list theSpl*andDs\*collections, a key and a valueDs\Pair. The others are described but never built, until a constructor is registered for them:SplHeapis abstract,SplObjectStorageis keyed by objects, the priority queues need a priority for each value,Generator,WeakMapandWeakReferencehave no meaningful source shape, and the interfaces are given an implementation throughMapperBuilder::infer().Every template declares a default, so that these classes stay usable without generics, as they appear bare in countless existing signatures. Mapping to one without them now reports that
mixedis not allowed in strict mode, wheremap(SplStack::class, [])previously returned an empty stack.DateTime,DateTimeImmutableandDateTimeZoneeach had a builder factory of their own doing the same job; both are dropped, their constructors joining the list. A built-in constructor applies when no registered one takes a single argument, so customising the supported date formats no longer adds one back when a registered constructor already takes the source.Fixes #529