Skip to content

Instantly share code, notes, and snippets.

@joshuaadickerson
Created May 22, 2017 06:50
Show Gist options
  • Save joshuaadickerson/240f3633d99106fb7aed44a98a0ef0fd to your computer and use it in GitHub Desktop.
Save joshuaadickerson/240f3633d99106fb7aed44a98a0ef0fd to your computer and use it in GitHub Desktop.
<?php
/**
* Copyright © 2013-2017 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 = [];
protected $tokenCount = 0;
protected $allowedOpenBraces = [
T_CURLY_OPEN => true,
T_DOLLAR_OPEN_CURLY_BRACES => true,
T_STRING_VARNAME => true,
];
protected $validNamespaceSeparators = [
T_WHITESPACE => true,
T_STRING => true,
T_NS_SEPARATOR => true,
];
protected $classes = [];
protected $namespace = '';
protected $class = '';
protected $triggerClass = false;
protected $triggerNamespace = false;
protected $braceLevel = 0;
protected $bracedNamespace = false;
/**
* 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 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;
}
/**
* 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()
{
$this->tokens = token_get_all($this->getFileContents());
$this->tokenCount = count($this->tokens);
$classes = [];
foreach ($this->tokens as $index => $token) {
$classes += $this->getTokenClass($index, $token);
}
return $classes;
}
/**
* @param int $index
* @param mixed $token
*
* @return array
*/
protected function getTokenClass($index, $token)
{
$class = '';
$this->setBraceLevel($token);
// The namespace keyword was found in the last loop
if ($this->triggerNamespace) {
// A string ; or a discovered namespace that looks like "namespace name { }"
if (!is_array($token) || ($this->namespace && $token[0] === T_WHITESPACE)) {
$this->triggerNamespace = false;
$this->namespace .= '\\';
return [];
}
$this->namespace .= $token[1];
}
// The class keyword was found in the last loop
elseif ($this->triggerClass && $token[0] === T_STRING) {
$this->triggerClass = false;
$class = $token[1];
}
$this->isTokenNamespace($index, $token);
$this->isTokenClass();
// We have a class name, let's concatenate and store it!
return $class !== '' ? trim($this->namespace) . trim($class) : [];
}
/**
* @param mixed $token
*/
protected function setBraceLevel($token)
{
// Is either a literal brace or an interpolated brace with a variable
if ($token === '{' || (is_array($token) && isset($this->allowedOpenBraces[$token[0]]))) {
$this->braceLevel++;
} else if ($token === '}') {
$this->braceLevel--;
}
}
/**
* Check if the current loop contains the namespace keyword. Between this and the semicolon is the namespace
*
* @param int $index
* @param string $token
* @return bool
*/
protected function isTokenNamespace($index, $token)
{
if ($token === T_NAMESPACE) {
$this->triggerNamespace = true;
$this->namespace = '';
$this->bracedNamespace = $this->isBracedNamespace($index);
return true;
}
return false;
}
/**
* Looks forward from the current index to determine if the namespace is nested in {} or terminated with ;
*
* @param integer $index
* @return bool
*
* @throws InvalidFileException when the namespace is not properly defined or not properly terminated
*/
private function isBracedNamespace($index)
{
while ($index++ < $this->tokenCount) {
if (!is_array($this->tokens[$index])) {
if ($this->tokens[$index] === ';') {
return false;
} else if ($this->tokens[$index] === '{') {
return true;
}
continue;
}
if (!isset($this->validNamespaceSeparators[$this->tokens[$index][0]])) {
throw new InvalidFileException('Namespace not defined properly');
}
}
throw new InvalidFileException('Could not find namespace termination');
}
/**
* Check if the current loop contains the class keyword. Next loop will have the class name itself.
* @return bool
*/
protected function isTokenClass()
{
if ($this->braceLevel === 0 || ($this->bracedNamespace && $this->braceLevel === 1)) {
$this->triggerClass = true;
return true;
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment