PhpRule.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <?php
  2. /**
  3. * Rule for searching php file dependency
  4. *
  5. * Copyright © Magento, Inc. All rights reserved.
  6. * See COPYING.txt for license details.
  7. */
  8. namespace Magento\TestFramework\Dependency;
  9. use Magento\Framework\App\Utility\Files;
  10. class PhpRule implements RuleInterface
  11. {
  12. /**
  13. * List of filepaths for DI files
  14. *
  15. * @var array
  16. */
  17. private $diFiles;
  18. /**
  19. * Map from plugin classes to the subjects they modify
  20. *
  21. * @var array
  22. */
  23. private $pluginMap;
  24. /**
  25. * List of routers
  26. *
  27. * Format: array(
  28. * '{Router}' => '{Module_Name}'
  29. * )
  30. *
  31. * @var array
  32. */
  33. protected $_mapRouters = [];
  34. /**
  35. * List of layout blocks
  36. *
  37. * Format: array(
  38. * '{Area}' => array(
  39. * '{Block_Name}' => array('{Module_Name}' => '{Module_Name}')
  40. * ))
  41. *
  42. * @var array
  43. */
  44. protected $_mapLayoutBlocks = [];
  45. /**
  46. * Default modules list.
  47. *
  48. * @var array
  49. */
  50. protected $_defaultModules = [
  51. 'frontend' => 'Magento\Theme',
  52. 'adminhtml' => 'Magento\Adminhtml',
  53. ];
  54. /**
  55. * Constructor
  56. *
  57. * @param array $mapRouters
  58. * @param array $mapLayoutBlocks
  59. * @param array $pluginMap
  60. */
  61. public function __construct(array $mapRouters, array $mapLayoutBlocks, array $pluginMap = [])
  62. {
  63. $this->_mapRouters = $mapRouters;
  64. $this->_mapLayoutBlocks = $mapLayoutBlocks;
  65. $this->_namespaces = implode('|', \Magento\Framework\App\Utility\Files::init()->getNamespaces());
  66. $this->pluginMap = $pluginMap ?: null;
  67. }
  68. /**
  69. * Gets alien dependencies information for current module by analyzing file's contents
  70. *
  71. * @param string $currentModule
  72. * @param string $fileType
  73. * @param string $file
  74. * @param string $contents
  75. * @return array
  76. */
  77. public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
  78. {
  79. if (!in_array($fileType, ['php', 'template'])) {
  80. return [];
  81. }
  82. $dependenciesInfo = [];
  83. $dependenciesInfo = $this->considerCaseDependencies(
  84. $dependenciesInfo,
  85. $this->caseClassesAndIdentifiers($currentModule, $file, $contents)
  86. );
  87. $dependenciesInfo = $this->considerCaseDependencies(
  88. $dependenciesInfo,
  89. $this->_caseGetUrl($currentModule, $contents)
  90. );
  91. $dependenciesInfo = $this->considerCaseDependencies(
  92. $dependenciesInfo,
  93. $this->_caseLayoutBlock($currentModule, $fileType, $file, $contents)
  94. );
  95. return $dependenciesInfo;
  96. }
  97. /**
  98. * Check references to classes and identifiers defined in other modules
  99. *
  100. * @param string $currentModule
  101. * @param string $file
  102. * @param string $contents
  103. * @return array
  104. */
  105. private function caseClassesAndIdentifiers($currentModule, $file, &$contents)
  106. {
  107. $pattern = '~\b(?<class>(?<module>('
  108. . implode(
  109. '[_\\\\]|',
  110. Files::init()->getNamespaces()
  111. )
  112. . '[_\\\\])[a-zA-Z0-9]+)'
  113. . '(?<class_inside_module>[a-zA-Z0-9_\\\\]*))\b(?:::(?<module_scoped_key>[a-z0-9_]+)[\'"])?~';
  114. if (!preg_match_all($pattern, $contents, $matches)) {
  115. return [];
  116. }
  117. $dependenciesInfo = [];
  118. $matches['module'] = array_unique($matches['module']);
  119. foreach ($matches['module'] as $i => $referenceModule) {
  120. $referenceModule = str_replace('_', '\\', $referenceModule);
  121. if ($currentModule == $referenceModule) {
  122. continue;
  123. }
  124. $dependencyClass = trim($matches['class'][$i]);
  125. if (empty($matches['class_inside_module'][$i]) && !empty($matches['module_scoped_key'][$i])) {
  126. $dependencyType = RuleInterface::TYPE_SOFT;
  127. } else {
  128. $currentClass = $this->getClassFromFilepath($file, $currentModule);
  129. $dependencyType = $this->isPluginDependency($currentClass, $dependencyClass)
  130. ? RuleInterface::TYPE_SOFT
  131. : RuleInterface::TYPE_HARD;
  132. }
  133. $dependenciesInfo[] = [
  134. 'module' => $referenceModule,
  135. 'type' => $dependencyType,
  136. 'source' => $dependencyClass,
  137. ];
  138. }
  139. return $dependenciesInfo;
  140. }
  141. /**
  142. * Get class name from filename based on class/file naming conventions
  143. *
  144. * @param string $filepath
  145. * @param string $module
  146. * @return string
  147. */
  148. private function getClassFromFilepath($filepath, $module)
  149. {
  150. $class = strstr($filepath, str_replace(['_', '\\', '/'], DIRECTORY_SEPARATOR, $module));
  151. $class = str_replace(DIRECTORY_SEPARATOR, '\\', strstr($class, '.php', true));
  152. return $class;
  153. }
  154. /**
  155. * @return array
  156. * @throws \Exception
  157. */
  158. private function loadDiFiles()
  159. {
  160. if (!$this->diFiles) {
  161. $this->diFiles = Files::init()->getDiConfigs();
  162. }
  163. return $this->diFiles;
  164. }
  165. /**
  166. * Generate an array of plugin info
  167. *
  168. * @return array
  169. */
  170. private function loadPluginMap()
  171. {
  172. if (!$this->pluginMap) {
  173. foreach ($this->loadDiFiles() as $filepath) {
  174. $dom = new \DOMDocument();
  175. $dom->loadXML(file_get_contents($filepath));
  176. $typeNodes = $dom->getElementsByTagName('type');
  177. /** @var \DOMElement $type */
  178. foreach ($typeNodes as $type) {
  179. /** @var \DOMElement $plugin */
  180. foreach ($type->getElementsByTagName('plugin') as $plugin) {
  181. $subject = $type->getAttribute('name');
  182. $pluginType = $plugin->getAttribute('type');
  183. $this->pluginMap[$pluginType] = $subject;
  184. }
  185. }
  186. }
  187. }
  188. return $this->pluginMap;
  189. }
  190. /**
  191. * Determine whether a the dependency relation is because of a plugin
  192. *
  193. * True IFF the dependent is a plugin for some class in the same module as the dependency.
  194. *
  195. * @param string $dependent
  196. * @param string $dependency
  197. * @return bool
  198. */
  199. private function isPluginDependency($dependent, $dependency)
  200. {
  201. $pluginMap = $this->loadPluginMap();
  202. $subject = isset($pluginMap[$dependent])
  203. ? $pluginMap[$dependent]
  204. : null;
  205. if ($subject === $dependency) {
  206. return true;
  207. } elseif ($subject) {
  208. $subjectModule = substr($subject, 0, strpos($subject, '\\', 9)); // (strlen('Magento\\') + 1) === 9
  209. return strpos($dependency, $subjectModule) === 0;
  210. } else {
  211. return false;
  212. }
  213. }
  214. /**
  215. * Check get URL method
  216. *
  217. * Ex.: getUrl('{path}')
  218. *
  219. * @param $currentModule
  220. * @param $contents
  221. * @return array
  222. */
  223. protected function _caseGetUrl($currentModule, &$contents)
  224. {
  225. $pattern = '/[\->:]+(?<source>getUrl\([\'"](?<router>[\w\/*]+)[\'"])/';
  226. $dependencies = [];
  227. if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) {
  228. return $dependencies;
  229. }
  230. foreach ($matches as $item) {
  231. $router = str_replace('/', '\\', $item['router']);
  232. if (isset($this->_mapRouters[$router])) {
  233. $modules = $this->_mapRouters[$router];
  234. if (!in_array($currentModule, $modules)) {
  235. foreach ($modules as $module) {
  236. $dependencies[] = [
  237. 'module' => $module,
  238. 'type' => RuleInterface::TYPE_HARD,
  239. 'source' => $item['source'],
  240. ];
  241. }
  242. }
  243. }
  244. }
  245. return $dependencies;
  246. }
  247. /**
  248. * Check layout blocks
  249. *
  250. * @param $currentModule
  251. * @param $fileType
  252. * @param $file
  253. * @param $contents
  254. * @return array
  255. */
  256. protected function _caseLayoutBlock($currentModule, $fileType, $file, &$contents)
  257. {
  258. $pattern = '/[\->:]+(?<source>(?:getBlock|getBlockHtml)\([\'"](?<block>[\w\.\-]+)[\'"]\))/';
  259. $area = $this->_getAreaByFile($file, $fileType);
  260. $result = [];
  261. if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) {
  262. return $result;
  263. }
  264. foreach ($matches as $match) {
  265. if (in_array($match['block'], ['root', 'content'])) {
  266. continue;
  267. }
  268. $check = $this->_checkDependencyLayoutBlock($currentModule, $area, $match['block']);
  269. $module = isset($check['module']) ? $check['module'] : null;
  270. if ($module) {
  271. $result[$module] = [
  272. 'type' => RuleInterface::TYPE_HARD,
  273. 'source' => $match['source'],
  274. ];
  275. }
  276. }
  277. return $this->_getUniqueDependencies($result);
  278. }
  279. /**
  280. * Get area from file path
  281. *
  282. * @param $file
  283. * @param $fileType
  284. * @return string|null
  285. */
  286. protected function _getAreaByFile($file, $fileType)
  287. {
  288. if ($fileType == 'php') {
  289. return null;
  290. }
  291. $area = 'default';
  292. if (preg_match('/\/(?<area>adminhtml|frontend)\//', $file, $matches)) {
  293. $area = $matches['area'];
  294. }
  295. return $area;
  296. }
  297. /**
  298. * Check layout block dependency
  299. *
  300. * Return: array(
  301. * 'module' // dependent module
  302. * 'source' // source text
  303. * )
  304. *
  305. * @param $currentModule
  306. * @param $area
  307. * @param $block
  308. * @return array
  309. */
  310. protected function _checkDependencyLayoutBlock($currentModule, $area, $block)
  311. {
  312. if (isset($this->_mapLayoutBlocks[$area][$block]) || $area === null) {
  313. // CASE 1: No dependencies
  314. $modules = [];
  315. if ($area === null) {
  316. foreach ($this->_mapLayoutBlocks as $blocks) {
  317. if (array_key_exists($block, $blocks)) {
  318. $modules += $blocks[$block];
  319. }
  320. }
  321. } else {
  322. $modules = $this->_mapLayoutBlocks[$area][$block];
  323. }
  324. if (isset($modules[$currentModule])) {
  325. return ['module' => null];
  326. }
  327. // CASE 2: Single dependency
  328. if (1 == count($modules)) {
  329. return ['module' => current($modules)];
  330. }
  331. // CASE 3: Default module dependency
  332. $defaultModule = $this->_getDefaultModuleName($area);
  333. if (isset($modules[$defaultModule])) {
  334. return ['module' => $defaultModule];
  335. }
  336. }
  337. // CASE 4: \Exception - Undefined block
  338. return [];
  339. }
  340. /**
  341. * Retrieve default module name (by area)
  342. *
  343. * @param string $area
  344. * @return null
  345. */
  346. protected function _getDefaultModuleName($area = 'default')
  347. {
  348. if (isset($this->_defaultModules[$area])) {
  349. return $this->_defaultModules[$area];
  350. }
  351. return null;
  352. }
  353. /**
  354. * Retrieve unique dependencies
  355. *
  356. * @param array $dependencies
  357. * @return array
  358. */
  359. protected function _getUniqueDependencies($dependencies = [])
  360. {
  361. $result = [];
  362. foreach ($dependencies as $module => $value) {
  363. $result[] = ['module' => $module, 'type' => $value['type'], 'source' => $value['source']];
  364. }
  365. return $result;
  366. }
  367. /**
  368. * @param array $known
  369. * @param array $new
  370. * @return array
  371. */
  372. private function considerCaseDependencies($known, $new)
  373. {
  374. return array_merge($known, $new);
  375. }
  376. }