Skip to content

Commit

Permalink
Merge pull request #1213 from magento-engcom/develop-prs
Browse files Browse the repository at this point in the history
Public Pull Requests

#9996
#8965
  • Loading branch information
Oleksii Korshenko authored Jun 20, 2017
2 parents 2afdaa2 + 4385418 commit 119eedf
Show file tree
Hide file tree
Showing 7 changed files with 504 additions and 16 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
sudo: required
dist: trusty
group: edge
addons:
apt:
packages:
Expand Down
79 changes: 64 additions & 15 deletions setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/
namespace Magento\Setup\Module\Di\Code\Reader;

use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\FileSystemException;

class ClassesScanner implements ClassesScannerInterface
Expand All @@ -14,12 +16,27 @@ class ClassesScanner implements ClassesScannerInterface
*/
protected $excludePatterns = [];

/**
* @var array
*/
private $fileResults = [];

/**
* @var string
*/
private $generationDirectory;

/**
* @param array $excludePatterns
* @param string $generationDirectory
*/
public function __construct(array $excludePatterns = [])
public function __construct(array $excludePatterns = [], DirectoryList $directoryList = null)
{
$this->excludePatterns = $excludePatterns;
if ($directoryList === null) {
$directoryList = ObjectManager::getInstance()->get(DirectoryList::class);
}
$this->generationDirectory = $directoryList->getPath(DirectoryList::GENERATION);
}

/**
Expand All @@ -43,7 +60,14 @@ public function addExcludePatterns(array $excludePatterns)
*/
public function getList($path)
{

$realPath = realpath($path);
$isGeneration = strpos($realPath, $this->generationDirectory) === 0;

// Generation folders should not have their results cached since they may actually change during compile
if (!$isGeneration && isset($this->fileResults[$realPath])) {
return $this->fileResults[$realPath];
}
if (!(bool)$realPath) {
throw new FileSystemException(new \Magento\Framework\Phrase('Invalid path: %1', [$path]));
}
Expand All @@ -52,46 +76,71 @@ public function getList($path)
\RecursiveIteratorIterator::SELF_FIRST
);

$classes = $this->extract($recursiveIterator);
if (!$isGeneration) {
$this->fileResults[$realPath] = $classes;
}
return $classes;
}

/**
* Extracts all the classes from the recursive iterator
*
* @param \RecursiveIteratorIterator $recursiveIterator
* @return array
*/
private function extract(\RecursiveIteratorIterator $recursiveIterator)
{
$classes = [];
foreach ($recursiveIterator as $fileItem) {
/** @var $fileItem \SplFileInfo */
if ($fileItem->isDir() || $fileItem->getExtension() !== 'php' || $fileItem->getBasename()[0] == '.') {
continue;
}
$fileItemPath = $fileItem->getRealPath();
foreach ($this->excludePatterns as $excludePatterns) {
if ($this->isExclude($fileItem, $excludePatterns)) {
if ($this->isExclude($fileItemPath, $excludePatterns)) {
continue 2;
}
}
$fileScanner = new FileScanner($fileItem->getRealPath());
$fileScanner = new FileClassScanner($fileItemPath);
$classNames = $fileScanner->getClassNames();
foreach ($classNames as $className) {
if (empty($className)) {
continue;
}
if (!class_exists($className)) {
require_once $fileItem->getRealPath();
}
$classes[] = $className;
}
$this->includeClasses($classNames, $fileItemPath);
$classes = array_merge($classes, $classNames);
}
return $classes;
}

/**
* @param array $classNames
* @param string $fileItemPath
* @return bool Whether the clas is included or not
*/
private function includeClasses(array $classNames, $fileItemPath)
{
foreach ($classNames as $className) {
if (!class_exists($className)) {
require_once $fileItemPath;
return true;
}
}
return false;
}

/**
* Find out if file should be excluded
*
* @param \SplFileInfo $fileItem
* @param string $fileItemPath
* @param string $patterns
* @return bool
*/
private function isExclude(\SplFileInfo $fileItem, $patterns)
private function isExclude($fileItemPath, $patterns)
{
if (!is_array($patterns)) {
$patterns = (array)$patterns;
}
foreach ($patterns as $pattern) {
if (preg_match($pattern, str_replace('\\', '/', $fileItem->getRealPath()))) {
if (preg_match($pattern, str_replace('\\', '/', $fileItemPath))) {
return true;
}
}
Expand Down
170 changes: 170 additions & 0 deletions setup/src/Magento/Setup/Module/Di/Code/Reader/FileClassScanner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/

namespace Magento\Setup\Module\Di\Code\Reader;

class FileClassScanner
{
/**
* The filename of the file to introspect
*
* @var string
*/
private $filename;

/**
* The list of classes found in the file.
*
* @var bool
*/
private $classNames = false;

/**
* @var array
*/
private $tokens;

/**
* Constructor for the file class scanner. Requires the filename
*
* @param string $filename
*/
public function __construct($filename)
{
$filename = realpath($filename);
if (!file_exists($filename) || !\is_file($filename)) {
throw new InvalidFileException(
sprintf(
'The file "%s" does not exist or is not a file',
$filename
)
);
}
$this->filename = $filename;
}

/**
* Retrieves the contents of a file. Mostly here for Mock injection
*
* @return string
*/
public function getFileContents()
{
return file_get_contents($this->filename);
}

/**
* Extracts the fully qualified class name from a file. It only searches for the first match and stops looking
* as soon as it enters the class definition itself.
*
* Warnings are suppressed for this method due to a micro-optimization that only really shows up when this logic
* is called several millions of times, which can happen quite easily with even moderately sized codebases.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @return array
*/
private function extract()
{
$allowedOpenBraces = [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES, T_STRING_VARNAME];
$classes = [];
$namespace = '';
$class = '';
$triggerClass = false;
$triggerNamespace = false;
$braceLevel = 0;
$bracedNamespace = false;

$this->tokens = token_get_all($this->getFileContents());
foreach ($this->tokens as $index => $token) {
// Is either a literal brace or an interpolated brace with a variable
if ($token == '{' || (is_array($token) && in_array($token[0], $allowedOpenBraces))) {
$braceLevel++;
} else if ($token == '}') {
$braceLevel--;
}
// The namespace keyword was found in the last loop
if ($triggerNamespace) {
// A string ; or a discovered namespace that looks like "namespace name { }"
if (!is_array($token) || ($namespace && $token[0] == T_WHITESPACE)) {
$triggerNamespace = false;
$namespace .= '\\';
continue;
}
$namespace .= $token[1];

// The class keyword was found in the last loop
} else if ($triggerClass && $token[0] == T_STRING) {
$triggerClass = false;
$class = $token[1];
}

switch ($token[0]) {
case T_NAMESPACE:
// Current loop contains the namespace keyword. Between this and the semicolon is the namespace
$triggerNamespace = true;
$namespace = '';
$bracedNamespace = $this->isBracedNamespace($index);
break;
case T_CLASS:
// Current loop contains the class keyword. Next loop will have the class name itself.
if ($braceLevel == 0 || ($bracedNamespace && $braceLevel == 1)) {
$triggerClass = true;
}
break;
}

// We have a class name, let's concatenate and store it!
if ($class != '') {
$namespace = trim($namespace);
$fqClassName = $namespace . trim($class);
$classes[] = $fqClassName;
$class = '';
}
}
return $classes;
}

/**
* Looks forward from the current index to determine if the namespace is nested in {} or terminated with ;
*
* @param integer $index
* @return bool
*/
private function isBracedNamespace($index)
{
$len = count($this->tokens);
while ($index++ < $len) {
if (!is_array($this->tokens[$index])) {
if ($this->tokens[$index] == ';') {
return false;
} else if ($this->tokens[$index] == '{') {
return true;
}
continue;
}

if (!in_array($this->tokens[$index][0], [T_WHITESPACE, T_STRING, T_NS_SEPARATOR])) {
throw new InvalidFileException('Namespace not defined properly');
}
}
throw new InvalidFileException('Could not find namespace termination');
}

/**
* Retrieves the first class found in a class file. The return value is in an array format so it retains the
* same usage as the FileScanner.
*
* @return array
*/
public function getClassNames()
{
if ($this->classNames === false) {
$this->classNames = $this->extract();
}
return $this->classNames;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/

namespace Magento\Setup\Module\Di\Code\Reader;

class InvalidFileException extends \InvalidArgumentException
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,31 @@
*/
namespace Magento\Setup\Test\Unit\Module\Di\Code\Reader;

use Magento\Framework\App\Filesystem\DirectoryList;

class ClassesScannerTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \Magento\Setup\Module\Di\Code\Reader\ClassesScanner
*/
private $model;

/**
* the /var/generation directory realpath
*
* @var string
*/

private $generation;

protected function setUp()
{
$this->model = new \Magento\Setup\Module\Di\Code\Reader\ClassesScanner();
$this->generation = realpath(__DIR__ . '/../../_files/var/generation');
$mock = $this->getMockBuilder(DirectoryList::class)->disableOriginalConstructor()->setMethods(
['getPath']
)->getMock();
$mock->method('getPath')->willReturn($this->generation);
$this->model = new \Magento\Setup\Module\Di\Code\Reader\ClassesScanner([], $mock);
}

public function testGetList()
Expand Down
Loading

0 comments on commit 119eedf

Please sign in to comment.