25
25
use Magento \SemanticVersionChecker \Registry \XmlRegistry ;
26
26
use PHPSemVerChecker \Registry \Registry ;
27
27
use PHPSemVerChecker \Report \Report ;
28
+ use Magento \SemanticVersionChecker \Operation \SystemXml \DuplicateFieldAdded ;
29
+ use RecursiveDirectoryIterator ;
28
30
29
31
/**
30
32
* Analyzes <kbd>system.xml</kbd> files:
@@ -92,14 +94,152 @@ public function analyze($registryBefore, $registryAfter)
92
94
$ beforeFile = $ registryBefore ->mapping [XmlRegistry::NODES_KEY ][$ moduleName ];
93
95
$ this ->reportRemovedNodes ($ beforeFile , $ removedNodes );
94
96
}
97
+
95
98
if ($ addedNodes ) {
96
99
$ afterFile = $ registryAfter ->mapping [XmlRegistry::NODES_KEY ][$ moduleName ];
97
- $ this ->reportAddedNodes ($ afterFile , $ addedNodes );
100
+ if (strpos ($ afterFile , '_files ' ) !== false ) {
101
+ $ this ->reportAddedNodes ($ afterFile , $ addedNodes );
102
+ } else {
103
+ $ baseDir = $ this ->getBaseDir ($ afterFile );
104
+ foreach ($ addedNodes as $ nodeId => $ node ) {
105
+ $ newNodeData = $ this ->getNodeData ($ node );
106
+ $ nodePath = $ newNodeData ['path ' ];
107
+
108
+ // Extract section, group, and fieldId with error handling
109
+ $ extractedData = $ this ->extractSectionGroupField ($ nodePath );
110
+ if ($ extractedData === null ) {
111
+ // Skip the node if its path is invalid
112
+ continue ;
113
+ }
114
+
115
+ // Extract section, group, and fieldId
116
+ list ($ sectionId , $ groupId , $ fieldId ) = $ extractedData ;
117
+
118
+ // Call function to check if this field is duplicated in other system.xml files
119
+ $ isDuplicated = $ this ->isDuplicatedFieldInXml (
120
+ $ baseDir ,
121
+ $ sectionId ,
122
+ $ groupId ,
123
+ $ fieldId ,
124
+ $ afterFile
125
+ );
126
+
127
+ foreach ($ isDuplicated as $ isDuplicatedItem ) {
128
+ if ($ isDuplicatedItem ['status ' ] === 'duplicate ' ) {
129
+ $ this ->reportDuplicateNodes ($ afterFile , [$ nodeId => $ node ]);
130
+ } else {
131
+ $ this ->reportAddedNodes ($ afterFile , [$ nodeId => $ node ]);
132
+ }
133
+ }
134
+ }
135
+ }
98
136
}
99
137
}
100
138
return $ this ->report ;
101
139
}
102
140
141
+ /**
142
+ * Get Magento Base directory from the path
143
+ *
144
+ * @param string $filePath
145
+ * @return string|null
146
+ */
147
+ private function getBaseDir (string $ filePath ): ?string
148
+ {
149
+ $ currentDir = dirname ($ filePath );
150
+ while ($ currentDir !== '/ ' && $ currentDir !== false ) {
151
+ // Check if current directory contains files unique to Magento root
152
+ if (file_exists ($ currentDir . '/SECURITY.md ' )) {
153
+ return $ currentDir ; // Found the Magento base directory
154
+ }
155
+ $ currentDir = dirname ($ currentDir );
156
+ }
157
+ return null ;
158
+ }
159
+
160
+ /**
161
+ * Search for system.xml files in both app/code and vendor directories, excluding the provided file.
162
+ *
163
+ * @param string $magentoBaseDir The base directory of Magento.
164
+ * @param string|null $excludeFile The file to exclude from the search.
165
+ * @return array An array of paths to system.xml files, excluding the specified file.
166
+ */
167
+ private function getSystemXmlFiles (string $ magentoBaseDir , ?string $ excludeFile = null ): array
168
+ {
169
+ $ systemXmlFiles = [];
170
+ $ directoryToSearch = [
171
+ $ magentoBaseDir . '/app/code '
172
+ ];
173
+
174
+ // Check if 'vendor' directory exists, and only add it if it does
175
+ if (is_dir ($ magentoBaseDir . '/vendor ' )) {
176
+ $ directoriesToSearch [] = $ magentoBaseDir . '/vendor ' ;
177
+ }
178
+ foreach ($ directoryToSearch as $ directory ) {
179
+ $ iterator = new \RecursiveIteratorIterator (new RecursiveDirectoryIterator ($ directory ));
180
+ foreach ($ iterator as $ file ) {
181
+ if ($ file ->getfileName () === 'system.xml ' ) {
182
+ $ filePath = $ file ->getRealPath ();
183
+ if ($ filePath !== $ excludeFile ) {
184
+ $ systemXmlFiles [] = $ file ->getRealPath ();
185
+ }
186
+ }
187
+ }
188
+ }
189
+ return $ systemXmlFiles ;
190
+ }
191
+
192
+ /**
193
+ * Method to extract section, group and field from the Node
194
+ *
195
+ * @param string $nodePath
196
+ * @return array|null
197
+ */
198
+ private function extractSectionGroupField (string $ nodePath ): ?array
199
+ {
200
+ $ parts = explode ('/ ' , $ nodePath );
201
+
202
+ if (count ($ parts ) < 3 ) {
203
+ // Invalid path if there are fewer than 3 parts
204
+ return null ;
205
+ }
206
+
207
+ $ sectionId = $ parts [0 ];
208
+ $ groupId = $ parts [1 ];
209
+ $ fieldId = $ parts [2 ];
210
+
211
+ return [$ sectionId , $ groupId , $ fieldId ];
212
+ }
213
+
214
+ /**
215
+ * Method to get Node Data using reflection class
216
+ *
217
+ * @param object|string $node
218
+ * @return array
219
+ * @throws \ReflectionException
220
+ */
221
+ private function getNodeData (object |string $ node ): array
222
+ {
223
+ $ data = [];
224
+
225
+ // Use reflection to get accessible properties
226
+ $ reflection = new \ReflectionClass ($ node );
227
+ foreach ($ reflection ->getMethods () as $ method ) {
228
+ // Skip 'getId' and 'getParent' methods for comparison
229
+ if ($ method ->getName () === 'getId ' || $ method ->getName () === 'getParent ' ) {
230
+ continue ;
231
+ }
232
+
233
+ // Dynamically call the getter methods
234
+ if (strpos ($ method ->getName (), 'get ' ) === 0 ) {
235
+ $ propertyName = lcfirst (str_replace ('get ' , '' , $ method ->getName ()));
236
+ $ data [$ propertyName ] = $ method ->invoke ($ node );
237
+ }
238
+ }
239
+
240
+ return $ data ;
241
+ }
242
+
103
243
/**
104
244
* Extracts the node from <var>$registry</var> as an associative array.
105
245
*
@@ -164,13 +304,32 @@ private function reportAddedNodes(string $file, array $nodes)
164
304
}
165
305
}
166
306
307
+ /**
308
+ * Creates reports for <var>$nodes</var> considering that they have been duplicated.
309
+ *
310
+ * @param string $file
311
+ * @param NodeInterface[] $nodes
312
+ * @return void
313
+ */
314
+ private function reportDuplicateNodes (string $ file , array $ nodes ): void
315
+ {
316
+ foreach ($ nodes as $ node ) {
317
+ switch (true ) {
318
+ case $ node instanceof Field:
319
+ $ this ->report ->add ('system ' , new DuplicateFieldAdded ($ file , $ node ->getPath ()));
320
+ break ;
321
+ }
322
+ }
323
+ }
324
+
167
325
/**
168
326
* Creates reports for <var>$modules</var> considering that <kbd>system.xml</kbd> has been removed from them.
169
327
*
170
328
* @param array $modules
171
329
* @param XmlRegistry $registryBefore
330
+ * @return void
172
331
*/
173
- private function reportRemovedFiles (array $ modules , XmlRegistry $ registryBefore )
332
+ private function reportRemovedFiles (array $ modules , XmlRegistry $ registryBefore ): void
174
333
{
175
334
foreach ($ modules as $ module ) {
176
335
$ beforeFile = $ registryBefore ->mapping [XmlRegistry::NODES_KEY ][$ module ];
@@ -183,8 +342,9 @@ private function reportRemovedFiles(array $modules, XmlRegistry $registryBefore)
183
342
*
184
343
* @param string $file
185
344
* @param NodeInterface[] $nodes
345
+ * @return void
186
346
*/
187
- private function reportRemovedNodes (string $ file , array $ nodes )
347
+ private function reportRemovedNodes (string $ file , array $ nodes ): void
188
348
{
189
349
foreach ($ nodes as $ node ) {
190
350
switch (true ) {
@@ -202,4 +362,56 @@ private function reportRemovedNodes(string $file, array $nodes)
202
362
}
203
363
}
204
364
}
365
+
366
+ /**
367
+ * @param string|null $baseDir
368
+ * @param string $sectionId
369
+ * @param string $groupId
370
+ * @param string|null $fieldId
371
+ * @param string $afterFile
372
+ * @return array
373
+ */
374
+ private function isDuplicatedFieldInXml (
375
+ ?string $ baseDir ,
376
+ string $ sectionId ,
377
+ string $ groupId ,
378
+ ?string $ fieldId ,
379
+ string $ afterFile
380
+ ): array {
381
+ $ hasDuplicate = false ;
382
+
383
+ $ result = [
384
+ 'status ' => 'minor ' ,
385
+ 'field ' => $ fieldId
386
+ ];
387
+
388
+ if ($ baseDir ) {
389
+ $ systemXmlFiles = $ this ->getSystemXmlFiles ($ baseDir , $ afterFile );
390
+
391
+ foreach ($ systemXmlFiles as $ systemXmlFile ) {
392
+ $ xmlContent = file_get_contents ($ systemXmlFile );
393
+ try {
394
+ $ xml = new \SimpleXMLElement ($ xmlContent );
395
+ } catch (\Exception $ e ) {
396
+ continue ; // Skip this file if there's a parsing error
397
+ }
398
+ // Find <field> nodes with the given field ID
399
+ // XPath to search for <field> within a specific section and group
400
+ $ fields = $ xml ->xpath ("//section[@id=' $ sectionId']/group[@id=' $ groupId']/field[@id=' $ fieldId'] " );
401
+ if (!empty ($ fields )) {
402
+ $ hasDuplicate = true ; // Set the duplicate flag to true if a match is found
403
+ break ; // Since we found a duplicate, we don't need to check further for this field
404
+ }
405
+ }
406
+ if ($ hasDuplicate ) {
407
+ return [
408
+ [
409
+ 'status ' => 'duplicate ' ,
410
+ 'field ' => $ fieldId
411
+ ]
412
+ ];
413
+ }
414
+ }
415
+ return [$ result ];
416
+ }
205
417
}
0 commit comments