2525use Magento \SemanticVersionChecker \Registry \XmlRegistry ;
2626use PHPSemVerChecker \Registry \Registry ;
2727use PHPSemVerChecker \Report \Report ;
28+ use Magento \SemanticVersionChecker \Operation \SystemXml \DuplicateFieldAdded ;
29+ use RecursiveDirectoryIterator ;
2830
2931/**
3032 * Analyzes <kbd>system.xml</kbd> files:
@@ -92,14 +94,152 @@ public function analyze($registryBefore, $registryAfter)
9294 $ beforeFile = $ registryBefore ->mapping [XmlRegistry::NODES_KEY ][$ moduleName ];
9395 $ this ->reportRemovedNodes ($ beforeFile , $ removedNodes );
9496 }
97+
9598 if ($ addedNodes ) {
9699 $ 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+ }
98136 }
99137 }
100138 return $ this ->report ;
101139 }
102140
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+
103243 /**
104244 * Extracts the node from <var>$registry</var> as an associative array.
105245 *
@@ -164,13 +304,32 @@ private function reportAddedNodes(string $file, array $nodes)
164304 }
165305 }
166306
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+
167325 /**
168326 * Creates reports for <var>$modules</var> considering that <kbd>system.xml</kbd> has been removed from them.
169327 *
170328 * @param array $modules
171329 * @param XmlRegistry $registryBefore
330+ * @return void
172331 */
173- private function reportRemovedFiles (array $ modules , XmlRegistry $ registryBefore )
332+ private function reportRemovedFiles (array $ modules , XmlRegistry $ registryBefore ): void
174333 {
175334 foreach ($ modules as $ module ) {
176335 $ beforeFile = $ registryBefore ->mapping [XmlRegistry::NODES_KEY ][$ module ];
@@ -183,8 +342,9 @@ private function reportRemovedFiles(array $modules, XmlRegistry $registryBefore)
183342 *
184343 * @param string $file
185344 * @param NodeInterface[] $nodes
345+ * @return void
186346 */
187- private function reportRemovedNodes (string $ file , array $ nodes )
347+ private function reportRemovedNodes (string $ file , array $ nodes ): void
188348 {
189349 foreach ($ nodes as $ node ) {
190350 switch (true ) {
@@ -202,4 +362,56 @@ private function reportRemovedNodes(string $file, array $nodes)
202362 }
203363 }
204364 }
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+ }
205417}
0 commit comments