Skip to content

Conversation

@romm
Copy link
Member

@romm romm commented Jan 23, 2026

It can sometimes be useful for an inferred interface implementation to be a generic class. To do so, it is not possible to use the class-string return type, because by nature a class-string cannot be generic.

To do so, a workaround is implemented: instead of returning a class-string, the callback can return a string value containing the signature of the generic class.

namespace My\App;

interface ApiResponse {}

/**
 * @template T
 */
final readonly class SuccessResponse implements ApiResponse
{
    /** @var T */
    public mixed $data;
}

final readonly class User
{
    public string $name;
    public string $email;
}

final readonly class Product
{
    public string $name;
    public float $price;
}

$mapper = (new \CuyZ\Valinor\MapperBuilder())
    ->infer(
        ApiResponse::class,
        /**
         * @return '\My\App\SuccessResponse<\My\App\User>'
         *        |'\My\App\SuccessResponse<\My\App\Product>'
         */
        static fn (string $type): string => match($type) {
            'user' => '\My\App\SuccessResponse<\My\App\User>',
            'product' => '\My\App\SuccessResponse<\My\App\Product>',
            default => throw new \DomainException("Unhandled `$type`."),
        }
    )
    ->mapper();

$userResponse = $mapper->map(ApiResponse::class, [
    'type' => 'user', // Will return a `SuccessResponse<User>`
    'name' => 'John Doe',
    'email' => '[email protected]',
]);

assert($userResponse instanceof SuccessResponse);
assert($userResponse->data instanceof User);

$productResponse = $mapper->map(ApiResponse::class, [
    'type' => 'product', // Will return a `SuccessResponse<Product>`
    'name' => 'Laptop',
    'price' => 1337.42,
]);

assert($productResponse instanceof SuccessResponse);
assert($productResponse->data instanceof Product);

Fixes #270

It can sometimes be useful for an inferred interface implementation to
be a generic class. To do so, it is not possible to use the
`class-string` return type, because by nature a class-string cannot be
generic.

To do so, a workaround is implemented: instead of returning a
`class-string`, the callback can return a string value containing the
signature of the generic class.

```php
namespace My\App;

interface ApiResponse {}

/**
 * @template T
 */
final readonly class SuccessResponse implements ApiResponse
{
    /** @var T */
    public mixed $data;
}

final readonly class User
{
    public string $name;
    public string $email;
}

final readonly class Product
{
    public string $name;
    public float $price;
}

$mapper = (new \CuyZ\Valinor\MapperBuilder())
    ->infer(
        ApiResponse::class,
        /**
         * @return '\My\App\SuccessResponse<\My\App\User>'
         *        |'\My\App\SuccessResponse<\My\App\Product>'
         */
        static fn (string $type): string => match($type) {
            'user' => '\My\App\SuccessResponse<\My\App\User>',
            'product' => '\My\App\SuccessResponse<\My\App\Product>',
            default => throw new \DomainException("Unhandled `$type`."),
        }
    )
    ->mapper();

$userResponse = $mapper->map(ApiResponse::class, [
    'type' => 'user', // Will return a `SuccessResponse<User>`
    'name' => 'John Doe',
    'email' => '[email protected]',
]);

assert($userResponse instanceof SuccessResponse);
assert($userResponse->data instanceof User);

$productResponse = $mapper->map(ApiResponse::class, [
    'type' => 'product', // Will return a `SuccessResponse<Product>`
    'name' => 'Laptop',
    'price' => 1337.42,
]);

assert($productResponse instanceof SuccessResponse);
assert($productResponse->data instanceof Product);
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for generics in interface inference

1 participant