ViewFileReferenceTest.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. /**
  3. * Test constructions of layout files
  4. *
  5. * Copyright © Magento, Inc. All rights reserved.
  6. * See COPYING.txt for license details.
  7. */
  8. /**
  9. * This test finds usages of modular view files, searched in non-modular context - it is obsolete and buggy
  10. * functionality, initially introduced in Magento 2.
  11. *
  12. * The test goes through modular calls of view files, and finds out, whether there are theme non-modular files
  13. * with the same path. Before fixing the bug, such call return theme files instead of modular files, which is
  14. * incorrect. After fixing the bug, such calls will start returning modular files, which is not a file we got used
  15. * to see, so such cases are probably should be fixed. The test finds such suspicious places.
  16. *
  17. * The test is intended to be deleted before Magento 2 release. With the release, having non-modular files with the
  18. * same paths as modular ones, is legitimate.
  19. */
  20. namespace Magento\Test\Integrity;
  21. use Magento\Framework\Component\ComponentRegistrar;
  22. /**
  23. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  24. */
  25. class ViewFileReferenceTest extends \PHPUnit\Framework\TestCase
  26. {
  27. /**
  28. * @var \Magento\Framework\View\Design\Fallback\Rule\RuleInterface
  29. */
  30. protected static $_fallbackRule;
  31. /**
  32. * @var \Magento\Framework\View\Design\FileResolution\Fallback\StaticFile
  33. */
  34. protected static $_viewFilesFallback;
  35. /**
  36. * @var \Magento\Framework\View\Design\FileResolution\Fallback\File
  37. */
  38. protected static $_filesFallback;
  39. /**
  40. * @var array
  41. */
  42. protected static $_checkThemeLocales = [];
  43. /**
  44. * @var \Magento\Theme\Model\Theme\Collection
  45. */
  46. protected static $_themeCollection;
  47. /**
  48. * @var \Magento\Framework\Component\ComponentRegistrar
  49. */
  50. protected static $_componentRegistrar;
  51. public static function setUpBeforeClass()
  52. {
  53. $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
  54. $objectManager->configure(
  55. ['preferences' => [\Magento\Theme\Model\Theme::class => \Magento\Theme\Model\Theme\Data::class]]
  56. );
  57. self::$_componentRegistrar = $objectManager->get(\Magento\Framework\Component\ComponentRegistrar::class);
  58. /** @var $fallbackPool \Magento\Framework\View\Design\Fallback\RulePool */
  59. $fallbackPool = $objectManager->get(\Magento\Framework\View\Design\Fallback\RulePool::class);
  60. self::$_fallbackRule = $fallbackPool->getRule(
  61. $fallbackPool::TYPE_STATIC_FILE
  62. );
  63. self::$_viewFilesFallback = $objectManager->get(
  64. \Magento\Framework\View\Design\FileResolution\Fallback\StaticFile::class
  65. );
  66. self::$_filesFallback = $objectManager->get(\Magento\Framework\View\Design\FileResolution\Fallback\File::class);
  67. // Themes to be checked
  68. self::$_themeCollection = $objectManager->get(\Magento\Theme\Model\Theme\Collection::class);
  69. // Compose list of locales, needed to be checked for themes
  70. self::$_checkThemeLocales = [];
  71. foreach (self::$_themeCollection as $theme) {
  72. $themeLocales = self::_getThemeLocales($theme);
  73. $themeLocales[] = null;
  74. // Default non-localized file will need to be checked as well
  75. self::$_checkThemeLocales[$theme->getFullPath()] = $themeLocales;
  76. }
  77. }
  78. /**
  79. * Return array of locales, supported by the theme
  80. *
  81. * @param \Magento\Framework\View\Design\ThemeInterface $theme
  82. * @return array
  83. */
  84. protected static function _getThemeLocales(\Magento\Framework\View\Design\ThemeInterface $theme)
  85. {
  86. $result = [];
  87. $patternDir = self::_getLocalePatternDir($theme);
  88. foreach (\ResourceBundle::getLocales('') as $locale) {
  89. $dir = str_replace('<locale_placeholder>', $locale, $patternDir);
  90. if (is_dir($dir)) {
  91. $result[] = $locale;
  92. }
  93. }
  94. return $result;
  95. }
  96. /**
  97. * Return pattern for theme locale directories, where <locale_placeholder> is placed to mark a locale's location.
  98. *
  99. * @param \Magento\Framework\View\Design\ThemeInterface $theme
  100. * @return string
  101. * @throws \Exception
  102. */
  103. protected static function _getLocalePatternDir(\Magento\Framework\View\Design\ThemeInterface $theme)
  104. {
  105. $localePlaceholder = '<locale_placeholder>';
  106. $params = ['area' => $theme->getArea(), 'theme' => $theme, 'locale' => $localePlaceholder];
  107. $patternDirs = self::$_fallbackRule->getPatternDirs($params);
  108. $themePath = self::$_componentRegistrar->getPath(
  109. \Magento\Framework\Component\ComponentRegistrar::THEME,
  110. $theme->getFullPath()
  111. );
  112. foreach ($patternDirs as $patternDir) {
  113. $patternPath = $patternDir . '/';
  114. if ((strpos($patternPath, $themePath) !== false) // It is theme's directory
  115. && (strpos($patternPath, $localePlaceholder) !== false) // It is localized directory
  116. ) {
  117. return $patternDir;
  118. }
  119. }
  120. throw new \Exception('Unable to determine theme locale path');
  121. }
  122. /**
  123. * @param string $modularCall
  124. * @param array $usages
  125. * @param null|string $area
  126. * @dataProvider modularFallbackDataProvider
  127. */
  128. public function testModularFallback($modularCall, array $usages, $area)
  129. {
  130. list(, $file) = explode(\Magento\Framework\View\Asset\Repository::FILE_ID_SEPARATOR, $modularCall);
  131. $wrongResolutions = [];
  132. foreach (self::$_themeCollection as $theme) {
  133. if ($area && $theme->getArea() != $area) {
  134. continue;
  135. }
  136. $found = $this->_getFileResolutions($theme, $file);
  137. $wrongResolutions = array_merge($wrongResolutions, $found);
  138. }
  139. if ($wrongResolutions) {
  140. // If file is found, then old functionality (find modular files in non-modular locations) is used
  141. $message = sprintf(
  142. "Found modular call:\n %s in\n %s\n which may resolve to non-modular location(s):\n %s",
  143. $modularCall,
  144. implode(', ', $usages),
  145. implode(', ', $wrongResolutions)
  146. );
  147. $this->fail($message);
  148. }
  149. }
  150. /**
  151. * Resolves file to find its fallback'ed paths
  152. *
  153. * @param \Magento\Framework\View\Design\ThemeInterface $theme
  154. * @param string $file
  155. * @return array
  156. */
  157. protected function _getFileResolutions(\Magento\Framework\View\Design\ThemeInterface $theme, $file)
  158. {
  159. $found = [];
  160. $fileResolved = self::$_filesFallback->getFile($theme->getArea(), $theme, $file);
  161. if (file_exists($fileResolved)) {
  162. $found[$fileResolved] = $fileResolved;
  163. }
  164. foreach (self::$_checkThemeLocales[$theme->getFullPath()] as $locale) {
  165. $fileResolved = self::$_viewFilesFallback->getFile($theme->getArea(), $theme, $locale, $file);
  166. if (file_exists($fileResolved)) {
  167. $found[$fileResolved] = $fileResolved;
  168. }
  169. }
  170. return $found;
  171. }
  172. /**
  173. * @return array
  174. */
  175. public static function modularFallbackDataProvider()
  176. {
  177. $result = [];
  178. foreach (self::_getFilesToProcess() as $file) {
  179. $file = (string)$file;
  180. $modulePattern = '[A-Z][a-z]+_[A-Z][a-z]+';
  181. $filePattern = '[[:alnum:]_/-]+\\.[[:alnum:]_./-]+';
  182. $pattern = '#' . $modulePattern
  183. . preg_quote(\Magento\Framework\View\Asset\Repository::FILE_ID_SEPARATOR)
  184. . $filePattern . '#S';
  185. if (!preg_match_all($pattern, file_get_contents($file), $matches)) {
  186. continue;
  187. }
  188. $area = self::_getArea($file);
  189. foreach ($matches[0] as $modularCall) {
  190. $dataSetKey = $modularCall . ' @ ' . ($area ?: 'any area');
  191. if (!isset($result[$dataSetKey])) {
  192. $result[$dataSetKey] = ['modularCall' => $modularCall, 'usages' => [], 'area' => $area];
  193. }
  194. $result[$dataSetKey]['usages'][$file] = $file;
  195. }
  196. }
  197. return $result;
  198. }
  199. /**
  200. * Return list of files, that must be processed, searching for modular calls to view files
  201. *
  202. * @return array
  203. */
  204. protected static function _getFilesToProcess()
  205. {
  206. $result = [];
  207. $componentRegistrar = new \Magento\Framework\Component\ComponentRegistrar();
  208. $dirs = array_merge(
  209. $componentRegistrar->getPaths(ComponentRegistrar::MODULE),
  210. $componentRegistrar->getPaths(ComponentRegistrar::THEME)
  211. );
  212. foreach ($dirs as $dir) {
  213. $iterator = new \RecursiveIteratorIterator(
  214. new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
  215. );
  216. $result = array_merge($result, iterator_to_array($iterator));
  217. }
  218. return $result;
  219. }
  220. /**
  221. * Get the area, where file is located.
  222. *
  223. * Null is returned, if the file is not within an area, e.g. it is a model/block/helper php-file.
  224. *
  225. * @param string $file
  226. * @return string|null
  227. */
  228. protected static function _getArea($file)
  229. {
  230. $file = str_replace('\\', '/', $file);
  231. $areaPatterns = [];
  232. $componentRegistrar = new ComponentRegistrar();
  233. foreach ($componentRegistrar->getPaths(ComponentRegistrar::THEME) as $themeDir) {
  234. $areaPatterns[] = '#' . $themeDir . '/([^/]+)/#S';
  235. }
  236. foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleDir) {
  237. $areaPatterns[] = '#' . $moduleDir . '/view/([^/]+)/#S';
  238. }
  239. foreach ($areaPatterns as $pattern) {
  240. if (preg_match($pattern, $file, $matches)) {
  241. return $matches[1];
  242. }
  243. }
  244. return null;
  245. }
  246. }