Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional return via static lookup array #9704

Closed
ruudk opened this issue Jul 31, 2023 · 14 comments
Closed

Conditional return via static lookup array #9704

ruudk opened this issue Jul 31, 2023 · 14 comments

Comments

@ruudk
Copy link
Contributor

ruudk commented Jul 31, 2023

Bug report

I'm trying to let PHPStan understand the following code using a static TYPES array to lookup a given type.

class HelloWorld
{
    private const TYPES = [
        'foo' => DateTime::class,
        'bar' => DateTimeImmutable::class,
    ];

    /**
     * @template T of key-of<self::TYPES>
     * @param T $type
     * @return self::TYPES[T]
     */
    public static function get(string $type) : ?object
    {
        $class = self::TYPES[$type] ?? null;
        if ($class === null) {
            return null;
        }

        return new $class('now');
    }
}

But whatever I try, it seems I cannot tell PHPStan that the return type will be an instance of the value from the self::TYPES constant array.

I also tried:

@return (self::TYPES[T] is defined ? self::TYPES[T] : null)

Code snippet that reproduces the problem

https://phpstan.org/r/e086655c-9d6d-4ed8-9f80-c3e3f33bdc9b

Expected output

I expect it to correctly understand the type.

Did PHPStan help you today? Did it make you happy in any way?

It does, ❤️ PHPStan

@ondrejmirtes
Copy link
Member

This gets you close: https://phpstan.org/r/ba879058-ecef-459c-b7ff-69e03be2ac1f

The problem is that in the array you have class-strings but you want objects. I'm not sure if it can currently be solved, most likely we'd need some kind of type that lets us go from class-string<T> to T.

@ruudk
Copy link
Contributor Author

ruudk commented Aug 1, 2023

Thanks, that already looks better.

Couldn't we use object<T> for this? Like: @return (K is key-of<M> ? object<M[K]> : null)

@ondrejmirtes ondrejmirtes added this to the Generics milestone Aug 7, 2023
@phpstan-bot
Copy link
Contributor

@ruudk After the latest push in 1.11.x, PHPStan now reports different result with your code snippet:

@@ @@
 15: PHPDoc tag @return with type array<string, string> is incompatible with native type object|null.
 17: Dumped type: T of 'bar'|'foo' (method HelloWorld::get(), argument)
+18: Offset T of 'bar'|'foo' on array{foo: 'DateTime', bar: 'DateTimeImmutable'} on left side of ?? always exists and is not nullable.
+19: Strict comparison using === between 'DateTime'|'DateTimeImmutable' and null will always evaluate to false.
 27: Dumped type: object|null
 30: Dumped type: object|null
Full report
Line Error
15 `PHPDoc tag @return with type array<string, string> is incompatible with native type object
17 `Dumped type: T of 'bar'
18 `Offset T of 'bar'
19 `Strict comparison using === between 'DateTime'
27 `Dumped type: object
30 `Dumped type: object

@ruudk
Copy link
Contributor Author

ruudk commented Apr 23, 2024

It's getting better but still not working properly: https://phpstan.org/r/e9ae422d-ab7f-400e-99df-f213c1022b09

@rvanvelzen
Copy link
Contributor

This patch would create an object<...> type as mentioned in a comment: https://gist.github.com/rvanvelzen/5bd097289e857cc8590adf278e77def5

@ruudk
Copy link
Contributor Author

ruudk commented May 3, 2024

Thanks!

@ondrejmirtes is this something you would accept as a PR?

@rvanvelzen would you want to submit this as a PR? If you don't want to I can do it with your code. But you did the work obviously 😄

@ondrejmirtes
Copy link
Member

Maybe I'm having some kind of friday afternoon brain fart but what's the difference between T and object<T>? Can you post a full example about how it should behave?

@ruudk
Copy link
Contributor Author

ruudk commented May 3, 2024

The issue is that T is class-string, not an object of T. Introducing object would allow to type that.

#9704 (comment)

@ondrejmirtes
Copy link
Member

What about new<T> instead? It'd make it more obvious. object<T> is not that intuitive I think.

@ruudk
Copy link
Contributor Author

ruudk commented May 3, 2024

That's even better!

@ondrejmirtes
Copy link
Member

Good news is: we can do it because a class cannot be named new: https://3v4l.org/YhCv3

@phpstan-bot
Copy link
Contributor

@ruudk After the latest push in 1.11.x, PHPStan now reports different result with your code snippet:

@@ @@
-15: PHPDoc tag @return with type array<string, string> is incompatible with native type object|null.
+15: PHPDoc tag @return with type array{foo: 'DateTime', bar: 'DateTimeImmutable'}[T] is incompatible with native type object|null.
 17: Dumped type: T of 'bar'|'foo' (method HelloWorld::get(), argument)
+18: Offset T of 'bar'|'foo' on array{foo: 'DateTime', bar: 'DateTimeImmutable'} on left side of ?? always exists and is not nullable.
+19: Strict comparison using === between 'DateTime'|'DateTimeImmutable' and null will always evaluate to false.
 27: Dumped type: object|null
 30: Dumped type: object|null
Full report
Line Error
15 `PHPDoc tag @return with type array{foo: 'DateTime', bar: 'DateTimeImmutable'}[T] is incompatible with native type object
17 `Dumped type: T of 'bar'
18 `Offset T of 'bar'
19 `Strict comparison using === between 'DateTime'
27 `Dumped type: object
30 `Dumped type: object

@phpstan-bot
Copy link
Contributor

@ruudk After the latest push in 1.11.x, PHPStan now reports different result with your code snippet:

@@ @@
-18: PHPDoc tag @return with type array<string, string> is incompatible with native type object|null.
+18: PHPDoc tag @return with type array{foo: 'DateTime', bar: 'DateTimeImmutable'}[T] is incompatible with native type object|null.
 20: Dumped type: T of 'bar'|'foo' (method HelloWorld::get(), argument)
 22: Dumped type: 'DateTime'|'DateTimeImmutable'
 28: Dumped type: object|null
 31: Dumped type: object|null
Full report
Line Error
18 `PHPDoc tag @return with type array{foo: 'DateTime', bar: 'DateTimeImmutable'}[T] is incompatible with native type object
20 `Dumped type: T of 'bar'
22 `Dumped type: 'DateTime'
28 `Dumped type: object
31 `Dumped type: object

ruudk added a commit to ruudk/phpstan-src that referenced this issue May 6, 2024
This makes it possible that a new instance of a class-string will be returned.

For example:
```php
/**
 * @template T of class-string
 * @param T $type
 *
 * @return new<T>
 */
public static function get(string $type) : ?object
{
	return new $type;
}
```

Or a more complex example:
```php
/**
 * @var array<string, class-string>
 */
private const TYPES = [
	'foo' => DateTime::class,
	'bar' => DateTimeImmutable::class,
];

/**
 * @template T of key-of<self::TYPES>
 * @param T $type
 *
 * @return new<self::TYPES[T]>
 */
public static function get(string $type) : ?object
{
	$class = self::TYPES[$type];
	return new $class('now');
}
```

See phpstan/phpstan#9704

The work was done by @rvanvelzen in a gist. I just created the PR for it.

Co-Authored-By: Richard van Velzen <rvanvelzen1@gmail.com>
ruudk added a commit to ruudk/phpstan-src that referenced this issue May 6, 2024
This makes it possible that a new instance of a class-string will be returned.

```php
/**
 * @var array<string, class-string>
 */
private const TYPES = [
	'foo' => DateTime::class,
	'bar' => DateTimeImmutable::class,
];

/**
 * @template T of key-of<self::TYPES>
 * @param T $type
 *
 * @return new<self::TYPES[T]>
 */
public static function get(string $type) : ?object
{
	$class = self::TYPES[$type];
	return new $class('now');
}
```

See phpstan/phpstan#9704

The work was done by @rvanvelzen in a gist. I just created the PR for it.

Co-Authored-By: Richard van Velzen <rvanvelzen1@gmail.com>
ondrejmirtes pushed a commit to phpstan/phpstan-src that referenced this issue May 6, 2024
This makes it possible that a new instance of a class-string will be returned.

```php
/**
 * @var array<string, class-string>
 */
private const TYPES = [
	'foo' => DateTime::class,
	'bar' => DateTimeImmutable::class,
];

/**
 * @template T of key-of<self::TYPES>
 * @param T $type
 *
 * @return new<self::TYPES[T]>
 */
public static function get(string $type) : ?object
{
	$class = self::TYPES[$type];
	return new $class('now');
}
```

See phpstan/phpstan#9704

The work was done by @rvanvelzen in a gist. I just created the PR for it.

Co-Authored-By: Richard van Velzen <rvanvelzen1@gmail.com>
@ondrejmirtes
Copy link
Member

Fixed phpstan/phpstan-src#3050

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants