在 PhpStan 中使用枚举作为 PHP 中的字符串

Using Enum as string in PHP with PhpStan

我正在 Symfony 6 应用程序中试验 PHP 枚举,我认为我找到了一个非常好的用例。一切正常,但 phpstan 一直抱怨类型 I return.

 ------ -----------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/Entity/User.php
 ------ -----------------------------------------------------------------------------------------------------------------------------------------------
  93     Return type (array<App\Security\Roles>) of method App\Entity\User::getRoles() should be compatible with return type (array<string>) of method
         Symfony\Component\Security\Core\User\UserInterface::getRoles()
 ------ -----------------------------------------------------------------------------------------------------------------------------------------------

我的枚举如下所示:

namespace App\Security;

enum Roles: string
{
    case Admin = 'ROLE_ADMIN';
    case User = 'ROLE_USER';
}

在我实现 Symfony\Component\Security\Core\User\UserInterfaceUser 实体中,我有这个:

/**
 * @see UserInterface
 * @return array<Roles>
 */
public function getRoles(): array
{
    $roles = $this->roles;
    // guarantee every user at least has ROLE_USER
    $roles[] = Roles::User->value;

    return array_unique($roles);
}

/**
 * @param array<Roles> $roles
 */
public function setRoles(array $roles): self
{
    $this->roles = $roles;

    return $this;
}

我知道 phpstan 期望数组包含接口中定义的字符串,

/**
 * @return string[]
 */
public function getRoles(): array;

但是有办法解决这个问题吗?我真的更愿意能够利用枚举。向枚举添加类型不是为了能够将其用作该类型吗?另外值得一提的是,我到处都使用严格的类型,除了 phpstan 对我的类型不太满意之外,一切正常。

您的 return 注释是错误的,因为 PhpStan 的报告是正确的。

你说你报告了一组角色:

/**
 *  @return array<Roles>
 */

如果是这种情况,roles 将直接包含枚举,而不是支持枚举的值:

$this->roles[] = Roles::Admin

通过保留错误的注释,您不仅让 PhpStan 感到不安,而且还向 getRoles() 的消费者提供了错误的信息。你的注释意味着我应该能够做到:

/** @var <array>Roles */
$roles = $user->getRoles();

foreach ($roles as $role) {
   if ($role === Roles::Admin) {
       // something
   }
}

但您实际上是在 returning string[],而不是 Roles[],所以我的代码无法运行,我的应用程序会崩溃。悲伤遍地。

没有看到额外的代码,看起来您并没有真正以不同于 class 常量的方式“使用枚举”。 roles 不是 Roles 的集合,您的 addRole() 方法需要 string,而不是 Roles,等等

当然很好。由于您需要遵守 UserInterface,因此带有一些常量的 class 可能更适合这里。

/**
 *  @return array<Roles::ROLE_*>
 */

此注释工作的一个简单示例是:

<?php declare(strict_types = 1);

abstract class Roles {
    const ROLE_FOO = 'foo';
    const ROLE_BAR = 'bar';
    const ROLE_BAZ = 'baz';
}

/**
 * @param array<Roles::*> $s
 */
function sayHello(array $s): void
    {
        foreach ($s as $si) {
            echo 'Hello, ' . $si, "\n";
        }
    }

$array = [
    Roles::ROLE_FOO,
    Roles::ROLE_BAR
];

sayHello($array);

$badArray = [
    Roles::ROLE_FOO,
    'some'
];

// this is an error!
sayHello($badArray);

(playground link)

如果你想为你的角色使用枚举,那么就实际使用它们。但是你不能直接在 getRoles() 中使用它们,因为它会破坏界面。

您可以使用一组配套方法(getRealRoles()addRealRole(Roles $role) 等)来处理 roles 属性,然后您将离开getRoles() 只是将 array<Roles> 转换为 array<string>.

不,你不能表达“getRoles returns 一个由元素组成的数组,这些元素是 Roles 枚举的可能支持值”,但它是很好,因为在你的(与不关心你的实现的框架代码相反)代码中你会使用与枚举一起工作的方法。