$features * @param string $kind One of the constants above. */ public function __construct( public readonly string $label, public readonly array $features, public readonly string $kind ) { } /** Locks its features whatever the release (the third-party code no longer fits or a mirror differs). */ public function isHard(): bool { return in_array($this->kind, self::HARD, true); } public function concerns(Capability $feature): bool { return $this->features === [] || in_array($feature, $this->features, true); } /** @return array{label: string, kind: string, features: list} For the transient. */ public function toArray(): array { return [ 'label' => $this->label, 'kind' => $this->kind, 'features' => array_map(static fn (Capability $feature): string => $feature->id(), $this->features), ]; } /** * The problem of toArray(), or null for anything else (an unknown kind or feature included). * * @param Closure(string): ?Capability $capability Finds a capability by its ID, null for an unknown one. */ public static function fromArray(mixed $row, Closure $capability): ?self { if ( ! is_array($row) || ! is_string($row['label'] ?? null) || ! in_array($row['kind'] ?? null, self::KINDS, true) || ! is_array($row['features'] ?? null) ) { return null; } $features = []; foreach ($row['features'] as $value) { $feature = is_string($value) ? $capability($value) : null; if ($feature === null) { return null; } $features[] = $feature; } return new self($row['label'], $features, $row['kind']); } }