Lines
98.27%
57 / 58
Methods
80.00%
4 / 5
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| import | 100.00% | 16 / 16 | 100.00% | 1 / 1 | 7 | ||
| defaults | 88.88% | 8 / 9 | 0.00% | 0 / 1 | 4.02 | ||
| importRow | 100.00% | 24 / 24 | 100.00% | 1 / 1 | 8 | ||
| values | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 6 | ||
| 24 | final readonly class AgentStatCsvImporter | |
| 25 | { | |
| 26 | /** Stay text: the agent, the faction and the datetime. */ | |
| 27 | private const array TEXT_COLUMNS = ['datetime', 'nickname', 'faction']; | |
| 28 | ||
| 29 | public function __construct( | |
| 30 | private DenormalizerInterface $denormalizer, | |
| 31 | private ValidatorInterface $validator, | |
| 32 | private AgentStatService $agentStatService, | |
| 33 | ) { | |
| 34 | } | |
| 35 | ||
| 36 | /** | |
| 37 | * @param resource $csv | |
| 38 | */ | |
| 39 | public function import(User $owner, $csv): CsvImportResult | |
| 40 | { | |
| 41 | $result = new CsvImportResult(); | |
| 42 | ||
| 43 | $header = \fgetcsv($csv, escape: ''); | |
| 44 | if (false === $header || [] === \array_filter($header, static fn (?string $column): bool => null !== $column && '' !== \trim($column))) { | |
| 45 | $result->errors[] = '1: no header row'; | |
| 46 | ||
| 47 | return $result; | |
| 48 | } | |
| 49 | ||
| 50 | $columns = \array_map(static fn (?string $column): string => \trim((string) $column), $header); | |
| 51 | $rows = []; | |
| 52 | $line = 1; | |
| 53 | while (false !== ($row = \fgetcsv($csv, escape: ''))) { | |
| 54 | ++$line; | |
| 55 | // Empty lines | |
| 56 | if ([null] !== $row) { | |
| 57 | $rows[$line] = $row; | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | $defaults = $this->defaults($owner, $columns, $rows); | |
| 62 | foreach ($rows as $number => $row) { | |
| 63 | $this->importRow($owner, $columns, $row, $number, $defaults, $result); | |
| 64 | } | |
| 65 | ||
| 66 | return $result; | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * For rows without them: 4e-website's rows before May 2020 have no nickname and faction. The | |
| 71 | * owner's agent name (else the file's first nickname) and the file's first faction. | |
| 72 | * | |
| 73 | * @param list<string> $columns | |
| 74 | * @param array<int, list<string|null>> $rows | |
| 75 | * | |
| 76 | * @return array{nickname: string, faction: string} | |
| 77 | */ | |
| 78 | private function defaults(User $owner, array $columns, array $rows): array | |
| 79 | { | |
| 80 | $first = static function (string $column) use ($columns, $rows): string { | |
| 81 | $index = \array_search($column, $columns, true); | |
| 82 | foreach ($rows as $row) { | |
| 83 | $value = false === $index ? '' : \trim((string) ($row[$index] ?? '')); | |
| 84 | if ('' !== $value) { | |
| 85 | return $value; | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | return ''; | |
| 90 | }; | |
| 91 | ||
| 92 | return ['nickname' => $owner->getAgentName() ?? $first('nickname'), 'faction' => $first('faction')]; | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * @param list<string> $columns | |
| 97 | * @param list<string|null> $row | |
| 98 | * @param array{nickname: string, faction: string} $defaults | |
| 99 | */ | |
| 100 | private function importRow(User $owner, array $columns, array $row, int $line, array $defaults, CsvImportResult $result): void | |
| 101 | { | |
| 102 | if (\count($row) !== \count($columns)) { | |
| 103 | $result->errors[] = \sprintf('%d: %d values for %d columns', $line, \count($row), \count($columns)); | |
| 104 | ||
| 105 | return; | |
| 106 | } | |
| 107 | ||
| 108 | try { | |
| 109 | $values = $this->values(\array_combine($columns, $row)); | |
| 110 | foreach ($defaults as $column => $default) { | |
| 111 | if ('' === ($values[$column] ?? '')) { | |
| 112 | $values[$column] = $default; | |
| 113 | } | |
| 114 | } | |
| 115 | ||
| 116 | $input = $this->denormalizer->denormalize($values, AgentStatInput::class); | |
| 117 | } catch (NotNormalizableValueException $notNormalizableValueException) { | |
| 118 | // E.g. text for a stat | |
| 119 | $result->errors[] = \sprintf('%d: %s: %s', $line, $notNormalizableValueException->getPath() ?? '', $notNormalizableValueException->getMessage()); | |
| 120 | ||
| 121 | return; | |
| 122 | } | |
| 123 | ||
| 124 | $violations = $this->validator->validate($input); | |
| 125 | if (\count($violations) > 0) { | |
| 126 | $result->errors[] = $line.': '.\implode(' ', \array_map( | |
| 127 | static fn (ConstraintViolationInterface $violation): string => \trim($violation->getPropertyPath().': '.$violation->getMessage(), ': '), | |
| 128 | \iterator_to_array($violations), | |
| 129 | )); | |
| 130 | ||
| 131 | return; | |
| 132 | } | |
| 133 | ||
| 134 | try { | |
| 135 | $this->agentStatService->create($owner, $input); | |
| 136 | ++$result->imported; | |
| 137 | } catch (StatsAlreadyAddedException) { | |
| 138 | ++$result->skipped; | |
| 139 | } catch (AgentNameException $agentNameException) { | |
| 140 | $result->errors[] = $line.': '.$agentNameException->getMessage(); | |
| 141 | } | |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * The values as AgentStatInput takes them: numbers for the stats, null for empty ones. | |
| 146 | * | |
| 147 | * @param array<string, string|null> $row | |
| 148 | * | |
| 149 | * @return array<string, int|string|null> | |
| 150 | */ | |
| 151 | private function values(array $row): array | |
| 152 | { | |
| 153 | $values = []; | |
| 154 | foreach ($row as $column => $value) { | |
| 155 | $value = \trim((string) $value); | |
| 156 | $values[$column] = match (true) { | |
| 157 | \in_array($column, self::TEXT_COLUMNS, true) => $value, | |
| 158 | '' === $value => null, | |
| 159 | // Not a number: left as it is, the denormalizer reports it | |
| 160 | 1 === \preg_match('/^\d+$/', $value) => (int) $value, | |
| 161 | default => $value, | |
| 162 | }; | |
| 163 | } | |
| 164 | ||
| 165 | return $values; | |
| 166 | } | |
| 167 | } |