|
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,142 @@ 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 | + $baseDir = $this->getBaseDir($afterFile); |
| 101 | + foreach ($addedNodes as $nodeId => $node) { |
| 102 | + $newNodeData = $this->getNodeData($node); |
| 103 | + $nodePath = $newNodeData['path']; |
| 104 | + |
| 105 | + // Extract section, group, and fieldId with error handling |
| 106 | + $extractedData = $this->extractSectionGroupField($nodePath); |
| 107 | + if ($extractedData === null) { |
| 108 | + // Skip the node if its path is invalid |
| 109 | + continue; |
| 110 | + } |
| 111 | + |
| 112 | + // Extract section, group, and fieldId |
| 113 | + list($sectionId, $groupId, $fieldId) = $extractedData; |
| 114 | + |
| 115 | + // Call function to check if this field is duplicated in other system.xml files |
| 116 | + $isDuplicated = $this->isDuplicatedFieldInXml($baseDir, $sectionId, $groupId, $fieldId, $afterFile); |
| 117 | + |
| 118 | + foreach ($isDuplicated as $isDuplicatedItem) { |
| 119 | + if ($isDuplicatedItem['status'] === 'duplicate') { |
| 120 | + $this->reportDuplicateNodes($afterFile, [$nodeId => $node ]); |
| 121 | + } else { |
| 122 | + $this->reportAddedNodes($afterFile, [$nodeId => $node ]); |
| 123 | + } |
| 124 | + } |
| 125 | + } |
98 | 126 | }
|
99 | 127 | }
|
100 | 128 | return $this->report;
|
101 | 129 | }
|
102 | 130 |
|
| 131 | + /** |
| 132 | + * Get Magento Base directory from the path |
| 133 | + * |
| 134 | + * @param string $filePath |
| 135 | + * @return string|null |
| 136 | + */ |
| 137 | + private function getBaseDir($filePath) |
| 138 | + { |
| 139 | + $currentDir = dirname($filePath); |
| 140 | + while ($currentDir !== '/' && $currentDir !== false) { |
| 141 | + // Check if current directory contains files unique to Magento root |
| 142 | + if (file_exists($currentDir . '/SECURITY.md')) { |
| 143 | + return $currentDir; // Found the Magento base directory |
| 144 | + } |
| 145 | + $currentDir = dirname($currentDir); |
| 146 | + } |
| 147 | + return null; |
| 148 | + } |
| 149 | + |
| 150 | + /** |
| 151 | + * Search for system.xml files in both app/code and vendor directories, excluding the provided file. |
| 152 | + * |
| 153 | + * @param string $magentoBaseDir The base directory of Magento. |
| 154 | + * @param string $excludeFile The file to exclude from the search. |
| 155 | + * @return array An array of paths to system.xml files, excluding the specified file. |
| 156 | + */ |
| 157 | + private function getSystemXmlFiles($magentoBaseDir, $excludeFile = null) |
| 158 | + { |
| 159 | + $systemXmlFiles = []; |
| 160 | + $directoryToSearch = [ |
| 161 | + $magentoBaseDir.'/app/code' |
| 162 | + ]; |
| 163 | + |
| 164 | + // Check if 'vendor' directory exists, and only add it if it does |
| 165 | + if (is_dir($magentoBaseDir . '/vendor')) { |
| 166 | + $directoriesToSearch[] = $magentoBaseDir . '/vendor'; |
| 167 | + } |
| 168 | + foreach ($directoryToSearch as $directory) { |
| 169 | + $iterator = new \RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); |
| 170 | + foreach ($iterator as $file) { |
| 171 | + if ($file->getfileName() === 'system.xml') { |
| 172 | + $filePath = $file->getRealPath(); |
| 173 | + if ($filePath !== $excludeFile) { |
| 174 | + $systemXmlFiles[] = $file->getRealPath(); |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + return $systemXmlFiles; |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Method to extract section, group and field from the Node |
| 184 | + * |
| 185 | + * @param $nodePath |
| 186 | + * @return array|null |
| 187 | + */ |
| 188 | + private function extractSectionGroupField($nodePath) |
| 189 | + { |
| 190 | + $parts = explode('/', $nodePath); |
| 191 | + |
| 192 | + if (count($parts) < 3) { |
| 193 | + // Invalid path if there are fewer than 3 parts |
| 194 | + return null; |
| 195 | + } |
| 196 | + |
| 197 | + $sectionId = $parts[0]; |
| 198 | + $groupId = $parts[1]; |
| 199 | + $fieldId = $parts[2]; |
| 200 | + |
| 201 | + return [$sectionId, $groupId, $fieldId]; |
| 202 | + } |
| 203 | + |
| 204 | + /** |
| 205 | + * Method to get Node Data using reflection class |
| 206 | + * |
| 207 | + * @param $node |
| 208 | + * @return array |
| 209 | + * @throws \ReflectionException |
| 210 | + */ |
| 211 | + private function getNodeData($node) |
| 212 | + { |
| 213 | + $data = []; |
| 214 | + |
| 215 | + // Use reflection to get accessible properties |
| 216 | + $reflection = new \ReflectionClass($node); |
| 217 | + foreach ($reflection->getMethods() as $method) { |
| 218 | + // Skip 'getId' and 'getParent' methods for comparison |
| 219 | + if ($method->getName() === 'getId' || $method->getName() === 'getParent') { |
| 220 | + continue; |
| 221 | + } |
| 222 | + |
| 223 | + // Dynamically call the getter methods |
| 224 | + if (strpos($method->getName(), 'get') === 0) { |
| 225 | + $propertyName = lcfirst(str_replace('get', '', $method->getName())); |
| 226 | + $data[$propertyName] = $method->invoke($node); |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + return $data; |
| 231 | + } |
| 232 | + |
103 | 233 | /**
|
104 | 234 | * Extracts the node from <var>$registry</var> as an associative array.
|
105 | 235 | *
|
@@ -164,6 +294,23 @@ private function reportAddedNodes(string $file, array $nodes)
|
164 | 294 | }
|
165 | 295 | }
|
166 | 296 |
|
| 297 | + /** |
| 298 | + * Creates reports for <var>$nodes</var> considering that they have been duplicated. |
| 299 | + * |
| 300 | + * @param string $file |
| 301 | + * @param NodeInterface[] $nodes |
| 302 | + */ |
| 303 | + private function reportDuplicateNodes(string $file, array $nodes) |
| 304 | + { |
| 305 | + foreach ($nodes as $node) { |
| 306 | + switch (true) { |
| 307 | + case $node instanceof Field: |
| 308 | + $this->report->add('system', new DuplicateFieldAdded($file, $node->getPath())); |
| 309 | + break; |
| 310 | + } |
| 311 | + } |
| 312 | + } |
| 313 | + |
167 | 314 | /**
|
168 | 315 | * Creates reports for <var>$modules</var> considering that <kbd>system.xml</kbd> has been removed from them.
|
169 | 316 | *
|
@@ -202,4 +349,53 @@ private function reportRemovedNodes(string $file, array $nodes)
|
202 | 349 | }
|
203 | 350 | }
|
204 | 351 | }
|
| 352 | + |
| 353 | + /** |
| 354 | + * @param string|null $baseDir |
| 355 | + * @param string $sectionId |
| 356 | + * @param string $groupId |
| 357 | + * @param string $fieldId |
| 358 | + * @param string $afterFile |
| 359 | + * @return array |
| 360 | + * @throws \Exception |
| 361 | + */ |
| 362 | + private function isDuplicatedFieldInXml(?string $baseDir, string $sectionId, string $groupId, ?string $fieldId, string $afterFile): array |
| 363 | + { |
| 364 | + $hasDuplicate = false; |
| 365 | + |
| 366 | + $result = [ |
| 367 | + 'status' => 'minor', |
| 368 | + 'field' => $fieldId |
| 369 | + ]; |
| 370 | + |
| 371 | + if ($baseDir) { |
| 372 | + $systemXmlFiles = $this->getSystemXmlFiles($baseDir, $afterFile); |
| 373 | + |
| 374 | + foreach ($systemXmlFiles as $systemXmlFile) { |
| 375 | + $xmlContent = file_get_contents($systemXmlFile); |
| 376 | + try { |
| 377 | + $xml = new \SimpleXMLElement($xmlContent); |
| 378 | + } catch (\Exception $e) { |
| 379 | + continue; // Skip this file if there's a parsing error |
| 380 | + } |
| 381 | + // Find <field> nodes with the given field ID |
| 382 | + // XPath to search for <field> within a specific section and group |
| 383 | + $fields = $xml->xpath("//section[@id='$sectionId']/group[@id='$groupId']/field[@id='$fieldId']"); |
| 384 | + if (!empty($fields)) { |
| 385 | + $hasDuplicate = true; // Set the duplicate flag to true if a match is found |
| 386 | + break; // Since we found a duplicate, we don't need to check further for this field |
| 387 | + } |
| 388 | + } |
| 389 | + if ($hasDuplicate) { |
| 390 | + return [ |
| 391 | + [ |
| 392 | + 'status' => 'duplicate', |
| 393 | + 'field' => $fieldId |
| 394 | + |
| 395 | + ] |
| 396 | + ]; |
| 397 | + } |
| 398 | + } |
| 399 | + return [$result]; |
| 400 | + } |
205 | 401 | }
|
0 commit comments