Skip to content

Commit 119eedf

Browse files
author
Oleksii Korshenko
authored
Merge pull request #1213 from magento-engcom/develop-prs
Public Pull Requests #9996 #8965
2 parents 2afdaa2 + 4385418 commit 119eedf

File tree

7 files changed

+504
-16
lines changed

7 files changed

+504
-16
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
sudo: required
22
dist: trusty
3+
group: edge
34
addons:
45
apt:
56
packages:

setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
namespace Magento\Setup\Module\Di\Code\Reader;
77

8+
use Magento\Framework\App\Filesystem\DirectoryList;
9+
use Magento\Framework\App\ObjectManager;
810
use Magento\Framework\Exception\FileSystemException;
911

1012
class ClassesScanner implements ClassesScannerInterface
@@ -14,12 +16,27 @@ class ClassesScanner implements ClassesScannerInterface
1416
*/
1517
protected $excludePatterns = [];
1618

19+
/**
20+
* @var array
21+
*/
22+
private $fileResults = [];
23+
24+
/**
25+
* @var string
26+
*/
27+
private $generationDirectory;
28+
1729
/**
1830
* @param array $excludePatterns
31+
* @param string $generationDirectory
1932
*/
20-
public function __construct(array $excludePatterns = [])
33+
public function __construct(array $excludePatterns = [], DirectoryList $directoryList = null)
2134
{
2235
$this->excludePatterns = $excludePatterns;
36+
if ($directoryList === null) {
37+
$directoryList = ObjectManager::getInstance()->get(DirectoryList::class);
38+
}
39+
$this->generationDirectory = $directoryList->getPath(DirectoryList::GENERATION);
2340
}
2441

2542
/**
@@ -43,7 +60,14 @@ public function addExcludePatterns(array $excludePatterns)
4360
*/
4461
public function getList($path)
4562
{
63+
4664
$realPath = realpath($path);
65+
$isGeneration = strpos($realPath, $this->generationDirectory) === 0;
66+
67+
// Generation folders should not have their results cached since they may actually change during compile
68+
if (!$isGeneration && isset($this->fileResults[$realPath])) {
69+
return $this->fileResults[$realPath];
70+
}
4771
if (!(bool)$realPath) {
4872
throw new FileSystemException(new \Magento\Framework\Phrase('Invalid path: %1', [$path]));
4973
}
@@ -52,46 +76,71 @@ public function getList($path)
5276
\RecursiveIteratorIterator::SELF_FIRST
5377
);
5478

79+
$classes = $this->extract($recursiveIterator);
80+
if (!$isGeneration) {
81+
$this->fileResults[$realPath] = $classes;
82+
}
83+
return $classes;
84+
}
85+
86+
/**
87+
* Extracts all the classes from the recursive iterator
88+
*
89+
* @param \RecursiveIteratorIterator $recursiveIterator
90+
* @return array
91+
*/
92+
private function extract(\RecursiveIteratorIterator $recursiveIterator)
93+
{
5594
$classes = [];
5695
foreach ($recursiveIterator as $fileItem) {
5796
/** @var $fileItem \SplFileInfo */
5897
if ($fileItem->isDir() || $fileItem->getExtension() !== 'php' || $fileItem->getBasename()[0] == '.') {
5998
continue;
6099
}
100+
$fileItemPath = $fileItem->getRealPath();
61101
foreach ($this->excludePatterns as $excludePatterns) {
62-
if ($this->isExclude($fileItem, $excludePatterns)) {
102+
if ($this->isExclude($fileItemPath, $excludePatterns)) {
63103
continue 2;
64104
}
65105
}
66-
$fileScanner = new FileScanner($fileItem->getRealPath());
106+
$fileScanner = new FileClassScanner($fileItemPath);
67107
$classNames = $fileScanner->getClassNames();
68-
foreach ($classNames as $className) {
69-
if (empty($className)) {
70-
continue;
71-
}
72-
if (!class_exists($className)) {
73-
require_once $fileItem->getRealPath();
74-
}
75-
$classes[] = $className;
76-
}
108+
$this->includeClasses($classNames, $fileItemPath);
109+
$classes = array_merge($classes, $classNames);
77110
}
78111
return $classes;
79112
}
80113

114+
/**
115+
* @param array $classNames
116+
* @param string $fileItemPath
117+
* @return bool Whether the clas is included or not
118+
*/
119+
private function includeClasses(array $classNames, $fileItemPath)
120+
{
121+
foreach ($classNames as $className) {
122+
if (!class_exists($className)) {
123+
require_once $fileItemPath;
124+
return true;
125+
}
126+
}
127+
return false;
128+
}
129+
81130
/**
82131
* Find out if file should be excluded
83132
*
84-
* @param \SplFileInfo $fileItem
133+
* @param string $fileItemPath
85134
* @param string $patterns
86135
* @return bool
87136
*/
88-
private function isExclude(\SplFileInfo $fileItem, $patterns)
137+
private function isExclude($fileItemPath, $patterns)
89138
{
90139
if (!is_array($patterns)) {
91140
$patterns = (array)$patterns;
92141
}
93142
foreach ($patterns as $pattern) {
94-
if (preg_match($pattern, str_replace('\\', '/', $fileItem->getRealPath()))) {
143+
if (preg_match($pattern, str_replace('\\', '/', $fileItemPath))) {
95144
return true;
96145
}
97146
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Module\Di\Code\Reader;
8+
9+
class FileClassScanner
10+
{
11+
/**
12+
* The filename of the file to introspect
13+
*
14+
* @var string
15+
*/
16+
private $filename;
17+
18+
/**
19+
* The list of classes found in the file.
20+
*
21+
* @var bool
22+
*/
23+
private $classNames = false;
24+
25+
/**
26+
* @var array
27+
*/
28+
private $tokens;
29+
30+
/**
31+
* Constructor for the file class scanner. Requires the filename
32+
*
33+
* @param string $filename
34+
*/
35+
public function __construct($filename)
36+
{
37+
$filename = realpath($filename);
38+
if (!file_exists($filename) || !\is_file($filename)) {
39+
throw new InvalidFileException(
40+
sprintf(
41+
'The file "%s" does not exist or is not a file',
42+
$filename
43+
)
44+
);
45+
}
46+
$this->filename = $filename;
47+
}
48+
49+
/**
50+
* Retrieves the contents of a file. Mostly here for Mock injection
51+
*
52+
* @return string
53+
*/
54+
public function getFileContents()
55+
{
56+
return file_get_contents($this->filename);
57+
}
58+
59+
/**
60+
* Extracts the fully qualified class name from a file. It only searches for the first match and stops looking
61+
* as soon as it enters the class definition itself.
62+
*
63+
* Warnings are suppressed for this method due to a micro-optimization that only really shows up when this logic
64+
* is called several millions of times, which can happen quite easily with even moderately sized codebases.
65+
*
66+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
67+
* @SuppressWarnings(PHPMD.NPathComplexity)
68+
* @return array
69+
*/
70+
private function extract()
71+
{
72+
$allowedOpenBraces = [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES, T_STRING_VARNAME];
73+
$classes = [];
74+
$namespace = '';
75+
$class = '';
76+
$triggerClass = false;
77+
$triggerNamespace = false;
78+
$braceLevel = 0;
79+
$bracedNamespace = false;
80+
81+
$this->tokens = token_get_all($this->getFileContents());
82+
foreach ($this->tokens as $index => $token) {
83+
// Is either a literal brace or an interpolated brace with a variable
84+
if ($token == '{' || (is_array($token) && in_array($token[0], $allowedOpenBraces))) {
85+
$braceLevel++;
86+
} else if ($token == '}') {
87+
$braceLevel--;
88+
}
89+
// The namespace keyword was found in the last loop
90+
if ($triggerNamespace) {
91+
// A string ; or a discovered namespace that looks like "namespace name { }"
92+
if (!is_array($token) || ($namespace && $token[0] == T_WHITESPACE)) {
93+
$triggerNamespace = false;
94+
$namespace .= '\\';
95+
continue;
96+
}
97+
$namespace .= $token[1];
98+
99+
// The class keyword was found in the last loop
100+
} else if ($triggerClass && $token[0] == T_STRING) {
101+
$triggerClass = false;
102+
$class = $token[1];
103+
}
104+
105+
switch ($token[0]) {
106+
case T_NAMESPACE:
107+
// Current loop contains the namespace keyword. Between this and the semicolon is the namespace
108+
$triggerNamespace = true;
109+
$namespace = '';
110+
$bracedNamespace = $this->isBracedNamespace($index);
111+
break;
112+
case T_CLASS:
113+
// Current loop contains the class keyword. Next loop will have the class name itself.
114+
if ($braceLevel == 0 || ($bracedNamespace && $braceLevel == 1)) {
115+
$triggerClass = true;
116+
}
117+
break;
118+
}
119+
120+
// We have a class name, let's concatenate and store it!
121+
if ($class != '') {
122+
$namespace = trim($namespace);
123+
$fqClassName = $namespace . trim($class);
124+
$classes[] = $fqClassName;
125+
$class = '';
126+
}
127+
}
128+
return $classes;
129+
}
130+
131+
/**
132+
* Looks forward from the current index to determine if the namespace is nested in {} or terminated with ;
133+
*
134+
* @param integer $index
135+
* @return bool
136+
*/
137+
private function isBracedNamespace($index)
138+
{
139+
$len = count($this->tokens);
140+
while ($index++ < $len) {
141+
if (!is_array($this->tokens[$index])) {
142+
if ($this->tokens[$index] == ';') {
143+
return false;
144+
} else if ($this->tokens[$index] == '{') {
145+
return true;
146+
}
147+
continue;
148+
}
149+
150+
if (!in_array($this->tokens[$index][0], [T_WHITESPACE, T_STRING, T_NS_SEPARATOR])) {
151+
throw new InvalidFileException('Namespace not defined properly');
152+
}
153+
}
154+
throw new InvalidFileException('Could not find namespace termination');
155+
}
156+
157+
/**
158+
* Retrieves the first class found in a class file. The return value is in an array format so it retains the
159+
* same usage as the FileScanner.
160+
*
161+
* @return array
162+
*/
163+
public function getClassNames()
164+
{
165+
if ($this->classNames === false) {
166+
$this->classNames = $this->extract();
167+
}
168+
return $this->classNames;
169+
}
170+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Module\Di\Code\Reader;
8+
9+
class InvalidFileException extends \InvalidArgumentException
10+
{
11+
12+
}

setup/src/Magento/Setup/Test/Unit/Module/Di/Code/Reader/ClassesScannerTest.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,31 @@
55
*/
66
namespace Magento\Setup\Test\Unit\Module\Di\Code\Reader;
77

8+
use Magento\Framework\App\Filesystem\DirectoryList;
9+
810
class ClassesScannerTest extends \PHPUnit_Framework_TestCase
911
{
1012
/**
1113
* @var \Magento\Setup\Module\Di\Code\Reader\ClassesScanner
1214
*/
1315
private $model;
1416

17+
/**
18+
* the /var/generation directory realpath
19+
*
20+
* @var string
21+
*/
22+
23+
private $generation;
24+
1525
protected function setUp()
1626
{
17-
$this->model = new \Magento\Setup\Module\Di\Code\Reader\ClassesScanner();
27+
$this->generation = realpath(__DIR__ . '/../../_files/var/generation');
28+
$mock = $this->getMockBuilder(DirectoryList::class)->disableOriginalConstructor()->setMethods(
29+
['getPath']
30+
)->getMock();
31+
$mock->method('getPath')->willReturn($this->generation);
32+
$this->model = new \Magento\Setup\Module\Di\Code\Reader\ClassesScanner([], $mock);
1833
}
1934

2035
public function testGetList()

0 commit comments

Comments
 (0)