Lines 100.00% 21 / 21
Methods 100.00% 6 / 6
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 fromArray 100.00% 4 / 4 100.00% 1 / 1 1
 toArray 100.00% 4 / 4 100.00% 1 / 1 1
 validate 100.00% 5 / 5 100.00% 1 / 1 4
 error 100.00% 6 / 6 100.00% 1 / 1 9
 isKey 100.00% 1 / 1 100.00% 1 / 1 1
17final class CustomMedals
18{
19    /** Medal and tier keys of 4e-website, e.g. plus_gamma_onsite, 2026_summer_solstice, platinum. */
20    private const string KEY_PATTERN = '/^[a-z0-9_]{1,50}$/';
21
22    private const array GROUPS = ['anomaly', 'annual', 'event'];
23
24    /**
25     * @param array<string, string> $anomaly medal => tier ("" for a medal without tiers)
26     * @param array<string, int>    $annual  medal => level 1 (bronze) to 5 (onyx)
27     * @param array<string, string> $event   medal => tier ("" for a medal without tiers)
28     */
29    public function __construct(
30        #[Groups([Me::READ, Me::WRITE, Agent::ITEM])]
31        public array $anomaly = [],
32        #[Groups([Me::READ, Me::WRITE, Agent::ITEM])]
33        public array $annual = [],
34        #[Groups([Me::READ, Me::WRITE, Agent::ITEM])]
35        public array $event = [],
36    ) {
37    }
38
39    /**
40     * @param array<string, array<string, int|string>> $medals as stored on the User
41     */
42    public static function fromArray(array $medals): self
43    {
44        /** @var array<string, string> $anomaly */
45        $anomaly = $medals['anomaly'] ?? [];
46        /** @var array<string, int> $annual */
47        $annual = $medals['annual'] ?? [];
48        /** @var array<string, string> $event */
49        $event = $medals['event'] ?? [];
50
51        return new self($anomaly, $annual, $event);
52    }
53
54    /**
55     * @return array<string, array<string, int|string>> for the User, empty groups left out
56     */
57    public function toArray(): array
58    {
59        return \array_filter(
60            ['anomaly' => $this->anomaly, 'annual' => $this->annual, 'event' => $this->event],
61            static fn (array $medals): bool => [] !== $medals,
62        );
63    }
64
65    #[Assert\Callback]
66    public function validate(ExecutionContextInterface $context): void
67    {
68        foreach (self::GROUPS as $group) {
69            foreach ($this->{$group} as $medal => $value) {
70                $error = $this->error($group, (string) $medal, $value);
71                if (null !== $error) {
72                    $context->buildViolation($error[1])->atPath($error[0])->addViolation();
73                }
74            }
75        }
76    }
77
78    /**
79     * @return array{string, string}|null the property path and the message, null if valid
80     */
81    private function error(string $group, string $medal, mixed $value): ?array
82    {
83        if (!$this->isKey($medal)) {
84            return [$group, 'Not a medal key.'];
85        }
86
87        $path = $group.'['.$medal.']';
88
89        if ('annual' === $group) {
90            return \is_int($value) && $value >= 1 && $value <= 5 ? null : [$path, 'The level must be 1 to 5.'];
91        }
92
93        return \is_string($value) && ('' === $value || $this->isKey($value)) ? null : [$path, 'Not a tier.'];
94    }
95
96    private function isKey(string $key): bool
97    {
98        return 1 === \preg_match(self::KEY_PATTERN, $key);
99    }
100}