10
10
namespace Magento \TestFramework \Dependency ;
11
11
12
12
use Magento \Framework \App \Utility \Files ;
13
+ use Magento \Framework \Config \Reader \Filesystem as ConfigReader ;
14
+ use Magento \Framework \Exception \ConfigurationMismatchException ;
13
15
use Magento \Framework \Exception \LocalizedException ;
14
16
use Magento \Framework \UrlInterface ;
15
17
use Magento \TestFramework \Dependency \Reader \ClassScanner ;
16
18
use Magento \TestFramework \Dependency \Route \RouteMapper ;
17
19
use Magento \TestFramework \Exception \NoSuchActionException ;
20
+ use Magento \TestFramework \Inspection \Exception ;
18
21
19
22
/**
20
23
* Rule to check the dependencies between modules based on references, getUrl and layout blocks
@@ -58,6 +61,12 @@ class PhpRule implements RuleInterface
58
61
*/
59
62
protected $ _mapLayoutBlocks = [];
60
63
64
+ /**
65
+ * Used to retrieve information from WebApi urls
66
+ * @var ConfigReader
67
+ */
68
+ protected $ configReader ;
69
+
61
70
/**
62
71
* Default modules list.
63
72
*
@@ -85,28 +94,36 @@ class PhpRule implements RuleInterface
85
94
*/
86
95
private $ classScanner ;
87
96
97
+ /**
98
+ * @var array
99
+ */
100
+ private $ serviceMethods ;
101
+
88
102
/**
89
103
* @param array $mapRouters
90
104
* @param array $mapLayoutBlocks
105
+ * @param ConfigReader $configReader
91
106
* @param array $pluginMap
92
107
* @param array $whitelists
93
108
* @param ClassScanner|null $classScanner
94
- *
95
- * @throws LocalizedException
109
+ * @param RouteMapper|null $routeMapper
96
110
*/
97
111
public function __construct (
98
112
array $ mapRouters ,
99
113
array $ mapLayoutBlocks ,
114
+ ConfigReader $ configReader ,
100
115
array $ pluginMap = [],
101
116
array $ whitelists = [],
102
- ClassScanner $ classScanner = null
117
+ ClassScanner $ classScanner = null ,
118
+ RouteMapper $ routeMapper = null
103
119
) {
104
120
$ this ->_mapRouters = $ mapRouters ;
105
121
$ this ->_mapLayoutBlocks = $ mapLayoutBlocks ;
122
+ $ this ->configReader = $ configReader ;
106
123
$ this ->pluginMap = $ pluginMap ?: null ;
107
- $ this ->routeMapper = new RouteMapper ();
108
124
$ this ->whitelists = $ whitelists ;
109
125
$ this ->classScanner = $ classScanner ?? new ClassScanner ();
126
+ $ this ->routeMapper = $ routeMapper ?? new RouteMapper ();
110
127
}
111
128
112
129
/**
@@ -132,7 +149,7 @@ public function getDependencyInfo($currentModule, $fileType, $file, &$contents)
132
149
);
133
150
$ dependenciesInfo = $ this ->considerCaseDependencies (
134
151
$ dependenciesInfo ,
135
- $ this ->_caseGetUrl ($ currentModule , $ contents )
152
+ $ this ->_caseGetUrl ($ currentModule , $ contents, $ file )
136
153
);
137
154
$ dependenciesInfo = $ this ->considerCaseDependencies (
138
155
$ dependenciesInfo ,
@@ -290,41 +307,29 @@ private function isPluginDependency($dependent, $dependency)
290
307
*
291
308
* @param string $currentModule
292
309
* @param string $contents
310
+ * @param string $file
293
311
* @return array
294
312
* @throws LocalizedException
295
- * @throws \Exception
296
- * @SuppressWarnings(PMD.CyclomaticComplexity)
297
313
*/
298
- protected function _caseGetUrl (string $ currentModule , string &$ contents ): array
314
+ protected function _caseGetUrl (string $ currentModule , string &$ contents, string $ file ): array
299
315
{
300
- $ pattern = '#(\->|:)(?<source>getUrl\(([ \'"])(?<route_id>[a-z0-9\-_]{3,}|\*) '
301
- .'(/(?<controller_name>[a-z0-9\-_]+|\*))?(/(?<action_name>[a-z0-9\-_]+|\*))?\3)#i ' ;
302
-
303
316
$ dependencies = [];
317
+ $ pattern = '#(\->|:)(?<source>getUrl\(([ \'"])(?<path>[a-zA-Z0-9\-_*/]+)\3)\s*[,)]# ' ;
304
318
if (!preg_match_all ($ pattern , $ contents , $ matches , PREG_SET_ORDER )) {
305
319
return $ dependencies ;
306
320
}
307
-
308
321
try {
309
322
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 );
317
331
}
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 )) {
328
333
$ dependencies [] = [
329
334
'modules ' => $ modules ,
330
335
'type ' => RuleInterface::TYPE_HARD ,
@@ -337,10 +342,136 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array
337
342
throw new LocalizedException (__ ('Invalid URL path: %1 ' , $ e ->getMessage ()), $ e );
338
343
}
339
344
}
340
-
341
345
return $ dependencies ;
342
346
}
343
347
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
+
344
475
/**
345
476
* Check layout blocks
346
477
*
0 commit comments