Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.88% covered (warning)
72.88%
86 / 118
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
CustomFieldCompilerService
72.88% covered (warning)
72.88%
86 / 118
22.22% covered (danger)
22.22%
2 / 9
109.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compile
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
8
 getCompiledFields
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldsForEntity
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 clearCache
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchPaths
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 processFile
73.17% covered (warning)
73.17%
30 / 41
0.00% covered (danger)
0.00%
0 / 1
20.94
 extractFieldData
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
8.19
 getActivePluginCustomFieldDirs
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2// src/Service/CustomFieldCompilerService.php
3
4declare(strict_types=1);
5
6namespace Satag\AmicronEntityBundle\Service;
7
8use Satag\AmicronEntityBundle\CustomField\MyCustomField;
9use Symfony\Component\Finder\Finder;
10
11class CustomFieldCompilerService
12{
13    /**
14     * @var array<string, array<string, array<string, mixed>>>
15     */
16    private array $compiledFields = [];
17
18    private bool $compiled = false;
19
20    public function __construct(
21        private readonly string $projectDir
22    ) {
23    }
24
25    /**
26     * Kompiliert alle CustomField-Klassen mit MyCustomField-Attributen
27     */
28    public function compile(): void
29    {
30        if ($this->compiled) {
31            return;
32        }
33
34        $this->compiledFields = [];
35
36        // Mögliche Verzeichnisse sammeln
37        $searchPaths = [];
38
39        // Plugin-Verzeichnisse (nur aktive Plugins aus plugins.json)
40        $pluginDirs = $this->getActivePluginCustomFieldDirs();
41        if (!empty($pluginDirs)) {
42            $searchPaths = array_merge($searchPaths, $pluginDirs);
43        }
44
45        // Haupt-CustomField-Verzeichnis
46        $mainCustomFieldDir = $this->projectDir . '/src/CustomField';
47        if (is_dir($mainCustomFieldDir)) {
48            $searchPaths[] = $mainCustomFieldDir;
49        }
50
51        // Plugin-spezifisches Verzeichnis (aktuelles Plugin)
52        $currentPluginDir = __DIR__ . '/../CustomField';
53        if (is_dir($currentPluginDir)) {
54            $searchPaths[] = $currentPluginDir;
55        }
56
57        // Nur suchen wenn Verzeichnisse existieren
58        if (!empty($searchPaths)) {
59            $finder = new Finder();
60            $finder->files()->name('*.php');
61
62            foreach ($searchPaths as $path) {
63                $finder->in($path);
64            }
65
66            foreach ($finder as $file) {
67                $this->processFile($file->getPathname());
68            }
69        }
70
71        $this->compiled = true;
72    }
73
74    /**
75     * @return array<string, array<string, array<string, mixed>>>
76     */
77    public function getCompiledFields(): array
78    {
79        $this->compile();
80
81        return $this->compiledFields;
82    }
83
84    /**
85     * @return array<string, array<string, mixed>>
86     */
87    public function getFieldsForEntity(string $entityName): array
88    {
89        $this->compile();
90        $fields = $this->compiledFields[$entityName] ?? [];
91        if (!empty($fields)) {
92            // Nach 'order' sortieren (aufsteigend)
93            uasort($fields, static function (array $a, array $b): int {
94                $orderA = isset($a['order']) ? (int) $a['order'] : 0;
95                $orderB = isset($b['order']) ? (int) $b['order'] : 0;
96
97                return $orderA <=> $orderB;
98            });
99        }
100
101        return $fields;
102    }
103
104    /**
105     * Cache zurücksetzen (nützlich für Development)
106     */
107    public function clearCache(): void
108    {
109        $this->compiled = false;
110        $this->compiledFields = [];
111    }
112
113    /**
114     * Debug-Methode zum Anzeigen gefundener Pfade
115     *
116     * @return array<int, string>
117     */
118    public function getSearchPaths(): array
119    {
120        $searchPaths = [];
121
122        // Plugin-Verzeichnisse (nur aktive Plugins aus plugins.json)
123        $pluginDirs = $this->getActivePluginCustomFieldDirs();
124        if (!empty($pluginDirs)) {
125            $searchPaths = array_merge($searchPaths, $pluginDirs);
126        }
127
128        // Haupt-CustomField-Verzeichnis
129        $mainCustomFieldDir = $this->projectDir . '/src/CustomField';
130        if (is_dir($mainCustomFieldDir)) {
131            $searchPaths[] = $mainCustomFieldDir;
132        }
133
134        // Plugin-spezifisches Verzeichnis (aktuelles Plugin)
135        $currentPluginDir = __DIR__ . '/../CustomField';
136        if (is_dir($currentPluginDir)) {
137            $searchPaths[] = $currentPluginDir;
138        }
139
140        return $searchPaths;
141    }
142
143    private function processFile(string $filePath): void
144    {
145        // Namespace und Klasse aus der Datei extrahieren
146        $content = file_get_contents($filePath);
147        if ($content === false) {
148            return;
149        }
150
151        if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
152            return;
153        }
154
155        if (!preg_match('/class\s+(\w+)/', $content, $classMatch)) {
156            return;
157        }
158
159        $namespace = $namespaceMatch[1];
160        $className = $classMatch[1];
161        $fullClassName = $namespace . '\\' . $className;
162
163        // Prüfen ob die Klasse existiert und ladbar ist
164        if (!class_exists($fullClassName)) {
165            return;
166        }
167
168        try {
169            $reflection = new \ReflectionClass($fullClassName);
170
171            // Nach MyCustomField-Attributen suchen
172            $attributes = $reflection->getAttributes(MyCustomField::class);
173
174            foreach ($attributes as $attribute) {
175                $attributeInstance = $attribute->newInstance();
176                $setTo = $attributeInstance->setTo;
177                $priority = $attributeInstance->priority ?? 0;
178
179                // CustomField-Instanz erstellen
180                if ($reflection->isInstantiable()) {
181                    $fieldInstance = $reflection->newInstance();
182
183                    // Field-Definition als Array konvertieren
184                    if (method_exists($fieldInstance, 'toArray')) {
185                        $fieldData = $fieldInstance->toArray();
186                    } else {
187                        // Fallback: Reflection verwenden
188                        $fieldData = $this->extractFieldData($fieldInstance);
189                    }
190
191                    // priority aus Attribut übernehmen
192                    $fieldData['priority'] = $priority;
193
194                    // Wenn Feld deaktiviert ist, komplett entfernen (überspringen)
195                    if (($fieldData['disabled'] ?? false) === true) {
196                        continue;
197                    }
198
199                    // Nach setTo kategorisieren
200                    if (!isset($this->compiledFields[$setTo])) {
201                        $this->compiledFields[$setTo] = [];
202                    }
203
204                    $identifier = $fieldData['identifier'] ?? null;
205                    if ($identifier === null || $identifier === '') {
206                        // Kein Identifier, überspringen
207                        continue;
208                    }
209
210                    // Bei bestehendem Identifier nach Priority entscheiden
211                    if (isset($this->compiledFields[$setTo][$identifier])) {
212                        $existing = $this->compiledFields[$setTo][$identifier];
213                        $existingPriority = $existing['priority'] ?? 0;
214                        if ($priority > $existingPriority) {
215                            $this->compiledFields[$setTo][$identifier] = $fieldData;
216                        } elseif ($priority === $existingPriority) {
217                            // Bei gleicher Priorität: letztes gewinnt (überschreibt)
218                            $this->compiledFields[$setTo][$identifier] = $fieldData;
219                        } // sonst niedrigere Priority -> ignorieren
220                    } else {
221                        $this->compiledFields[$setTo][$identifier] = $fieldData;
222                    }
223                }
224            }
225        } catch (\Exception $e) {
226            // Fehler beim Verarbeiten der Klasse - ignorieren und weitermachen
227            error_log("Error processing CustomField class {$fullClassName}" . $e->getMessage());
228        }
229    }
230
231    /**
232     * @return array<string, mixed>
233     */
234    private function extractFieldData(object $fieldInstance): array
235    {
236        $data = [];
237
238        // Standard-Properties über Reflection extrahieren
239        $reflection = new \ReflectionClass($fieldInstance);
240
241        // Getter-Methoden suchen
242        $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
243
244        foreach ($methods as $method) {
245            $methodName = $method->getName();
246
247            // Getter-Pattern erkennen
248            if (str_starts_with($methodName, 'get') && $method->getNumberOfParameters() === 0) {
249                $property = lcfirst(substr($methodName, 3));
250
251                try {
252                    $data[$property] = $method->invoke($fieldInstance);
253                } catch (\Exception) {
254                    // Fehler ignorieren
255                }
256            } elseif (str_starts_with($methodName, 'is') && $method->getNumberOfParameters() === 0) {
257                $property = lcfirst(substr($methodName, 2));
258
259                try {
260                    $data[$property] = $method->invoke($fieldInstance);
261                } catch (\Exception) {
262                    // Fehler ignorieren
263                }
264            }
265        }
266
267        return $data;
268    }
269
270    /**
271     * Liefert die CustomField-Verzeichnisse aller aktiven Plugins aus plugins.json
272     *
273     * @return array<int, string>
274     */
275    private function getActivePluginCustomFieldDirs(): array
276    {
277        $dirs = [];
278        $pluginsJson = $this->projectDir . '/plugins/plugins.json';
279        if (is_file($pluginsJson)) {
280            $json = file_get_contents($pluginsJson);
281            if ($json !== false) {
282                $data = json_decode($json, true);
283                if (\is_array($data)) {
284                    foreach ($data as $plugin) {
285                        if (
286                            \is_array($plugin)
287                            && ($plugin['active'] ?? false) === true
288                            && isset($plugin['path'])
289                            && \is_string($plugin['path'])
290                        ) {
291                            $customFieldDir = rtrim($this->projectDir . '/' . $plugin['path'], '/')
292                                . '/src/CustomField';
293                            if (is_dir($customFieldDir)) {
294                                $dirs[] = $customFieldDir;
295                            }
296                        }
297                    }
298                }
299            }
300        }
301
302        return $dirs;
303    }
304}