Skip to content

Commit 364c21e

Browse files
author
Oleksii Korshenko
committed
MAGETWO-66502: Reduce calls to SplFileInfo::realpath() in the Magento\Setup\Module\Di\Code\Reader\ClassesScanner class #8965
- Merge Pull Request #8965 from kschroeder/magento2:develop - Merged commits: 1. 43c898f 2. f1e6e2d 3. acc587f 4. b127afa 5. 1f5de9d 6. 60a289b 7. 06865dc 8. 98828dc 9. 0c86dcd 10. 60be236 11. e46b0a9 12. 3a80af3 13. 4aa7ce0 14. 55a765f 15. 63927ba 16. bf87fdb 17. dd3f4b5 18. a8abd1b 19. c593ef6 20. ac82998 21. 70647be 22. 212b301 23. b8703c4 24. ea90720 25. 5fc1ea5 26. dc61477 27. b1ba446 28. 7f9d3fe 29. 5df626c 30. f2f6b0a
2 parents 2afdaa2 + 8f043c1 commit 364c21e

File tree

6 files changed

+502
-16
lines changed

6 files changed

+502
-16
lines changed

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

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)