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
@@ -0,0 +1,7 @@
/ci/ export-ignore
/docs/ export-ignore
/test/ export-ignore
/.gitignore export-ignore
/.gitlab-ci.yml export-ignore
/composer.lock export-ignore
/phpunit.xml export-ignore
+13
View File
@@ -0,0 +1,13 @@
Copyright (c) 2018 Hayden Pierce
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+126
View File
@@ -0,0 +1,126 @@
ClassFinder
===========
A dead simple utility to identify classes in a given namespace.
This package is an improved implementation of an [answer on Stack Overflow](https://stackoverflow.com/a/40229665/3000068)
and provides additional features with less configuration required.
Requirements
------------
* Application is using Composer.
* Classes can be autoloaded with Composer.
* PHP >= 5.3.0
Installing
----------
Installing is done by requiring it with Composer.
```
$ composer require haydenpierce/class-finder
```
No other installation methods are currently supported.
Supported Autoloading Methods
--------------------------------
| Method | Supported | with `ClassFinder::RECURSIVE_MODE` |
| ---------- | --------- | ---------------------------------- |
| PSR-4 | ✔️ | ✔️ |
| PSR-0 | ❌️* | ❌️* |
| Classmap | ✔️ | ✔️ |
| Files | ✔️^ | ❌️** |
\^ Experimental.
\* Planned.
\** Not planned. Open an issue if you need this feature.
Examples
--------
**Standard Mode**
```
<?php
require_once __DIR__ . '/vendor/autoload.php';
$classes = ClassFinder::getClassesInNamespace('TestApp1\Foo');
/**
* array(
* 'TestApp1\Foo\Bar',
* 'TestApp1\Foo\Baz',
* 'TestApp1\Foo\Foo'
* )
*/
var_dump($classes);
```
**Recursive Mode** *(in v0.3-beta)*
```
<?php
require_once __DIR__ . '/vendor/autoload.php';
$classes = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE);
/**
* array(
* 'TestApp1\Foo\Bar',
* 'TestApp1\Foo\Baz',
* 'TestApp1\Foo\Foo',
* 'TestApp1\Foo\Box\Bar',
* 'TestApp1\Foo\Box\Baz',
* 'TestApp1\Foo\Box\Foo',
* 'TestApp1\Foo\Box\Lon\Bar',
* 'TestApp1\Foo\Box\Lon\Baz',
* 'TestApp1\Foo\Box\Lon\Foo',
* )
*/
var_dump($classes);
```
Documentation
-------------
[Changelog](docs/changelog.md)
**Exceptions**:
* [Files could not locate PHP](docs/exceptions/filesCouldNotLocatePHP.md)
* [Files exec not available](docs/exceptions/filesExecNotAvailable.md)
* [Missing composer.json](docs/exceptions/missingComposerConfig.md)
**Internals**
* [How Testing Works](docs/testing.md)
* [Continuous Integration Notes](docs/ci.md)
Future Work
-----------
> **WARNING**: Before 1.0.0, expect that bug fixes _will not_ be backported to older versions. Backwards incompatible changes
may be introduced in minor 0.X.Y versions, where X changes.
* `psr0` support
* Additional features:
Various ideas:
* ~~`ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE)`.
Providing classes multiple namespaces deep.~~ (included v0.3-beta)
* `ClassFinder::getClassesImplementingInterface('TestApp1\Foo', 'TestApp1\FooInterface', ClassFinder::RECURSIVE_MODE)`.
Filtering classes to only classes that implement a namespace.
* `ClassFinder::debugRenderReport('TestApp1\Foo\Baz')`
Guidance for solving "class not found" errors resulting from typos in namespaces, missing directories, etc. Would print
an HTML report. Not intended for production use, but debugging.
+27
View File
@@ -0,0 +1,27 @@
{
"name": "haydenpierce/class-finder",
"description" : "A library that can provide of a list of classes in a given namespace",
"type": "library",
"license": "MIT",
"version": "0.4.3",
"authors": [
{
"name": "Hayden Pierce",
"email": "hayden@haydenpierce.com"
}
],
"require": {
"php": ">=5.3",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "~9.0",
"mikey179/vfsstream": "^1.6"
},
"autoload": {
"psr-4": {
"HaydenPierce\\ClassFinder\\": "src/",
"HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit"
}
}
}
+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;
}
}