Passed
Push — master ( ab6f49...c762ff )
by
unknown
16:59 queued 08:07
created

SearchEngineFieldSynchronizer::syncFromJson()   C

Complexity

Conditions 12
Paths 33

Size

Total Lines 55
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 32
c 1
b 0
f 0
nc 33
nop 2
dl 0
loc 55
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Search;
8
9
use Chamilo\CoreBundle\Entity\SearchEngineField;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Symfony\Component\Validator\Exception\ValidatorException;
12
13
final class SearchEngineFieldSynchronizer
14
{
15
    public function __construct(
16
        private readonly EntityManagerInterface $entityManager
17
    ) {}
18
19
    /**
20
     * Applies JSON-defined search fields to the search_engine_field table.
21
     *
22
     * Non-destructive by default:
23
     * - Creates missing fields
24
     * - Updates titles
25
     * - Does NOT delete fields that disappeared from JSON
26
     *
27
     * @return array{created:int, updated:int, deleted:int}
28
     */
29
    public function syncFromJson(?string $json, bool $allowDeletes = false): array
30
    {
31
        $json = trim((string) $json);
32
33
        if ('' === $json) {
34
            return ['created' => 0, 'updated' => 0, 'deleted' => 0];
35
        }
36
37
        $desired = $this->parseJsonToCodeTitleMap($json); // code => title
38
39
        /** @var SearchEngineField[] $existing */
40
        $existing = $this->entityManager->getRepository(SearchEngineField::class)->findAll();
41
42
        $existingByCode = [];
43
        foreach ($existing as $field) {
44
            $existingByCode[$field->getCode()] = $field;
45
        }
46
47
        $created = 0;
48
        $updated = 0;
49
        $deleted = 0;
50
51
        foreach ($desired as $code => $title) {
52
            if (isset($existingByCode[$code])) {
53
                $field = $existingByCode[$code];
54
55
                if ($field->getTitle() !== $title) {
56
                    $field->setTitle($title);
57
                    $this->entityManager->persist($field);
58
                    $updated++;
59
                }
60
            } else {
61
                $field = (new SearchEngineField())
62
                    ->setCode($code)
63
                    ->setTitle($title);
64
65
                $this->entityManager->persist($field);
66
                $created++;
67
            }
68
        }
69
70
        if ($allowDeletes) {
71
            foreach ($existingByCode as $code => $field) {
72
                if (!isset($desired[$code])) {
73
                    $this->entityManager->remove($field);
74
                    $deleted++;
75
                }
76
            }
77
        }
78
79
        if ($created > 0 || $updated > 0 || $deleted > 0) {
80
            $this->entityManager->flush();
81
        }
82
83
        return ['created' => $created, 'updated' => $updated, 'deleted' => $deleted];
84
    }
85
86
    /**
87
     * Supported formats:
88
     *
89
     * 1) Canonical (recommended):
90
     *    {"fields":[{"code":"C","title":"Course"},{"code":"S","title":"Session"}], "options": {...}}
91
     *
92
     * Backward-compatible formats:
93
     * 2) {"course":{"prefix":"C","title":"Course"}}
94
     * 3) {"c":"Course"}
95
     * 4) [{"code":"c","title":"Course"}]
96
     *
97
     * @return array<string,string> code => title
98
     */
99
    private function parseJsonToCodeTitleMap(string $json): array
100
    {
101
        $decoded = json_decode($json, true);
102
103
        if (JSON_ERROR_NONE !== json_last_error()) {
104
            throw new ValidatorException('Invalid JSON for search engine fields.');
105
        }
106
107
        if (!is_array($decoded)) {
108
            throw new ValidatorException('Search engine fields JSON must be an object or an array.');
109
        }
110
111
        // Canonical wrapper: {"fields":[...], ...}
112
        if (isset($decoded['fields'])) {
113
            if (!is_array($decoded['fields'])) {
114
                throw new ValidatorException('"fields" must be an array.');
115
            }
116
117
            return $this->parseListOfFields($decoded['fields']);
118
        }
119
120
        // List format: [{"code":"c","title":"Course"}]
121
        if ($this->isList($decoded)) {
122
            return $this->parseListOfFields($decoded);
123
        }
124
125
        // Object formats:
126
        // - {"c":"Course"}
127
        // - {"course":{"prefix":"C","title":"Course"}}
128
        $map = [];
129
130
        foreach ($decoded as $key => $value) {
131
            if (is_string($value)) {
132
                $code = $this->normalizeCode((string) $key);
133
                $title = $this->normalizeTitle($value);
134
                $map[$code] = $title;
135
                continue;
136
            }
137
138
            if (is_array($value) && isset($value['prefix'], $value['title'])) {
139
                $code = $this->normalizeCode((string) $value['prefix']);
140
                $title = $this->normalizeTitle($value['title']);
141
                $map[$code] = $title;
142
                continue;
143
            }
144
145
            throw new ValidatorException('Invalid search fields JSON structure.');
146
        }
147
148
        return $map;
149
    }
150
151
    /**
152
     * @param array<int, mixed> $rows
153
     * @return array<string,string>
154
     */
155
    private function parseListOfFields(array $rows): array
156
    {
157
        $map = [];
158
159
        foreach ($rows as $row) {
160
            if (!is_array($row)) {
161
                throw new ValidatorException('Each field entry must be an object with "code" and "title".');
162
            }
163
164
            // Accept "code" (canonical) and "prefix" (backward compatibility)
165
            $rawCode = $row['code'] ?? $row['prefix'] ?? '';
166
            $rawTitle = $row['title'] ?? null;
167
168
            $code = $this->normalizeCode((string) $rawCode);
169
            $title = $this->normalizeTitle($rawTitle);
170
171
            $map[$code] = $title;
172
        }
173
174
        return $map;
175
    }
176
177
    private function normalizeCode(string $code): string
178
    {
179
        $code = trim($code);
180
181
        if ('' === $code) {
182
            throw new ValidatorException('Field code cannot be empty.');
183
        }
184
185
        // Keep ONLY the first character.
186
        $code = mb_substr($code, 0, 1);
187
188
        // Keep DB consistent with current data (c/s/f/g...)
189
        return strtolower($code);
190
    }
191
192
    private function normalizeTitle(mixed $title): string
193
    {
194
        $title = trim((string) $title);
195
196
        if ('' === $title) {
197
            throw new ValidatorException('Field title cannot be empty.');
198
        }
199
200
        return $title;
201
    }
202
203
    private function isList(array $arr): bool
204
    {
205
        $i = 0;
206
        foreach ($arr as $k => $_) {
207
            if ($k !== $i) {
208
                return false;
209
            }
210
            $i++;
211
        }
212
213
        return true;
214
    }
215
}
216