Lines 100.00% 46 / 46
Functions and Methods 100.00% 8 / 8
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
CustomMedalsJsonImporter 100.00% 46 / 46 100.00% 8 / 8 30 100.00% 1 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 import 100.00% 11 / 11 100.00% 1 / 1 3
 known 100.00% 4 / 4 100.00% 1 / 1 2
 importMedal 100.00% 5 / 5 100.00% 1 / 1 5
 decode 100.00% 8 / 8 100.00% 1 / 1 3
 entries 100.00% 3 / 3 100.00% 1 / 1 4
 rank 100.00% 6 / 6 100.00% 1 / 1 8
 merge 100.00% 8 / 8 100.00% 1 / 1 4
1<?php
2
3declare(strict_types=1);
4
5namespace App\Import;
6
7use App\Entity\User;
8use App\Medal\CustomMedal;
9use App\Medal\MedalChecker;
10use Doctrine\ORM\EntityManagerInterface;
11
12/**
13 * Imports custom medals from a JSON file as 4e-website exports them (Migrate → Export custom
14 * medals): {"anomaly": {"orion": "platinum"}, "annual": {"voyager": 5}, "event": {…}}. They are
15 * merged with the user's: a medal the user has keeps the higher tier, as the app merges them.
16 * Medals or tiers AgentTools does not know are reported.
17 */
18final readonly class CustomMedalsJsonImporter
19{
20    public function __construct(
21        private MedalChecker $medalChecker,
22        private EntityManagerInterface $entityManager,
23    ) {
24    }
25
26    public function import(User $owner, string $json): CustomMedalsImportResult
27    {
28        $result = new CustomMedalsImportResult();
29        $file = $this->decode($json, $result);
30        if (null === $file) {
31            return $result;
32        }
33
34        $known = $this->known();
35        $medals = $owner->getCustomMedals();
36        foreach ($this->entries($file) as [$group, $key, $value]) {
37            $this->importMedal($known[$group.'/'.$key] ?? null, $group, $key, $value, $medals, $result);
38        }
39
40        $owner->setCustomMedals($medals);
41        $this->entityManager->flush();
42
43        return $result;
44    }
45
46    /**
47     * @return array<string, CustomMedal> by "group/key"
48     */
49    private function known(): array
50    {
51        $known = [];
52        foreach ($this->medalChecker->customMedals() as $medal) {
53            $known[$medal->group.'/'.$medal->key] = $medal;
54        }
55
56        return $known;
57    }
58
59    /**
60     * Merges one medal of the file ([$medal] null for one AgentTools does not know).
61     *
62     * @param array<string, array<string, int|string>> $medals the user's, changed
63     */
64    private function importMedal(?CustomMedal $medal, string $group, string $key, mixed $value, array &$medals, CustomMedalsImportResult $result): void
65    {
66        $rank = $medal instanceof CustomMedal ? $this->rank($medal, $value) : null;
67        if (!$medal instanceof CustomMedal || null === $rank) {
68            $result->errors[] = \sprintf('%s: %s %s', $group, $key, \is_scalar($value) ? (string) $value : '');
69
70            return;
71        }
72
73        $this->merge($medals, $medal, $value, $rank, $result);
74    }
75
76    /**
77     * The file's groups, null (and why in [$result]) for no JSON object.
78     *
79     * @return array<mixed>|null
80     */
81    private function decode(string $json, CustomMedalsImportResult $result): ?array
82    {
83        try {
84            $file = \json_decode($json, true, 8, \JSON_THROW_ON_ERROR);
85        } catch (\JsonException $jsonException) {
86            $result->errors[] = 'Not JSON: '.$jsonException->getMessage();
87
88            return null;
89        }
90
91        if (!\is_array($file)) {
92            $result->errors[] = 'Not custom medals: an object of groups is expected.';
93
94            return null;
95        }
96
97        return $file;
98    }
99
100    /**
101     * Every medal of every group in the file.
102     *
103     * @param array<mixed> $file
104     *
105     * @return \Generator<array{string, string, mixed}> group, medal key, tier or level
106     */
107    private function entries(array $file): \Generator
108    {
109        foreach ($file as $group => $values) {
110            foreach (\is_array($values) ? $values : [] as $key => $value) {
111                yield [(string) $group, (string) $key, $value];
112            }
113        }
114    }
115
116    /**
117     * How high [$value] is for [$medal] (a tier's position, an annual medal's level), null if it
118     * is none of its tiers.
119     */
120    private function rank(CustomMedal $medal, mixed $value): ?int
121    {
122        if ('annual' === $medal->group) {
123            return \is_int($value) && $value >= 1 && $value <= 5 ? $value : null;
124        }
125
126        if ([] === $medal->tiers) {
127            return '' === $value ? 0 : null;
128        }
129
130        $index = \array_search($value, $medal->tiers, true);
131
132        return false === $index ? null : $index;
133    }
134
135    /**
136     * @param array<string, array<string, int|string>> $medals the user's, changed
137     */
138    private function merge(array &$medals, CustomMedal $medal, mixed $value, int $rank, CustomMedalsImportResult $result): void
139    {
140        $saved = $medals[$medal->group][$medal->key] ?? null;
141        if (null === $saved) {
142            ++$result->added;
143        } elseif ($rank > ($this->rank($medal, $saved) ?? -1)) {
144            ++$result->upgraded;
145        } else {
146            ++$result->kept;
147
148            return;
149        }
150
151        $medals[$medal->group][$medal->key] = \is_int($value) ? $value : (string) $value;
152    }
153}