This commit is contained in:
Your Name
2021-07-26 19:46:18 +02:00
parent e7a49138bb
commit aae17f10a6
818 changed files with 70695 additions and 16408 deletions
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace HaydenPierce\ClassFinder;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class AppConfig
{
/** @var string */
private $appRoot;
public function __construct()
{
$this->appRoot = $this->findAppRoot();
}
/**
* @return string
*/
private function findAppRoot()
{
if ($this->appRoot) {
$appRoot = $this->appRoot;
} else {
$workingDirectory = str_replace('\\', '/', __DIR__);
$workingDirectory = str_replace('/vendor/haydenpierce/class-finder/src', '', $workingDirectory);
$directoryPathPieces = explode('/', $workingDirectory);
$appRoot = null;
do {
$path = implode('/', $directoryPathPieces) . '/composer.json';
if (file_exists($path)) {
$appRoot = implode('/', $directoryPathPieces) . '/';
} else {
array_pop($directoryPathPieces);
}
} while (is_null($appRoot) && count($directoryPathPieces) > 0);
}
$this->throwIfInvalidAppRoot($appRoot);
$this->appRoot= $appRoot;
return $this->appRoot;
}
/**
* @param string $appRoot
* @return void
* @throws ClassFinderException
*/
private function throwIfInvalidAppRoot($appRoot)
{
if (!file_exists($appRoot . '/composer.json')) {
throw new ClassFinderException(sprintf("Could not locate composer.json. You can get around this by setting ClassFinder::\$appRoot manually. See '%s' for details.",
'https://gitlab.com/hpierce1102/ClassFinder/blob/master/docs/exceptions/missingComposerConfig.md'
));
}
}
/**
* @return string
*/
public function getAppRoot()
{
if ($this->appRoot === null) {
$this->appRoot = $this->findAppRoot();
}
$this->throwIfInvalidAppRoot($this->appRoot);
return $this->appRoot;
}
/**
* @param string $appRoot
* @return void
*/
public function setAppRoot($appRoot)
{
$this->appRoot = $appRoot;
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace HaydenPierce\ClassFinder;
use HaydenPierce\ClassFinder\Classmap\ClassmapEntryFactory;
use HaydenPierce\ClassFinder\Classmap\ClassmapFinder;
use HaydenPierce\ClassFinder\Files\FilesEntryFactory;
use HaydenPierce\ClassFinder\Files\FilesFinder;
use HaydenPierce\ClassFinder\PSR4\PSR4Finder;
use HaydenPierce\ClassFinder\PSR4\PSR4NamespaceFactory;
class ClassFinder
{
const STANDARD_MODE = 1;
const RECURSIVE_MODE = 2;
/** @var AppConfig */
private static $config;
/** @var PSR4Finder */
private static $psr4;
/** @var ClassmapFinder */
private static $classmap;
/** @var FilesFinder */
private static $files;
/** @var boolean */
private static $useFilesSupport = false;
/** @var boolean */
private static $usePSR4Support = true;
/** @var boolean */
private static $useClassmapSupport = true;
/**
* @return void
*/
private static function initialize()
{
if (!(self::$config instanceof AppConfig)) {
self::$config = new AppConfig();
}
if (!(self::$psr4 instanceof PSR4Finder)) {
$PSR4Factory = new PSR4NamespaceFactory(self::$config);
self::$psr4 = new PSR4Finder($PSR4Factory);
}
if (!(self::$classmap instanceof ClassmapFinder)) {
$classmapFactory = new ClassmapEntryFactory(self::$config);
self::$classmap = new ClassmapFinder($classmapFactory);
}
if (!(self::$files instanceof FilesFinder) && self::$useFilesSupport) {
$filesFactory = new FilesEntryFactory(self::$config);
self::$files = new FilesFinder($filesFactory);
}
}
/**
* Identify classes in a given namespace.
*
* @param string $namespace
* @param int $options
* @return string[]
*
* @throws \Exception
*/
public static function getClassesInNamespace($namespace, $options = self::STANDARD_MODE)
{
self::initialize();
$findersWithNamespace = self::findersWithNamespace($namespace);
$classes = array_reduce($findersWithNamespace, function($carry, FinderInterface $finder) use ($namespace, $options){
return array_merge($carry, $finder->findClasses($namespace, $options));
}, array());
return array_unique($classes);
}
/**
* Check if a given namespace contains any classes.
*
* @param string $namespace
* @return bool
*/
public static function namespaceHasClasses($namespace)
{
self::initialize();
return count(self::findersWithNamespace($namespace)) > 0;
}
/**
* @param string $appRoot
* @return void
*/
public static function setAppRoot($appRoot)
{
self::initialize();
self::$config->setAppRoot($appRoot);
}
/**
* @return void
*/
public static function enableExperimentalFilesSupport()
{
self::$useFilesSupport = true;
}
/**
* @return void
*/
public static function disableExperimentalFilesSupport()
{
self::$useFilesSupport = false;
}
/**
* @return void
*/
public static function enablePSR4Support()
{
self::$usePSR4Support = true;
}
/**
* @return void
*/
public static function disablePSR4Support()
{
self::$usePSR4Support = false;
}
/**
* @return void
*/
public static function enableClassmapSupport()
{
self::$useClassmapSupport = true;
}
/**
* @return void
*/
public static function disableClassmapSupport()
{
self::$useClassmapSupport = false;
}
/**
* @return FinderInterface[]
*/
private static function getSupportedFinders()
{
$supportedFinders = array();
/*
* This is done for testing. For some tests, allowing PSR4 classes contaminates the test results. This could also be
* disabled for performance reasons (less finders in use means less work), but most people probably won't do that.
*/
if (self::$usePSR4Support) {
$supportedFinders[] = self::$psr4;
}
/*
* This is done for testing. For some tests, allowing classmap classes contaminates the test results. This could also be
* disabled for performance reasons (less finders in use means less work), but most people probably won't do that.
*/
if (self::$useClassmapSupport) {
$supportedFinders[] = self::$classmap;
}
/*
* Files support is tucked away behind a flag because it will need to use some kind of shell access via exec, or
* system.
*
* #1 Many environments (such as shared space hosts) may not allow these functions, and attempting to call
* these functions will blow up.
* #2 I've heard of performance issues with calling these functions.
* #3 Files support probably doesn't benefit most projects.
* #4 Using exec() or system() is against many PHP developers' religions.
*/
if (self::$useFilesSupport) {
$supportedFinders[] = self::$files;
}
return $supportedFinders;
}
/**
* @param string $namespace
* @return FinderInterface[]
*/
private static function findersWithNamespace($namespace)
{
$findersWithNamespace = array_filter(self::getSupportedFinders(), function (FinderInterface $finder) use ($namespace) {
return $finder->isNamespaceKnown($namespace);
});
return $findersWithNamespace;
}
}
@@ -0,0 +1,77 @@
<?php
namespace HaydenPierce\ClassFinder\Classmap;
use HaydenPierce\ClassFinder\ClassFinder;
class ClassmapEntry
{
/** @var string */
private $className;
/**
* @param string $fullyQualifiedClassName
*/
public function __construct($fullyQualifiedClassName)
{
$this->className = $fullyQualifiedClassName;
}
/**
* @param string $namespace
* @return bool
*/
public function knowsNamespace($namespace)
{
return strpos($this->className, $namespace) !== false;
}
/**
* @param string $namespace
* @return bool
*/
public function matches($namespace, $options)
{
if ($options === ClassFinder::RECURSIVE_MODE) {
return $this->doesMatchAnyNamespace($namespace);
} else {
return $this->doesMatchDirectNamespace($namespace);
}
}
/**
* @return string
*/
public function getClassName()
{
return $this->className;
}
/**
* Checks if the class is a child or subchild of the given namespace.
*
* @param $namespace
* @return bool
*/
private function doesMatchAnyNamespace($namespace)
{
return strpos($this->getClassName(),$namespace) === 0;
}
/**
* Checks if the class is a DIRECT child of the given namespace.
*
* @param string $namespace
* @return bool
*/
private function doesMatchDirectNamespace($namespace)
{
$classNameFragments = explode('\\', $this->getClassName());
array_pop($classNameFragments);
$classNamespace = implode('\\', $classNameFragments);
$namespace = trim($namespace, '\\');
return $namespace === $classNamespace;
}
}
@@ -0,0 +1,36 @@
<?php
namespace HaydenPierce\ClassFinder\Classmap;
use HaydenPierce\ClassFinder\AppConfig;
class ClassmapEntryFactory
{
/** @var AppConfig */
private $appConfig;
public function __construct(AppConfig $appConfig)
{
$this->appConfig = $appConfig;
}
/**
* @return ClassmapEntry[]
*/
public function getClassmapEntries()
{
// Composer will compile user declared mappings to autoload_classmap.php. So no additional work is needed
// to fetch user provided entries.
$classmap = require($this->appConfig->getAppRoot() . 'vendor/composer/autoload_classmap.php');
// if classmap has no entries return empty array
if(count($classmap) == 0) {
return array();
}
$classmapKeys = array_keys($classmap);
return array_map(function($index) use ($classmapKeys){
return new ClassmapEntry($classmapKeys[$index]);
}, range(0, count($classmap) - 1));
}
}
@@ -0,0 +1,51 @@
<?php
namespace HaydenPierce\ClassFinder\Classmap;
use HaydenPierce\ClassFinder\FinderInterface;
class ClassmapFinder implements FinderInterface
{
/** @var ClassmapEntryFactory */
private $factory;
public function __construct(ClassmapEntryFactory $factory)
{
$this->factory = $factory;
}
/**
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace)
{
$classmapEntries = $this->factory->getClassmapEntries();
foreach($classmapEntries as $classmapEntry) {
if ($classmapEntry->knowsNamespace($namespace)) {
return true;
}
}
return false;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options)
{
$classmapEntries = $this->factory->getClassmapEntries();
$matchingEntries = array_filter($classmapEntries, function(ClassmapEntry $entry) use ($namespace, $options) {
return $entry->matches($namespace, $options);
});
return array_map(function(ClassmapEntry $entry) {
return $entry->getClassName();
}, $matchingEntries);
}
}
@@ -0,0 +1,8 @@
<?php
namespace HaydenPierce\ClassFinder\Exception;
class ClassFinderException extends \Exception
{
}
@@ -0,0 +1,101 @@
<?php
namespace HaydenPierce\ClassFinder\Files;
class FilesEntry
{
/** @var string */
private $file;
/** @var string */
private $php;
/**
* @param string $fileToInclude
* @param string $php
*/
public function __construct($fileToInclude, $php)
{
$this->file = $this->normalizePath($fileToInclude);
$this->php = $php;
}
/**
* @param string $namespace
* @return bool
*/
public function knowsNamespace($namespace)
{
$classes = $this->getClassesInFile();
foreach($classes as $class) {
if (strpos($class, $namespace) !== false) {
return true;
};
}
return false;
}
/**
* Gets a list of classes that belong to the given namespace.
*
* @param string $namespace
* @return string[]
*/
public function getClasses($namespace)
{
$classes = $this->getClassesInFile();
return array_values(array_filter($classes, function($class) use ($namespace) {
$classNameFragments = explode('\\', $class);
array_pop($classNameFragments);
$classNamespace = implode('\\', $classNameFragments);
$namespace = trim($namespace, '\\');
return $namespace === $classNamespace;
}));
}
/**
* Dynamically execute files and check for defined classes.
*
* This is where the real magic happens. Since classes in a randomly included file could contain classes in any namespace,
* (or even multiple namespaces!) we must execute the file and check for newly defined classes. This has a potential
* downside that files being executed will execute their side effects - which may be undesirable. However, Composer
* will require these files anyway - so hopefully causing those side effects isn't that big of a deal.
*
* @return array
*/
private function getClassesInFile()
{
// get_declared_classes() returns a bunch of classes that are built into PHP. So we need a control here.
$script = "var_export(get_declared_classes());";
exec($this->php . " -r \"$script\"", $output);
$classes = 'return ' . implode('', $output) . ';';
$initialClasses = eval($classes);
// clear the exec() buffer.
unset($output);
// This brings in the new classes. so $classes here will include the PHP defaults and the newly defined classes
$script = "require_once '{$this->file}'; var_export(get_declared_classes());";
exec($this->php . ' -r "' . $script . '"', $output);
$classes = 'return ' . implode('', $output) . ';';
$allClasses = eval($classes);
return array_diff($allClasses, $initialClasses);
}
/**
* TODO: Similar to PSR4Namespace::normalizePath. Maybe we refactor?
* @param string $path
* @return string
*/
private function normalizePath($path)
{
$path = str_replace('\\', '/', $path);
return $path;
}
}
@@ -0,0 +1,66 @@
<?php
namespace HaydenPierce\ClassFinder\Files;
use HaydenPierce\ClassFinder\AppConfig;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class FilesEntryFactory
{
/** @var AppConfig */
private $appConfig;
public function __construct(AppConfig $appConfig)
{
$this->appConfig = $appConfig;
}
/**
* @return FilesEntry[]
*/
public function getFilesEntries()
{
$files = require($this->appConfig->getAppRoot() . 'vendor/composer/autoload_files.php');
$phpPath = $this->findPHP();
$filesKeys = array_values($files);
return array_map(function($index) use ($filesKeys, $phpPath){
return new FilesEntry($filesKeys[$index], $phpPath);
}, range(0, count($files) - 1));
}
/**
* Locates the PHP interrupter.
*
* If PHP 5.4 or newer is used, the PHP_BINARY value is used.
* Otherwise we attempt to find it from shell commands.
*
* @return string
* @throws ClassFinderException
*/
private function findPHP()
{
if (defined("PHP_BINARY")) {
// PHP_BINARY was made available in PHP 5.4
$php = PHP_BINARY;
} else {
$isHostWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
if ($isHostWindows) {
exec('where php', $output);
$php = $output[0];
} else {
exec('which php', $output);
$php = $output[0];
}
}
if (!isset($php)) {
throw new ClassFinderException(sprintf(
'Could not locate PHP interrupter. See "%s" for details.',
'https://gitlab.com/hpierce1102/ClassFinder/blob/master/docs/exceptions/filesCouldNotLocatePHP.md'
));
}
return $php;
}
}
@@ -0,0 +1,59 @@
<?php
namespace HaydenPierce\ClassFinder\Files;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
use HaydenPierce\ClassFinder\FinderInterface;
class FilesFinder implements FinderInterface
{
/** @var FilesEntryFactory */
private $factory;
/**
* @param FilesEntryFactory $factory
* @throws ClassFinderException
*/
public function __construct(FilesEntryFactory $factory)
{
$this->factory = $factory;
if (!function_exists('exec')) {
throw new ClassFinderException(sprintf(
'FilesFinder requires that exec() is available. Check your php.ini to see if it is disabled. See "%s" for details.',
'https://gitlab.com/hpierce1102/ClassFinder/blob/master/docs/exceptions/filesExecNotAvailable.md'
));
}
}
/**
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace)
{
$filesEntries = $this->factory->getFilesEntries();
foreach($filesEntries as $filesEntry) {
if ($filesEntry->knowsNamespace($namespace)) {
return true;
}
}
return false;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options)
{
$filesEntries = $this->factory->getFilesEntries();
return array_reduce($filesEntries, function($carry, FilesEntry $entry) use ($namespace){
return array_merge($carry, $entry->getClasses($namespace));
}, array());
}
}
@@ -0,0 +1,29 @@
<?php
namespace HaydenPierce\ClassFinder;
interface FinderInterface
{
/**
* Find classes in a given namespace.
*
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options);
/**
* Check if a given namespace is known.
*
* A namespace is "known" if a Finder can determine that the autoloader can create classes from that namespace.
*
* For instance:
* If given a classmap for "TestApp1\Foo\Bar\Baz", the namespace "TestApp1\Foo" is known, even if nothing loads
* from that namespace directly. It is known because classes that include that namespace are known.
*
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace);
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace HaydenPierce\ClassFinder\PSR4;
use HaydenPierce\ClassFinder\ClassFinder;
use HaydenPierce\ClassFinder\FinderInterface;
class PSR4Finder implements FinderInterface
{
/** @var PSR4NamespaceFactory */
private $factory;
public function __construct(PSR4NamespaceFactory $factory)
{
$this->factory = $factory;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options)
{
if ($options === ClassFinder::RECURSIVE_MODE) {
$applicableNamespaces = $this->findAllApplicableNamespaces($namespace);
}
if (empty($applicableNamespaces)) {
$bestNamespace = $this->findBestPSR4Namespace($namespace);
$applicableNamespaces = array($bestNamespace);
}
return array_reduce($applicableNamespaces, function($carry, $psr4NamespaceOrNull) use ($namespace, $options) {
if ($psr4NamespaceOrNull instanceof PSR4Namespace) {
$classes = $psr4NamespaceOrNull->findClasses($namespace, $options);
} else {
$classes = array();
}
return array_merge($carry, $classes);
}, array());
}
/**
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace)
{
$composerNamespaces = $this->factory->getPSR4Namespaces();
foreach($composerNamespaces as $psr4Namespace) {
if ($psr4Namespace->knowsNamespace($namespace)) {
return true;
}
}
return false;
}
/**
* @param string $namespace
* @return PSR4Namespace[]
*/
private function findAllApplicableNamespaces($namespace)
{
$composerNamespaces = $this->factory->getPSR4Namespaces();
return array_filter($composerNamespaces, function(PSR4Namespace $potentialNamespace) use ($namespace){
return $potentialNamespace->isAcceptableNamespaceRecursiveMode($namespace);
});
}
/**
* @param string $namespace
* @return PSR4Namespace
*/
private function findBestPSR4Namespace($namespace)
{
$composerNamespaces = $this->factory->getPSR4Namespaces();
$acceptableNamespaces = array_filter($composerNamespaces, function(PSR4Namespace $potentialNamespace) use ($namespace){
return $potentialNamespace->isAcceptableNamespace($namespace);
});
$carry = new \stdClass();
$carry->highestMatchingSegments = 0;
$carry->bestNamespace = null;
/** @var PSR4Namespace $bestNamespace */
$bestNamespace = array_reduce($acceptableNamespaces, function ($carry, PSR4Namespace $potentialNamespace) use ($namespace) {
$matchingSegments = $potentialNamespace->countMatchingNamespaceSegments($namespace);
if ($matchingSegments > $carry->highestMatchingSegments) {
$carry->highestMatchingSegments = $matchingSegments;
$carry->bestNamespace = $potentialNamespace;
}
return $carry;
}, $carry);
return $bestNamespace->bestNamespace;
}
}
@@ -0,0 +1,302 @@
<?php
namespace HaydenPierce\ClassFinder\PSR4;
use HaydenPierce\ClassFinder\ClassFinder;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class PSR4Namespace
{
/** @var string */
private $namespace;
/** @var string[] */
private $directories;
/** @var PSR4Namespace[] */
private $directSubnamespaces;
/**
* @param string $namespace
* @param string[] $directories
*/
public function __construct($namespace, $directories)
{
$this->namespace = $namespace;
$this->directories = $directories;
}
/**
* @param string $namespace
* @return bool
*/
public function knowsNamespace($namespace)
{
$numberOfSegments = count(explode('\\', $namespace));
$matchingSegments = $this->countMatchingNamespaceSegments($namespace);
if ($matchingSegments === 0) {
// Provided namespace doesn't map to anything registered.
return false;
} elseif ($numberOfSegments <= $matchingSegments) {
// This namespace is a superset of the provided namespace. Namespace is known.
return true;
} else {
// This namespace is a subset of the provided namespace. We must resolve the remaining segments to a directory.
$relativePath = substr($namespace, strlen($this->namespace));
foreach ($this->directories as $directory) {
$path = $this->normalizePath($directory, $relativePath);
if (is_dir($path)) {
return true;
}
}
return false;
}
}
/**
* Determines how many namespace segments match the internal namespace. This is useful because multiple namespaces
* may technically match a registered namespace root, but one of the matches may be a better match. Namespaces that
* match, but are not _the best_ match are incorrect matches. TestApp1\\ is **not** the best match when searching for
* namespace TestApp1\\Multi\\Foo if TestApp1\\Multi was explicitly registered.
*
* PSR4Namespace $a;
* $a->namespace = "TestApp1\\";
* $a->countMatchingNamespaceSegments("TestApp1\\Multi") -> 1, TestApp1 matches.
*
* PSR4Namespace $b;
* $b->namespace = "TestApp1\\Multi";
* $b->countMatchingNamespaceSegments("TestApp1\\Multi") -> 2, TestApp1\\Multi matches
*
* PSR4Namespace $c;
* $c->namespace = "HaydenPierce\\Foo\\Bar";
* $c->countMatchingNamespaceSegments("TestApp1\\Multi") -> 0, No matches.
*
* @param string $namespace
* @return int
*/
public function countMatchingNamespaceSegments($namespace)
{
$namespaceFragments = explode('\\', $namespace);
$undefinedNamespaceFragments = array();
while($namespaceFragments) {
$possibleNamespace = implode('\\', $namespaceFragments) . '\\';
if(strpos($this->namespace, $possibleNamespace) !== false){
return count(explode('\\', $possibleNamespace)) - 1;
}
array_unshift($undefinedNamespaceFragments, array_pop($namespaceFragments));
}
return 0;
}
/**
* @param string $namespace
* @return bool
*/
public function isAcceptableNamespace($namespace)
{
$namespaceSegments = count(explode('\\', $this->namespace)) - 1;
$matchingSegments = $this->countMatchingNamespaceSegments($namespace);
return $namespaceSegments === $matchingSegments;
}
/**
* @param string $namespace
* @return bool
*/
public function isAcceptableNamespaceRecursiveMode($namespace)
{
// Remove prefix backslash (TODO: review if we do this eariler).
$namespace = ltrim($namespace, '\\');
return strpos($this->namespace, $namespace) === 0;
}
/**
* Used to identify subnamespaces.
*
* @return string[]
*/
public function findDirectories()
{
$self = $this;
$directories = array_reduce($this->directories, function($carry, $directory) use ($self){
$path = $self->normalizePath($directory, '');
$realDirectory = realpath($path);
if ($realDirectory !== false) {
return array_merge($carry, array($realDirectory));
} else {
return $carry;
}
}, array());
$arraysOfClasses = array_map(function($directory) use ($self) {
$files = scandir($directory);
return array_map(function($file) use ($directory, $self) {
return $self->normalizePath($directory, $file);
}, $files);
}, $directories);
$potentialDirectories = array_reduce($arraysOfClasses, function($carry, $arrayOfClasses) {
return array_merge($carry, $arrayOfClasses);
}, array());
// Remove '.' and '..' directories
$potentialDirectories = array_filter($potentialDirectories, function($potentialDirectory) {
$segments = explode('/', $potentialDirectory);
$lastSegment = array_pop($segments);
return $lastSegment !== '.' && $lastSegment !== '..';
});
$confirmedDirectories = array_filter($potentialDirectories, function($potentialDirectory) {
return is_dir($potentialDirectory);
});
return $confirmedDirectories;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options = ClassFinder::STANDARD_MODE)
{
$relativePath = substr($namespace, strlen($this->namespace));
$self = $this;
$directories = array_reduce($this->directories, function($carry, $directory) use ($relativePath, $namespace, $self){
$path = $self->normalizePath($directory, $relativePath);
$realDirectory = realpath($path);
if ($realDirectory !== false) {
return array_merge($carry, array($realDirectory));
} else {
return $carry;
}
}, array());
$arraysOfClasses = array_map(function($directory) {
return scandir($directory);
}, $directories);
$potentialClassFiles = array_reduce($arraysOfClasses, function($carry, $arrayOfClasses) {
return array_merge($carry, $arrayOfClasses);
}, array());
$potentialClasses = array_map(function($file) use ($namespace){
return $namespace . '\\' . str_replace('.php', '', $file);
}, $potentialClassFiles);
if ($options == ClassFinder::RECURSIVE_MODE) {
return $this->getClassesFromListRecursively($namespace);
} else {
return array_filter($potentialClasses, function($potentialClass) {
if (function_exists($potentialClass)) {
// For some reason calling class_exists() on a namespace'd function raises a Fatal Error (tested PHP 7.0.8)
// Example: DeepCopy\deep_copy
return false;
} else {
return class_exists($potentialClass);
}
});
}
}
/**
* @return string[]
*/
private function getDirectClassesOnly()
{
$self = $this;
$directories = array_reduce($this->directories, function($carry, $directory) use ($self){
$path = $self->normalizePath($directory, '');
$realDirectory = realpath($path);
if ($realDirectory !== false) {
return array_merge($carry, array($realDirectory));
} else {
return $carry;
}
}, array());
$arraysOfClasses = array_map(function($directory) {
return scandir($directory);
}, $directories);
$potentialClassFiles = array_reduce($arraysOfClasses, function($carry, $arrayOfClasses) {
return array_merge($carry, $arrayOfClasses);
}, array());
$selfNamespace = $this->namespace; // PHP 5.3 BC
$potentialClasses = array_map(function($file) use ($self, $selfNamespace) {
return $selfNamespace . str_replace('.php', '', $file);
}, $potentialClassFiles);
return array_filter($potentialClasses, function($potentialClass) {
if (function_exists($potentialClass)) {
// For some reason calling class_exists() on a namespace'd function raises a Fatal Error (tested PHP 7.0.8)
// Example: DeepCopy\deep_copy
return false;
} else {
return class_exists($potentialClass);
}
});
}
/**
* @param string $namespace
* @return string[]
*/
public function getClassesFromListRecursively($namespace)
{
$initialClasses = strpos( $this->namespace, $namespace) !== false ? $this->getDirectClassesOnly() : array();
return array_reduce($this->getDirectSubnamespaces(), function($carry, PSR4Namespace $subNamespace) use ($namespace) {
return array_merge($carry, $subNamespace->getClassesFromListRecursively($namespace));
}, $initialClasses);
}
/**
* Join an absolute path and a relative path in a platform agnostic way.
*
* This method is also extracted so that it can be turned into a vfs:// stream URL for unit testing.
*
* @param string $directory
* @param string $relativePath
* @return mixed
*/
public function normalizePath($directory, $relativePath)
{
$path = str_replace('\\', '/', $directory . '/' . $relativePath);
return $path;
}
/**
* @return PSR4Namespace[]
*/
public function getDirectSubnamespaces()
{
return $this->directSubnamespaces;
}
/**
* @param PSR4Namespace[] $directSubnamespaces
*/
public function setDirectSubnamespaces($directSubnamespaces)
{
$this->directSubnamespaces = $directSubnamespaces;
}
/**
* @return mixed
*/
public function getNamespace()
{
return trim($this->namespace, '\\');
}
}
@@ -0,0 +1,153 @@
<?php
namespace HaydenPierce\ClassFinder\PSR4;
use HaydenPierce\ClassFinder\AppConfig;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class PSR4NamespaceFactory
{
/** @var AppConfig */
private $appConfig;
public function __construct(AppConfig $appConfig)
{
$this->appConfig = $appConfig;
}
/**
* @return string[]
*/
public function getPSR4Namespaces()
{
$namespaces = $this->getUserDefinedPSR4Namespaces();
$vendorNamespaces = require($this->appConfig->getAppRoot() . 'vendor/composer/autoload_psr4.php');
$namespaces = array_merge($vendorNamespaces, $namespaces);
// There's some wackiness going on here for PHP 5.3 compatibility.
$names = array_keys($namespaces);
$directories = array_values($namespaces);
$self = $this;
$namespaces = array_map(function($index) use ($self, $names, $directories) {
return $self->createNamespace($names[$index], $directories[$index]);
},range(0, count($namespaces) - 1));
return $namespaces;
}
/**
* @return string[]
*/
private function getUserDefinedPSR4Namespaces()
{
$appRoot = $this->appConfig->getAppRoot();
$composerJsonPath = $appRoot . 'composer.json';
$composerConfig = json_decode(file_get_contents($composerJsonPath));
if (!isset($composerConfig->autoload)) {
return array();
}
//Apparently PHP doesn't like hyphens, so we use variable variables instead.
$psr4 = "psr-4";
return (array)$composerConfig->autoload->$psr4;
}
/**
* Creates a namespace from composer_psr4.php and composer.json autoload.psr4 items.
*
* @param string $namespace
* @param string[] $directories
* @return PSR4Namespace
* @throws ClassFinderException
*/
public function createNamespace($namespace, $directories)
{
if (is_string($directories)) {
// This is an acceptable format according to composer.json
$directories = array($directories);
} elseif (is_array($directories)) {
// composer_psr4.php seems to put everything in this format
} else {
throw new ClassFinderException('Unknown PSR4 definition.');
}
$self = $this;
$appConfig = $this->appConfig;
$directories = array_map(function($directory) use ($self, $appConfig) {
if ($self->isAbsolutePath($directory)) {
return $directory;
} else {
return $appConfig->getAppRoot() . $directory;
}
}, $directories);
$directories = array_filter(array_map(function($directory) {
return realpath($directory);
}, $directories));
$psr4Namespace = new PSR4Namespace($namespace, $directories);
$subNamespaces = $this->getSubnamespaces($psr4Namespace);
$psr4Namespace->setDirectSubnamespaces($subNamespaces);
return $psr4Namespace;
}
/**
* @param PSR4Namespace $psr4Namespace
* @return PSR4Namespace[]
*/
private function getSubnamespaces(PSR4Namespace $psr4Namespace)
{
// Scan it's own directories.
$directories = $psr4Namespace->findDirectories();
$self = $this;
$subnamespaces = array_map(function($directory) use ($self, $psr4Namespace){
$segments = explode('/', $directory);
$subnamespaceSegment = array_pop($segments);
$namespace = $psr4Namespace->getNamespace() . "\\" . $subnamespaceSegment . "\\";
return $self->createNamespace($namespace, $directory);
}, $directories);
return $subnamespaces;
}
/**
* Check if a path is absolute.
*
* Mostly this answer https://stackoverflow.com/a/38022806/3000068
* A few changes: Changed exceptions to be ClassFinderExceptions, removed some ctype dependencies,
* updated the root prefix regex to handle Window paths better.
*
* @param string $path
* @return bool
* @throws ClassFinderException
*/
public function isAbsolutePath($path) {
if (!is_string($path)) {
$mess = sprintf('String expected but was given %s', gettype($path));
throw new ClassFinderException($mess);
}
// Optional wrapper(s).
$regExp = '%^(?<wrappers>(?:[[:print:]]{2,}://)*)';
// Optional root prefix.
$regExp .= '(?<root>(?:[[:alpha:]]:[/\\\\]|/)?)';
// Actual path.
$regExp .= '(?<path>(?:[[:print:]]*))$%';
$parts = array();
if (!preg_match($regExp, $path, $parts)) {
$mess = sprintf('Path is NOT valid, was given %s', $path);
throw new ClassFinderException($mess);
}
if ('' !== $parts['root']) {
return true;
}
return false;
}
}