1010namespace Magento \TestFramework \Dependency ;
1111
1212use Magento \Framework \App \Utility \Files ;
13+ use Magento \Framework \Config \Reader \Filesystem as ConfigReader ;
14+ use Magento \Framework \Exception \ConfigurationMismatchException ;
1315use Magento \Framework \Exception \LocalizedException ;
1416use Magento \Framework \UrlInterface ;
1517use Magento \TestFramework \Dependency \Reader \ClassScanner ;
1618use Magento \TestFramework \Dependency \Route \RouteMapper ;
1719use Magento \TestFramework \Exception \NoSuchActionException ;
20+ use Magento \TestFramework \Inspection \Exception ;
1821
1922/**
2023 * Rule to check the dependencies between modules based on references, getUrl and layout blocks
@@ -58,6 +61,12 @@ class PhpRule implements RuleInterface
5861 */
5962 protected $ _mapLayoutBlocks = [];
6063
64+ /**
65+ * Used to retrieve information from WebApi urls
66+ * @var ConfigReader
67+ */
68+ protected $ configReader ;
69+
6170 /**
6271 * Default modules list.
6372 *
@@ -85,28 +94,36 @@ class PhpRule implements RuleInterface
8594 */
8695 private $ classScanner ;
8796
97+ /**
98+ * @var array
99+ */
100+ private $ serviceMethods ;
101+
88102 /**
89103 * @param array $mapRouters
90104 * @param array $mapLayoutBlocks
105+ * @param ConfigReader $configReader
91106 * @param array $pluginMap
92107 * @param array $whitelists
93108 * @param ClassScanner|null $classScanner
94- *
95- * @throws LocalizedException
109+ * @param RouteMapper|null $routeMapper
96110 */
97111 public function __construct (
98112 array $ mapRouters ,
99113 array $ mapLayoutBlocks ,
114+ ConfigReader $ configReader ,
100115 array $ pluginMap = [],
101116 array $ whitelists = [],
102- ClassScanner $ classScanner = null
117+ ClassScanner $ classScanner = null ,
118+ RouteMapper $ routeMapper = null
103119 ) {
104120 $ this ->_mapRouters = $ mapRouters ;
105121 $ this ->_mapLayoutBlocks = $ mapLayoutBlocks ;
122+ $ this ->configReader = $ configReader ;
106123 $ this ->pluginMap = $ pluginMap ?: null ;
107- $ this ->routeMapper = new RouteMapper ();
108124 $ this ->whitelists = $ whitelists ;
109125 $ this ->classScanner = $ classScanner ?? new ClassScanner ();
126+ $ this ->routeMapper = $ routeMapper ?? new RouteMapper ();
110127 }
111128
112129 /**
@@ -132,7 +149,7 @@ public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
132149 );
133150 $ dependenciesInfo = $ this ->considerCaseDependencies (
134151 $ dependenciesInfo ,
135- $ this ->_caseGetUrl ($ currentModule , $ contents )
152+ $ this ->_caseGetUrl ($ currentModule , $ contents, $ file )
136153 );
137154 $ dependenciesInfo = $ this ->considerCaseDependencies (
138155 $ dependenciesInfo ,
@@ -290,41 +307,29 @@ private function isPluginDependency($dependent, $dependency)
290307 *
291308 * @param string $currentModule
292309 * @param string $contents
310+ * @param string $file
293311 * @return array
294312 * @throws LocalizedException
295- * @throws \Exception
296- * @SuppressWarnings(PMD.CyclomaticComplexity)
297313 */
298- protected function _caseGetUrl (string $ currentModule , string &$ contents ): array
314+ protected function _caseGetUrl (string $ currentModule , string &$ contents, string $ file ): array
299315 {
300- $ pattern = '#(\->|:)(?<source>getUrl\(([ \'"])(?<route_id>[a-z0-9\-_]{3,}|\*) '
301- .'(/(?<controller_name>[a-z0-9\-_]+|\*))?(/(?<action_name>[a-z0-9\-_]+|\*))?\3)#i ' ;
302-
303316 $ dependencies = [];
317+ $ pattern = '#(\->|:)(?<source>getUrl\(([ \'"])(?<path>[a-zA-Z0-9\-_*/]+)\3)\s*[,)]# ' ;
304318 if (!preg_match_all ($ pattern , $ contents , $ matches , PREG_SET_ORDER )) {
305319 return $ dependencies ;
306320 }
307-
308321 try {
309322 foreach ($ matches as $ item ) {
310- $ routeId = $ item ['route_id ' ];
311- $ controllerName = $ item ['controller_name ' ] ?? UrlInterface::DEFAULT_CONTROLLER_NAME ;
312- $ actionName = $ item ['action_name ' ] ?? UrlInterface::DEFAULT_ACTION_NAME ;
313-
314- // skip rest
315- if ($ routeId === "rest " ) { //MC-19890
316- continue ;
323+ $ path = $ item ['path ' ];
324+ $ modules = [];
325+ if (strpos ($ path , '* ' ) !== false ) {
326+ $ modules = $ this ->processWildcardUrl ($ path , $ file );
327+ } elseif (preg_match ('#rest(?<service>/V1/.+)#i ' , $ path , $ apiMatch )) {
328+ $ modules = $ this ->processApiUrl ($ apiMatch ['service ' ]);
329+ } else {
330+ $ modules = $ this ->processStandardUrl ($ path );
317331 }
318- // skip wildcards
319- if ($ routeId === "* " || $ controllerName === "* " || $ actionName === "* " ) { //MC-19890
320- continue ;
321- }
322- $ modules = $ this ->routeMapper ->getDependencyByRoutePath (
323- $ routeId ,
324- $ controllerName ,
325- $ actionName
326- );
327- if (!in_array ($ currentModule , $ modules )) {
332+ if ($ modules && !in_array ($ currentModule , $ modules )) {
328333 $ dependencies [] = [
329334 'modules ' => $ modules ,
330335 'type ' => RuleInterface::TYPE_HARD ,
@@ -337,10 +342,136 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array
337342 throw new LocalizedException (__ ('Invalid URL path: %1 ' , $ e ->getMessage ()), $ e );
338343 }
339344 }
340-
341345 return $ dependencies ;
342346 }
343347
348+ /**
349+ * Helper method to get module dependencies used by a wildcard Url
350+ *
351+ * @param string $urlPath
352+ * @param string $filePath
353+ * @return string[]
354+ * @throws NoSuchActionException
355+ */
356+ private function processWildcardUrl (string $ urlPath , string $ filePath )
357+ {
358+ $ filePath = strtolower ($ filePath );
359+ $ urlRoutePieces = explode ('/ ' , $ urlPath );
360+ $ routeId = array_shift ($ urlRoutePieces );
361+ //Skip route wildcard processing as this requires using the routeMapper
362+ if ('* ' === $ routeId ) {
363+ return [];
364+ }
365+
366+ /**
367+ * Only handle Controllers. ie: Ignore Blocks, Templates, and Models due to complexity in static resolution
368+ * of route
369+ */
370+ if (!preg_match (
371+ '#controller/(adminhtml/)?(?<controller_name>.+)/(?<action_name>\w+).php$# ' ,
372+ $ filePath ,
373+ $ fileParts
374+ )) {
375+ return [];
376+ }
377+
378+ $ controllerName = array_shift ($ urlRoutePieces );
379+ if ('* ' === $ controllerName ) {
380+ $ controllerName = str_replace ('/ ' , '_ ' , $ fileParts ['controller_name ' ]);
381+ }
382+
383+ if (empty ($ urlRoutePieces ) || !$ urlRoutePieces [0 ]) {
384+ $ actionName = UrlInterface::DEFAULT_ACTION_NAME ;
385+ } else {
386+ $ actionName = array_shift ($ urlRoutePieces );
387+ if ('* ' === $ actionName ) {
388+ $ actionName = $ fileParts ['action_name ' ];
389+ }
390+ }
391+
392+ return $ this ->routeMapper ->getDependencyByRoutePath (
393+ strtolower ($ routeId ),
394+ strtolower ($ controllerName ),
395+ strtolower ($ actionName )
396+ );
397+ }
398+
399+ /**
400+ * Helper method to get module dependencies used by a standard URL
401+ *
402+ * @param string $path
403+ * @return string[]
404+ * @throws NoSuchActionException
405+ */
406+ private function processStandardUrl (string $ path )
407+ {
408+ $ pattern = '#(?<route_id>[a-z0-9\-_]{3,}) '
409+ . '(/(?<controller_name>[a-z0-9\-_]+))?(/(?<action_name>[a-z0-9\-_]+))?#i ' ;
410+ if (!preg_match ($ pattern , $ path , $ match )) {
411+ throw new NoSuchActionException ('Failed to parse standard url path: ' . $ path );
412+ }
413+ $ routeId = $ match ['route_id ' ];
414+ $ controllerName = $ match ['controller_name ' ] ?? UrlInterface::DEFAULT_CONTROLLER_NAME ;
415+ $ actionName = $ match ['action_name ' ] ?? UrlInterface::DEFAULT_ACTION_NAME ;
416+
417+ return $ this ->routeMapper ->getDependencyByRoutePath (
418+ $ routeId ,
419+ $ controllerName ,
420+ $ actionName
421+ );
422+ }
423+
424+ /**
425+ * Create regex patterns from service url paths
426+ *
427+ * @return array
428+ */
429+ private function getServiceMethodRegexps (): array
430+ {
431+ if (!$ this ->serviceMethods ) {
432+ $ this ->serviceMethods = [];
433+ $ serviceRoutes = $ this ->configReader ->read ()['routes ' ];
434+ foreach ($ serviceRoutes as $ serviceRouteUrl => $ methods ) {
435+ $ pattern = '#:\w+# ' ;
436+ $ replace = '\w+ ' ;
437+ $ serviceRouteUrlRegex = preg_replace ($ pattern , $ replace , $ serviceRouteUrl );
438+ $ serviceRouteUrlRegex = '#^ ' . $ serviceRouteUrlRegex . '$# ' ;
439+ $ this ->serviceMethods [$ serviceRouteUrlRegex ] = $ methods ;
440+ }
441+ }
442+ return $ this ->serviceMethods ;
443+ }
444+
445+ /**
446+ * Helper method to get module dependencies used by an API URL
447+ *
448+ * @param string $path
449+ * @return string[]
450+ *
451+ * @throws NoSuchActionException
452+ * @throws Exception
453+ */
454+ private function processApiUrl (string $ path ): array
455+ {
456+ foreach ($ this ->getServiceMethodRegexps () as $ serviceRouteUrlRegex => $ methods ) {
457+ /**
458+ * Since we expect that every service method should be within the same module, we can use the class from
459+ * any method
460+ */
461+ if (preg_match ($ serviceRouteUrlRegex , $ path )) {
462+ $ method = reset ($ methods );
463+
464+ $ className = $ method ['service ' ]['class ' ];
465+ //get module from className
466+ if (preg_match ('#^(?<module>\w+[ \\\]\w+)# ' , $ className , $ match )) {
467+ return [$ match ['module ' ]];
468+ }
469+ throw new Exception ('Failed to parse class from className: ' . $ className );
470+ }
471+ }
472+ throw new NoSuchActionException ('Failed to match service with url path: ' . $ path );
473+ }
474+
344475 /**
345476 * Check layout blocks
346477 *
0 commit comments