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

Templates on static methods yet again #7975

Open
someniatko opened this issue May 16, 2022 · 8 comments
Open

Templates on static methods yet again #7975

someniatko opened this issue May 16, 2022 · 8 comments

Comments

@someniatko
Copy link
Contributor

I try to design something like this: https://psalm.dev/r/4cf10dedf9

However, as stated by @orklah in #7507 (comment),

This is on purpose. Psalm made a design choice that class-level templates and static properties/method should not mix.

Is there a way to design the interface in such a way Psalm won't complain?

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/4cf10dedf9
<?php

/**
 * @template T
 */
interface PersistableEntityInterface
{
    /**
     * Returns a DTO containing state of the entity so that it can be saved to a persistent storage.
     * @return T
     */
    public function saveState(): mixed;

    /**
     * Constructs entity restoring the state from DTO taken from a storage. 
     * @param T $state
     */
    public static function restoreState(mixed $state): static;
}

/** @template-implements PersistableEntityInterface<UserState> */
final class User implements PersistableEntityInterface
{
    public function __construct(
        private readonly string $id,
        private readonly string $name,
    ) {}
    
    public function name(): string
    {
        return strtoupper($this->name);
    }
    
    public function saveState(): UserState
    {
        return new UserState($this->id, $this->name);
    }
    
    public static function restoreState(mixed $state): self
    {
        return new self($state->id, $state->name);
    }
}

class UserState
{
    public function __construct(
        public readonly string $id,
        public readonly string $name,
    ) {}
}

class UserRepository
{
    /** @var array<string, UserState> */
    private array $users = [];
    
    public function getById(string $id): User
    {
        return User::restoreState($this->users[$id] ?? throw new \RuntimeException);
    }
    
    public function save(User $user): void
    {
        $userState = $user->saveState();
        $this->users[$userState->id] = $userState;
    }
}
Psalm output (using commit f960d71):

ERROR: UndefinedDocblockClass - 16:15 - Docblock-defined class, interface or enum named T does not exist

INFO: MixedInferredReturnType - 39:56 - Could not verify return type 'User&PersistableEntityInterface' for User::restoreState

ERROR: InvalidArgument - 60:35 - Argument 1 of User::restoreState expects T, UserState provided

@ZebulanStanphill
Copy link
Contributor

I was trying to setup Psalm on a codebase that's already using PHPStan (I figured it would be good to check things with both tools), and this is one of the main things blocking me from doing so. I have several abstract classes that define template types used in the params of their static methods. For (an abridged) example:

/**
 * @template T of Table
 * @template R of array<non-empty-string, mixed>
 */
abstract class Model {
	/**
	 * @phpstan-param R $row
	 *
	 * @phpstan-return self<T, R>
	 */
	abstract public static function fromRow(array $row): self;
}

The param type of the fromRow method causes Psalm to complain Docblock-defined class, interface or enum named My\Namespace\R does not exist.

@PatchRanger
Copy link

I confirm the issue and join the request of how to do templating for static properties.
The minimal case of reproduction is attached below.
https://psalm.dev/r/9a4c417e38

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/9a4c417e38
<?php
/**
 * @template T
 */
class Foo {
  /**
   * @var array<class-string<T>, mixed>
   */
   private static array $classMap = [];
}
Psalm output (using commit a82e7fc):

ERROR: UndefinedDocblockClass - 9:19 - Docblock-defined class, interface or enum named T does not exist

@someniatko
Copy link
Contributor Author

someniatko commented Jun 2, 2023

@orklah Do you think introducing something like @static-template or @template-static may solve the issue? (and @template-static-implements, @template-static-extends)

@orklah
Copy link
Collaborator

orklah commented Jun 4, 2023

Possibly. Frankly, this is kinda over my head. It's a design choice that was made before I started actively contributing and I would have no idea how to change that meaningfully

@discordier
Copy link
Contributor

This also affects static properties as seen in https://psalm.dev/r/047cddc9ac.

Having a static template annotation won't help when we have mixed usage I suppose (storing values from static context but using them in instanced context).

Copy link

I found these snippets:

https://psalm.dev/r/047cddc9ac
<?php
declare(strict_types=1);

/** @template T */
trait FooTrait
{
    // All broken for static usage.

    /** 
     * This bails but should not.
     * @var array<string, T>
     */
    private static array $values = [];

    /** @param T $arg */
    private static function failsButShouldNot($arg): void {}

    // All fine for non static usage.
    
    /** 
     * This works as expected
     * @var array<string, T>
     */
    private array $values2 = [];

    /** @param T $arg */
    private function worksAsExpected($arg): void { if ($arg === null); }
}

final readonly class Bar
{
    /** @use FooTrait<string> */
    use FooTrait;
}
Psalm output (using commit 03ee02c):

ERROR: UndefinedDocblockClass - 13:20 - Docblock-defined class, interface or enum named T does not exist

ERROR: UndefinedDocblockClass - 15:16 - Docblock-defined class, interface or enum named T does not exist

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

No branches or pull requests

5 participants