CompilerTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <?php
  2. /**
  3. * Compiler test. Check compilation of DI definitions and code generation
  4. *
  5. * Copyright © Magento, Inc. All rights reserved.
  6. * See COPYING.txt for license details.
  7. */
  8. namespace Magento\Test\Integrity\Di;
  9. use Magento\Framework\Api\Code\Generator\Mapper;
  10. use Magento\Framework\Api\Code\Generator\SearchResults;
  11. use Magento\Framework\App\Filesystem\DirectoryList;
  12. use Magento\Framework\Component\ComponentRegistrar;
  13. use Magento\Framework\Interception\Code\InterfaceValidator;
  14. use Magento\Framework\ObjectManager\Code\Generator\Converter;
  15. use Magento\Framework\ObjectManager\Code\Generator\Factory;
  16. use Magento\Framework\ObjectManager\Code\Generator\Repository;
  17. use Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator;
  18. use Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator;
  19. use Magento\Framework\App\Utility\Files;
  20. use Magento\TestFramework\Integrity\PluginValidator;
  21. /**
  22. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  23. */
  24. class CompilerTest extends \PHPUnit\Framework\TestCase
  25. {
  26. /**
  27. * @var string
  28. */
  29. protected $_command;
  30. /**
  31. * @var \Magento\Framework\Shell
  32. */
  33. protected $_shell;
  34. /**
  35. * @var string
  36. */
  37. protected $_generationDir;
  38. /**
  39. * @var string
  40. */
  41. protected $_compilationDir;
  42. /**
  43. * @var \Magento\Framework\ObjectManager\Config\Mapper\Dom()
  44. */
  45. protected $_mapper;
  46. /**
  47. * @var \Magento\Framework\Code\Validator
  48. */
  49. protected $_validator;
  50. /**
  51. * Class arguments reader
  52. *
  53. * @var PluginValidator
  54. */
  55. protected $pluginValidator;
  56. /**
  57. * @var string[]|null
  58. */
  59. private $pluginBlacklist;
  60. protected function setUp()
  61. {
  62. $this->_shell = new \Magento\Framework\Shell(new \Magento\Framework\Shell\CommandRenderer());
  63. $basePath = BP;
  64. $basePath = str_replace('\\', '/', $basePath);
  65. $directoryList = new DirectoryList($basePath);
  66. $this->_generationDir = $directoryList->getPath(DirectoryList::GENERATED_CODE);
  67. $this->_compilationDir = $directoryList->getPath(DirectoryList::GENERATED_METADATA);
  68. $this->_command = 'php ' . $basePath . '/bin/magento setup:di:compile';
  69. $booleanUtils = new \Magento\Framework\Stdlib\BooleanUtils();
  70. $constInterpreter = new \Magento\Framework\Data\Argument\Interpreter\Constant();
  71. $argumentInterpreter = new \Magento\Framework\Data\Argument\Interpreter\Composite(
  72. [
  73. 'boolean' => new \Magento\Framework\Data\Argument\Interpreter\Boolean($booleanUtils),
  74. 'string' => new \Magento\Framework\Data\Argument\Interpreter\BaseStringUtils($booleanUtils),
  75. 'number' => new \Magento\Framework\Data\Argument\Interpreter\Number(),
  76. 'null' => new \Magento\Framework\Data\Argument\Interpreter\NullType(),
  77. 'object' => new \Magento\Framework\Data\Argument\Interpreter\DataObject($booleanUtils),
  78. 'const' => $constInterpreter,
  79. 'init_parameter' => new \Magento\Framework\App\Arguments\ArgumentInterpreter($constInterpreter),
  80. ],
  81. \Magento\Framework\ObjectManager\Config\Reader\Dom::TYPE_ATTRIBUTE
  82. );
  83. // Add interpreters that reference the composite
  84. $argumentInterpreter->addInterpreter(
  85. 'array',
  86. new \Magento\Framework\Data\Argument\Interpreter\ArrayType($argumentInterpreter)
  87. );
  88. $this->_mapper = new \Magento\Framework\ObjectManager\Config\Mapper\Dom(
  89. $argumentInterpreter,
  90. $booleanUtils,
  91. new \Magento\Framework\ObjectManager\Config\Mapper\ArgumentParser()
  92. );
  93. $this->_validator = new \Magento\Framework\Code\Validator();
  94. $this->_validator->add(new \Magento\Framework\Code\Validator\ConstructorIntegrity());
  95. $this->_validator->add(new \Magento\Framework\Code\Validator\TypeDuplication());
  96. $this->_validator->add(new \Magento\Framework\Code\Validator\ArgumentSequence());
  97. $this->_validator->add(new \Magento\Framework\Code\Validator\ConstructorArgumentTypes());
  98. $this->pluginValidator = new PluginValidator(new InterfaceValidator());
  99. }
  100. /**
  101. * Return plugin blacklist class names
  102. *
  103. * @return string[]
  104. */
  105. private function getPluginBlacklist(): array
  106. {
  107. if ($this->pluginBlacklist === null) {
  108. $blacklistFiles = str_replace(
  109. '\\',
  110. '/',
  111. realpath(__DIR__) . '/../_files/blacklist/compiler_plugins*.txt'
  112. );
  113. $blacklistItems = [];
  114. foreach (glob($blacklistFiles) as $fileName) {
  115. $blacklistItems = array_merge(
  116. $blacklistItems,
  117. file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)
  118. );
  119. }
  120. $this->pluginBlacklist = $blacklistItems;
  121. }
  122. return $this->pluginBlacklist;
  123. }
  124. /**
  125. * Validate DI config file
  126. *
  127. * @param string $file
  128. */
  129. protected function _validateFile($file)
  130. {
  131. $dom = new \DOMDocument();
  132. $dom->load($file);
  133. $data = $this->_mapper->convert($dom);
  134. foreach ($data as $instanceName => $parameters) {
  135. if (!isset($parameters['parameters']) || empty($parameters['parameters'])) {
  136. continue;
  137. }
  138. if (\Magento\Framework\App\Utility\Classes::isVirtual($instanceName)) {
  139. $instanceName = \Magento\Framework\App\Utility\Classes::resolveVirtualType($instanceName);
  140. }
  141. if (!$this->_classExistsAsReal($instanceName)) {
  142. continue;
  143. }
  144. $reflectionClass = new \ReflectionClass($instanceName);
  145. $constructor = $reflectionClass->getConstructor();
  146. if (!$constructor) {
  147. $this->fail('Class ' . $instanceName . ' does not have __constructor');
  148. }
  149. $parameters = $parameters['parameters'];
  150. $classParameters = $constructor->getParameters();
  151. foreach ($classParameters as $classParameter) {
  152. $parameterName = $classParameter->getName();
  153. if (array_key_exists($parameterName, $parameters)) {
  154. unset($parameters[$parameterName]);
  155. }
  156. }
  157. $message = 'Configuration of ' . $instanceName . ' contains data for non-existed parameters: ' . implode(
  158. ', ',
  159. array_keys($parameters)
  160. );
  161. $this->assertEmpty($parameters, $message);
  162. }
  163. }
  164. /**
  165. * Checks if class is a real one or generated Factory
  166. * @param string $instanceName class name
  167. * @throws \PHPUnit\Framework\AssertionFailedError
  168. * @return bool
  169. */
  170. protected function _classExistsAsReal($instanceName)
  171. {
  172. if (class_exists($instanceName)) {
  173. return true;
  174. }
  175. // check for generated factory
  176. if (substr($instanceName, -7) == 'Factory' && class_exists(substr($instanceName, 0, -7))) {
  177. return false;
  178. }
  179. $this->fail('Detected configuration of non existed class: ' . $instanceName);
  180. }
  181. /**
  182. * Get php classes list
  183. *
  184. * @return array
  185. */
  186. protected function _phpClassesDataProvider()
  187. {
  188. $generationPath = str_replace('/', '\\', $this->_generationDir);
  189. $files = Files::init()->getPhpFiles(Files::INCLUDE_APP_CODE | Files::INCLUDE_LIBS);
  190. $patterns = ['/' . preg_quote($generationPath) . '/',];
  191. $replacements = [''];
  192. $componentRegistrar = new ComponentRegistrar();
  193. foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $modulePath) {
  194. $patterns[] = '/' . preg_quote(str_replace('/', '\\', $modulePath)) . '/';
  195. $replacements[] = '\\' . str_replace('_', '\\', $moduleName);
  196. }
  197. foreach ($componentRegistrar->getPaths(ComponentRegistrar::LIBRARY) as $libPath) {
  198. $patterns[] = '/' . preg_quote(str_replace('/', '\\', $libPath)) . '/';
  199. $replacements[] = '\\Magento\\Framework';
  200. }
  201. /** Convert file names into class name format */
  202. $classes = [];
  203. foreach ($files as $file) {
  204. $file = str_replace('/', '\\', $file);
  205. $filePath = preg_replace($patterns, $replacements, $file);
  206. $className = substr($filePath, 0, -4);
  207. if (class_exists($className, false)) {
  208. $file = str_replace('\\', DIRECTORY_SEPARATOR, $file);
  209. $classes[$file] = $className;
  210. }
  211. }
  212. /** Build class inheritance hierarchy */
  213. $output = [];
  214. $allowedFiles = array_keys($classes);
  215. foreach ($classes as $class) {
  216. if (!in_array($class, $output)) {
  217. $output = array_merge($output, $this->_buildInheritanceHierarchyTree($class, $allowedFiles));
  218. $output = array_unique($output);
  219. }
  220. }
  221. /** Convert data into data provider format */
  222. $outputClasses = [];
  223. foreach ($output as $className) {
  224. $outputClasses[] = [$className];
  225. }
  226. return $outputClasses;
  227. }
  228. /**
  229. * Build inheritance hierarchy tree
  230. *
  231. * @param string $className
  232. * @param array $allowedFiles
  233. * @return array
  234. */
  235. protected function _buildInheritanceHierarchyTree($className, array $allowedFiles)
  236. {
  237. $output = [];
  238. if (0 !== strpos($className, '\\')) {
  239. $className = '\\' . $className;
  240. }
  241. $class = new \ReflectionClass($className);
  242. $parent = $class->getParentClass();
  243. $file = false;
  244. if ($parent) {
  245. $file = str_replace('\\', DIRECTORY_SEPARATOR, $parent->getFileName());
  246. }
  247. /** Prevent analysis of non Magento classes */
  248. if ($parent && in_array($file, $allowedFiles)) {
  249. $output = array_merge(
  250. $this->_buildInheritanceHierarchyTree($parent->getName(), $allowedFiles),
  251. [$className],
  252. $output
  253. );
  254. } else {
  255. $output[] = $className;
  256. }
  257. return array_unique($output);
  258. }
  259. /**
  260. * Validate class
  261. *
  262. * @param string $className
  263. */
  264. protected function _validateClass($className)
  265. {
  266. try {
  267. $this->_validator->validate($className);
  268. } catch (\Magento\Framework\Exception\ValidatorException $exceptions) {
  269. $this->fail($exceptions->getMessage());
  270. } catch (\ReflectionException $exceptions) {
  271. $this->fail($exceptions->getMessage());
  272. }
  273. }
  274. /**
  275. * Validate DI configuration
  276. */
  277. public function testConfigurationOfInstanceParameters()
  278. {
  279. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  280. $invoker(
  281. function ($file) {
  282. $this->_validateFile($file);
  283. },
  284. Files::init()->getDiConfigs(true)
  285. );
  286. }
  287. /**
  288. * Validate constructor integrity
  289. */
  290. public function testConstructorIntegrity()
  291. {
  292. $generatorIo = new \Magento\Framework\Code\Generator\Io(
  293. new \Magento\Framework\Filesystem\Driver\File(),
  294. $this->_generationDir
  295. );
  296. $generator = new \Magento\Framework\Code\Generator(
  297. $generatorIo,
  298. [
  299. Factory::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Factory::class,
  300. Repository::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Repository::class,
  301. Converter::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Converter::class,
  302. Mapper::ENTITY_TYPE => \Magento\Framework\Api\Code\Generator\Mapper::class,
  303. SearchResults::ENTITY_TYPE => \Magento\Framework\Api\Code\Generator\SearchResults::class,
  304. ExtensionAttributesInterfaceGenerator::ENTITY_TYPE =>
  305. \Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator::class,
  306. ExtensionAttributesGenerator::ENTITY_TYPE =>
  307. \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::class
  308. ]
  309. );
  310. $generationAutoloader = new \Magento\Framework\Code\Generator\Autoloader($generator);
  311. spl_autoload_register([$generationAutoloader, 'load']);
  312. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  313. $invoker(
  314. function ($className) {
  315. $this->_validateClass($className);
  316. },
  317. $this->_phpClassesDataProvider()
  318. );
  319. spl_autoload_unregister([$generationAutoloader, 'load']);
  320. }
  321. /**
  322. * Test consistency of plugin interfaces
  323. */
  324. public function testPluginInterfaces()
  325. {
  326. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  327. $invoker(
  328. function ($plugin, $type) {
  329. $this->validatePlugins($plugin, $type);
  330. },
  331. $this->pluginDataProvider()
  332. );
  333. }
  334. /**
  335. * Validate plugin interface
  336. *
  337. * @param string $plugin
  338. * @param string $type
  339. */
  340. protected function validatePlugins($plugin, $type)
  341. {
  342. try {
  343. $module = \Magento\Framework\App\Utility\Classes::getClassModuleName($type);
  344. if (Files::init()->isModuleExists($module)) {
  345. $this->pluginValidator->validate($plugin, $type);
  346. }
  347. } catch (\Magento\Framework\Exception\ValidatorException $exception) {
  348. $this->fail($exception->getMessage());
  349. }
  350. }
  351. /**
  352. * Get application plugins
  353. *
  354. * @return array
  355. * @throws \Exception
  356. */
  357. protected function pluginDataProvider()
  358. {
  359. $files = Files::init()->getDiConfigs();
  360. $plugins = [];
  361. foreach ($files as $file) {
  362. $dom = new \DOMDocument();
  363. $dom->load($file);
  364. $xpath = new \DOMXPath($dom);
  365. $pluginList = $xpath->query('//config/type/plugin');
  366. foreach ($pluginList as $node) {
  367. /** @var $node \DOMNode */
  368. $type = $node->parentNode->attributes->getNamedItem('name')->nodeValue;
  369. $type = \Magento\Framework\App\Utility\Classes::resolveVirtualType($type);
  370. if ($node->attributes->getNamedItem('type')) {
  371. $plugin = $node->attributes->getNamedItem('type')->nodeValue;
  372. if (!in_array($plugin, $this->getPluginBlacklist())) {
  373. $plugin = \Magento\Framework\App\Utility\Classes::resolveVirtualType($plugin);
  374. $plugins[] = ['plugin' => $plugin, 'intercepted type' => $type];
  375. }
  376. }
  377. }
  378. }
  379. return $plugins;
  380. }
  381. }