Aller au contenu

Métadonnées

phil/enums

AsMetadatableEnumTrait associe des valeurs aux cas au moyen d'attributs — un libellé, une couleur, un poids — et les relit par des accesseurs nommés d'après eux.

Déclarer des métadonnées sur une enum

Une propriété méta est une classe d'attribut qui étend Meta\AbstractMetaProperty. L'enum énumère celles qu'elle active dans #[Meta(...)], et chaque cas porte celles pour lesquelles il a des valeurs :

use Phil\Enums\Attribute\Label;
use Phil\Enums\Attribute\Meta;
use Phil\Enums\Traits\AsMetadatableEnumTrait;

#[Meta(Label::class, Badge::class)]
enum Ticket: string
{
    use AsMetadatableEnumTrait;

    #[Label('Open')] #[Badge('amber')]
    case OPEN = 'open';

    #[Label('Resolved')] #[Badge('teal')]
    case RESOLVED = 'resolved';

    #[Label('Spam')]
    case SPAM = 'spam';
}

Label est fourni avec la bibliothèque ; Badge est propre à l'application :

use Attribute;
use Phil\Enums\Meta\AbstractMetaProperty;

/** @extends AbstractMetaProperty<string> */
#[Attribute]
final class Badge extends AbstractMetaProperty
{
}

Les lire

Chaque propriété méta activée donne aux cas un accesseur : le nom court de la classe, avec une première lettre minuscule.

Ticket::OPEN->label();   // 'Open'
Ticket::RESOLVED->badge(); // 'teal'
Ticket::SPAM->badge();   // null: the case carries no Badge, and Badge's default value is null

Une méthode qu'aucune propriété méta activée ne nomme échoue avec BadMethodCallException, formulée comme l'erreur propre à PHP — Call to undefined method App\Ticket::lable() — : un accesseur mal orthographié, ou dont la propriété méta a été retirée de #[Meta], ne passe donc jamais pour une valeur manquante. L'extension PHPStan le signale comme une méthode inconnue avant l'exécution (PHPStan).

Écrire une propriété méta

Une sous-classe d'AbstractMetaProperty peut remplacer trois choses.

RemplacementEffetExemple
transform(mixed $value): mixedRéécrit la valeur à la construction de l'attribut.'amber' stocké sous la forme 'badge-amber'
defaultValue(): mixed (statique)Ce que répond un cas sans l'attribut ; null sauf remplacement. Restreindre son type de retour (string) restreint celui de l'accesseur pour PHPStan.'grey'
customMethodName(): ?string (statique)Le nom de l'accesseur, lorsque le nom de la classe ne doit pas le donner.'colour' donne ->colour()
#[Attribute]
final class Badge extends AbstractMetaProperty
{
    public static function defaultValue(): string
    {
        return 'badge-grey';
    }

    protected function transform(mixed $value): mixed
    {
        return 'badge-'.$value;
    }

    protected static function customMethodName(): ?string
    {
        return 'colour';
    }
}

Ticket::OPEN->colour(); // 'badge-amber'
Ticket::SPAM->colour(); // 'badge-grey'

method() donne le nom de l'accesseur, tel que le calculent le trait et l'extension PHPStan.

Trouver un cas par ses métadonnées

fromMeta() et tryFromMeta() prennent une instance de la propriété méta — construite avec la méthode statique make(), qui transforme la valeur comme le ferait l'attribut — et donnent le premier cas qui porte cette valeur :

Ticket::fromMeta(Label::make('Resolved'));  // Ticket::RESOLVED
Ticket::tryFromMeta(Label::make('Closed')); // null
Ticket::fromMeta(Label::make('Closed'));    // ValueError

Propriétés méta déclarées par un trait

Un trait qu'utilise l'enum peut porter son propre #[Meta(...)]. Ses propriétés méta sont lues après celles de l'enum, à l'exécution comme par l'extension PHPStan : un trait peut donc apporter un attribut et son accesseur à chaque enum qui l'utilise.

#[Meta(Shade::class)]
trait HasShade
{
}

#[Meta(Label::class)]
enum Theme
{
    use AsMetadatableEnumTrait;
    use HasShade;

    #[Label('Dark')] #[Shade('black')]
    case DARK;
}

Theme::DARK->label(); // 'Dark'
Theme::DARK->tint();  // 'black', when Shade names its accessor `tint` through customMethodName()

Lues une seule fois

Les propriétés méta d'une enum, et les valeurs que porte chaque cas, sont lues au premier accès et conservées pour le reste du processus : les attributs ne peuvent pas changer pendant l'exécution du code. Un accesseur, fromMeta(), tryFromMeta() ou whereMeta() appelé de nouveau ne fait aucune réflexion et n'instancie aucun attribut. Seuls les attributs de propriétés méta sont instanciés, jamais un autre attribut que porte un cas. Pour une propriété méta répétée comme Group, l'accesseur donne la première valeur ; Reflection::metaValueList() les donne toutes, dans l'ordre de déclaration, et une liste vide pour un cas qui ne l'a pas.

Messages d'erreur

fromMeta() nomme l'enum, la propriété méta et la valeur cherchée : une valeur scalaire ou Stringable est écrite sous forme de chaîne, toute autre valeur sous forme de son type — Enum App\Status does not have a case with a meta property "App\Weight" of value "array".

Propriétés méta incluses

Le paquet est fourni avec cinq propriétés méta prêtes à l'emploi :

Propriété métaValeurRépétableUtilisée par
DescriptionstringNonVos propres accesseurs ; une intégration peut la traduire.
GroupstringOuiinGroup() et groupByMeta() (Opérations).
LabelstringNonVos propres accesseurs ; une intégration peut la traduire.
AliasstringOuiparse() (Ordre, analyse et valeurs par défaut).
Weightint, 0 ou plus, 1 par défautNonrandom() (Opérations).

Trois attributs ne sont pas des propriétés méta — ils ne portent aucune valeur par cas et n'ont pas d'accesseur : #[DefaultCase] et #[TransitionsTo] sur les cas (Ordre, analyse, valeurs par défaut et transitions), et #[BitFlags] sur une enum (Ensembles et tables associatives).

Métadonnées requises

#[RequiredMeta] nomme les propriétés méta que chaque cas de l'enum doit porter, comme un Label pour chaque cas qu'affiche un formulaire. Il est répétable.

use Phil\Enums\Attribute\RequiredMeta;

#[Meta(Label::class, Description::class)]
#[RequiredMeta(Label::class)]
enum Status: string
{
    use AsMetadatableEnumTrait;

    #[Label('Draft')]
    case DRAFT = 'draft';

    case PUBLISHED = 'published'; // reported: no #[Label]
}

Deux vérifications le lisent. L'extension PHPStan signale chaque cas auquel il en manque une, avec l'identifiant philEnums.requiredMeta (PHPStan). Reflection::missingRequiredMeta() les énumère à l'exécution, par nom de cas, pour un test dans un projet sans PHPStan (Tests) :

Reflection::missingRequiredMeta(Status::class); // ['PUBLISHED' => [Label::class]]

Une propriété requise que porte chaque cas type aussi son accesseur sans null, puisqu'aucun cas ne se rabat sur la valeur par défaut.

Typer les accesseurs sans l'extension PHPStan

Les accesseurs sont servis par __call(), qu'un éditeur ne peut pas voir. Avec PHPStan, l'extension les type. Pour aider aussi un éditeur, déclarez-les avec @method sur l'enum, ou sur un trait que plusieurs enums utilisent avec la même propriété méta :

/**
 * @method string      label()
 * @method string|null badge()
 */
#[Meta(Label::class, Badge::class)]
enum Ticket: string
{
    use AsMetadatableEnumTrait;
    // …
}