Code and test

master 2.0.0
Julien Rosset 2 years ago
parent 6f50113fce
commit 8215fd4987

@ -19,7 +19,8 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"jrosset\\": "src/" "jrosset\\": "src/",
"Tests\\": "tests/"
}, },
"exclude-from-classmap": [ "tests/" ] "exclude-from-classmap": [ "tests/" ]
}, },

@ -0,0 +1,654 @@
<?php
/** @noinspection PhpUnused */
namespace jrosset\Reflection;
use FilesystemIterator;
use ReflectionClass;
use ReflectionEnum;
use ReflectionException;
/**
* A reflection class for namespaces (based on {@link https://www.php-fig.org/psr/psr-4/ PSR-4})
*
* Reports information about a namespace
*/
class ReflectionNamespace {
/**
* Don't return abstract classes
*
* @see static::getClasses()
*/
public const CLASS_IS_NOT_ABSTRACT = 1;
/**
* Don't return final classes
*
* @see static::getClasses()
*/
public const CLASS_IS_NOT_FINAL = 2;
/**
* @var string The namespace name
*/
private string $name;
/**
* @var string[] The list of directories that maps the namespace
*/
private array $directories;
/**
* Create a reflection on a namespace
*
* @param string $name The namespace name
* @param string[]|string $directories The list of directories that maps the namespace
*/
public function __construct (string $name, array|string $directories) {
$this->name = self::normalizeNamespaceName($name);
if (!is_array($directories)) {
$directories = [$directories];
}
$this->setDirectories($directories);
}
/**
* Create a reflection on a namespace from a mapping of {namespace} => {directories}
*
* @param string $name The namespace name
* @param array<string,string[]> $mapping The mapping
*
* @return static The namespace reflection
*
* @throws ReflectionException If the namespace can not be found with the mapping
*/
public static function createFromMapping (string $name, array $mapping): static {
$nsReflection = new static ($name, []);
//region Normalize the mapping
$normalizedMapping = [];
foreach ($mapping as $mapNamespace => $mapDirectories) {
$normalizedMapping[self::normalizeNamespaceName($mapNamespace)] = static::normalizeDirectoriesPath($mapDirectories);
}
//endregion
//region Get the map matching this namespace
$namespaceParts = explode('\\', $nsReflection->getName());
$namespacePartsDiscarded = [];
$map = null;
while (count($namespaceParts) > 0) {
$namespaceSearch = implode('\\', $namespaceParts);
if (!isset($normalizedMapping[$namespaceSearch])) {
$namespacePartsDiscarded[] = array_pop($namespaceParts);
continue;
}
$map = $normalizedMapping[$namespaceSearch];
break;
}
if ($map === null || count($map) === 0) {
throw new ReflectionException('Unable to identify namespace directories');
}
$mapDirectoriesSuffix = implode(DIRECTORY_SEPARATOR, array_reverse($namespacePartsDiscarded));
//endregion
//region Check each directory of the map
$nsDirectories = [];
foreach ($map as $mapDirectory) {
$mapDirectory .= DIRECTORY_SEPARATOR . $mapDirectoriesSuffix;
if (!file_exists($mapDirectory) || !is_dir($mapDirectory) || !is_readable($mapDirectory)) {
continue;
}
$nsDirectories[] = $mapDirectory;
}
if (count($nsDirectories) === 0) {
throw new ReflectionException('Unable to identify namespace directories');
}
//endregion
return $nsReflection->setDirectories($nsDirectories);
}
/**
* Create a reflection on a namespace from the {@link https://getcomposer.org/ composer} mapping (PSR-0 and PSR-4)
*
* @param string $name The namespace name
* @param string $vendorDirectoryPath The path to the “vendor” directory
*
* @return static The namespace reflection
*
* @throws ReflectionException If the namespace can not be found
*/
public static function createFromComposerMapping (string $name, string $vendorDirectoryPath): static {
$composerDirectory = static::normalizeDirectoryPath($vendorDirectoryPath) . DIRECTORY_SEPARATOR . 'composer';
if (!file_exists($composerDirectory) || !is_dir($vendorDirectoryPath) || !is_readable($vendorDirectoryPath)) {
throw new ReflectionException('Unable to find composer directory: ' . $composerDirectory);
}
$composerDirectory .= DIRECTORY_SEPARATOR;
$composerFiles = [
$composerDirectory . 'autoload_psr4.php', // PSR-4
];
$mapping = [];
foreach ($composerFiles as $composerFile) {
if (!file_exists($composerFile)) {
continue;
}
$mapping = array_merge_recursive(
$mapping,
require($composerFile)
);
}
return static::createFromMapping(
$name,
$mapping
);
}
/**
* The namespace name
*
* @return string The namespace name
*/
public function getName (): string {
return $this->name;
}
/**
* The namespace short name, without parent namespaces
*
* @return string The namespace short name
*/
public function getShortName (): string {
$name = $this->getName();
if (($pos = mb_strrpos($name, '\\')) !== false) {
$name = mb_substr($name, $pos + 1);
}
return $name;
}
/**
* The parent namespace name or Null if there is no parent
*
* @return string|null The parent namespace name or Null if there is no parent
*/
public function getParentName (): ?string {
if (($pos = mb_strrpos($this->getName(), '\\')) === false) {
return null;
}
return mb_substr($this->getName(), 0, $pos);
}
/**
* The parent namespace or Null if there is no parent
*
* <b>WARNING:</b> There is a high probability this method return a {@see ReflectionException} but with invalid directories, breaking his behavior.
* I recommend using a createFromXXX method with {@see static::getParentName()}
*
* @return static|null The parent namespace or Null if there is no parent
*/
public function getParent (): ?static {
if (($parentNamespaceName = $this->getParentName()) === null) {
return null;
}
$parentNamespaceDirectories = [];
foreach ($this->getDirectories() as $directory) {
if (($parentNamespaceDirectory = basename($directory)) === '') {
continue;
}
$parentNamespaceDirectories[] = $parentNamespaceDirectory;
}
if (count($parentNamespaceDirectories) === 0) {
return null;
}
return new static ($parentNamespaceName, $parentNamespaceDirectories);
}
/**
* Get a sub namespace of the namespace
*
* @param string $subNamespaceName The sub namespace name
*
* @return static The sub namespace
*
* @throws ReflectionException If the sub namespace doesn't exist
*/
public function getSubNamespace (string $subNamespaceName): static {
$this->checkValidDirectories();
$subNamespaceDirectories = [];
foreach ($this->getDirectories() as $directory) {
$subNamespaceDirectory = $directory . DIRECTORY_SEPARATOR . $subNamespaceName;
if (!file_exists($subNamespaceDirectory) || !is_dir($subNamespaceDirectory) || !is_readable($subNamespaceDirectory)) {
continue;
}
$subNamespaceDirectories[] = $subNamespaceDirectory;
}
if (count($subNamespaceDirectories) === 0) {
throw new ReflectionException('Unable to find namespace "' . $this->getName() . '\\' . $subNamespaceName . '"');
}
return new static ($this->getName() . '\\' . $subNamespaceName, $subNamespaceDirectories);
}
/**
* Get the sub namespaces of the namespace
*
* @param bool $recursive Search also in sub-sub namespaces
*
* @return static[] The list of subs namespaces
*
* @throws ReflectionException If the namespace has no configured directories
*/
public function getSubNamespaces (bool $recursive = false): array {
$this->checkValidDirectories();
/** @var ReflectionNamespace[] $subNamespaces */
$subNamespaces = [];
//region For each directory of the namespace
foreach ($this->getDirectories() as $directory) {
$directoryIterator = new FilesystemIterator($directory, FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS);
//region For each file of the current directory
foreach ($directoryIterator as $directoryEntry) {
//region Ignore hidden files (start with a dot)
if (mb_substr($directoryEntry->getFilename(), 0, 1) === '.') {
continue;
}
//endregion
//region Ignore elements that are not directories
if (!$directoryEntry->isDir()) {
continue;
}
//endregion
$subNamespaceReflection = new static ($this->getName() . '\\' . $directoryEntry->getFilename(), [$directoryEntry->getPathname()]);
//region Treat recursivity
if ($recursive) {
$subNamespaces = array_merge(
$subNamespaces,
$subNamespaceReflection->getSubNamespaces(true)
);
}
//endregion }
if (isset($subNamespaces[$subNamespaceReflection->getName()])) {
$existingSubNamespace = $subNamespaces[$subNamespaceReflection->getName()];
$existingSubNamespace->setDirectories(
array_unique(
array_merge(
$existingSubNamespace->getDirectories(),
$subNamespaceReflection->getDirectories()
)
)
);
}
else {
$subNamespaces[$subNamespaceReflection->getName()] = $subNamespaceReflection;
}
}
//endregion
}
//endregion
return $subNamespaces;
}
/**
* Get a class of the namespace
*
* @param string $className The class name
*
* @return ReflectionClass The reflection class
*
* @throws ReflectionException If the class doesn't exist or is not a class
*/
public function getClass (string $className): ReflectionClass {
$reflection = new ReflectionClass($this->getName() . '\\' . $className);
if ($reflection->isInterface() || $reflection->isTrait()) {
throw new ReflectionException('The "' . $this->getName() . '\\' . $className . '" is not a class');
}
return $reflection;
}
/**
* Get the classes of the namespace
*
* @param int|null $filters The filters, separated with "|".
* <br>All possibles filters: {@see static::CLASS_IS_NOT_ABSTRACT}, {@see static::CLASS_IS_NOT_FINAL}
* @param bool $recursive Search also in sub namespaces
*
* @return ReflectionClass[] The list of classes
*
* @throws ReflectionException If the namespace has no configured directories
*/
public function getClasses (?int $filters = null, bool $recursive = false): array {
$filters ??= 0;
$classes = [];
//region For each valid element of the namespace
foreach ($this->getValidElements($recursive) as $element) {
//region Try to get the reflection class
try {
$classReflection = new ReflectionClass($this->getName() . '\\' . $element);
}
catch (ReflectionException) {
continue;
}
//endregion
//region Check it is a class (ignore interfaces and traits)
if ($classReflection->isInterface() || $classReflection->isTrait() || $classReflection->isEnum()) {
continue;
}
//endregion
//region Check filters
if (($filters & static::CLASS_IS_NOT_ABSTRACT) === static::CLASS_IS_NOT_ABSTRACT && $classReflection->isAbstract()) {
continue;
}
if (($filters & static::CLASS_IS_NOT_FINAL) === static::CLASS_IS_NOT_FINAL && $classReflection->isFinal()) {
continue;
}
//endregion
$classes[] = $classReflection;
}
//endregion
return $classes;
}
/**
* Get an interface of the namespace
*
* @param string $interfaceName The interface name
*
* @return ReflectionClass The reflection interface
*
* @throws ReflectionException If the interface doesn't exist or is not an interface
*/
public function getInterface (string $interfaceName): ReflectionClass {
$reflection = new ReflectionClass($this->getName() . '\\' . $interfaceName);
if (!$reflection->isInterface()) {
throw new ReflectionException('The "' . $this->getName() . '\\' . $interfaceName . '" is not an interface');
}
return $reflection;
}
/**
* Get the interfaces of the namespace
*
* @param bool $recursive Search also in sub namespaces
*
* @return ReflectionClass[] The list of interfaces
*
* @throws ReflectionException If the namespace has no configured directories
*/
public function getInterfaces (bool $recursive = false): array {
$interfaces = [];
//region For each valid element of the namespace
foreach ($this->getValidElements($recursive) as $element) {
//region Try to get the reflection class
try {
$interfaceReflection = new ReflectionClass($this->getName() . '\\' . $element);
}
catch (ReflectionException) {
continue;
}
//endregion
//region Check it is an interface
if (!$interfaceReflection->isInterface()) {
continue;
}
//endregion
$interfaces[] = $interfaceReflection;
}
//endregion
return $interfaces;
}
/**
* Get a trait of the namespace
*
* @param string $traitName The trait name
*
* @return ReflectionClass The reflection trait
*
* @throws ReflectionException If the trait doesn't exist or is not a trait
*/
public function getTrait (string $traitName): ReflectionClass {
$reflection = new ReflectionClass($this->getName() . '\\' . $traitName);
if (!$reflection->isTrait()) {
throw new ReflectionException('The "' . $this->getName() . '\\' . $traitName . '" is not a trait');
}
return $reflection;
}
/**
* Get the traits of the namespace
*
* @param bool $recursive Search also in sub namespaces
*
* @return ReflectionClass[] The list of traits
*
* @throws ReflectionException If the namespace has no configured directories
*/
public function getTraits (bool $recursive = false): array {
$traits = [];
//region For each valid element of the namespace
foreach ($this->getValidElements($recursive) as $element) {
//region Try to get the reflection class
try {
$traitReflection = new ReflectionClass($this->getName() . '\\' . $element);
}
catch (ReflectionException) {
continue;
}
//endregion
//region Check it is a trait
if (!$traitReflection->isTrait()) {
continue;
}
//endregion
$traits[] = $traitReflection;
}
//endregion
return $traits;
}
/**
* Get an enum of the namespace
*
* @param string $enumName The enum name
*
* @return ReflectionEnum The reflection enum
*
* @throws ReflectionException If the enum doesn't exist
*/
public function getEnum (string $enumName): ReflectionEnum {
return new ReflectionEnum($this->getName() . '\\' . $enumName);
}
/**
* Get the enums of the namespace
*
* @param bool $recursive Search also in sub namespaces
*
* @return ReflectionEnum[] The list of enums
*
* @throws ReflectionException If the namespace has no configured directories
*/
public function getEnums (bool $recursive = false): array {
$traits = [];
//region For each valid element of the namespace
foreach ($this->getValidElements($recursive) as $element) {
//region Try to get the reflection class
try {
$enumReflection = new ReflectionEnum($this->getName() . '\\' . $element);
}
catch (ReflectionException) {
continue;
}
//endregion
$traits[] = $enumReflection;
}
//endregion
return $traits;
}
/**
* The list of directories that maps the namespace
*
* @return string[] The list of directories that maps the namespace
*/
public function getDirectories (): array {
return $this->directories;
}
/**
* Set the list of directories that maps the namespace
*
* @param string[] $directories The list of directories that maps the namespace
*/
public function setDirectories (array $directories): static {
$this->directories = array_unique(static::normalizeDirectoriesPath($directories));
return $this;
}
/**
* Check if at least one directory is configured
*
* @return void
*
* @throws ReflectionException If the namespace has no configured directories
*/
protected function checkValidDirectories (): void {
if (count($this->getDirectories()) === 0) {
throw new ReflectionException('The namespace must have at least one directory configured');
}
}
/**
* Get the valid “elements” (class, interface, trait or enum) name of the namespace
*
* @param bool $recursive Search also in sub namespaces
*
* @return string[] The list of valid elements
*
* @throws ReflectionException If the namespace has no configured directories
*/
protected function getValidElements (bool $recursive): array {
$this->checkValidDirectories();
$namespaceName = $this->getName();
$elements = [];
//region For each directory of the namespace
foreach ($this->getDirectories() as $directory) {
$directoryIterator = new FilesystemIterator($directory, FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS);
//region For each file of the current directory
foreach ($directoryIterator as $directoryEntry) {
//region Ignore hidden files (start with a dot)
if (mb_substr($directoryEntry->getFilename(), 0, 1) === '.') {
continue;
}
//endregion
//region Treat subdirectories
if ($directoryEntry->isDir()) {
//region Recursivity disabled → ignore subdirectories
if (!$recursive) {
continue;
}
//endregion
//region PSR-4 → each subdirectory is a sub namespace → created it and get its elements
$subNamespaceReflection = new static ($this->getName() . '\\' . $directoryEntry->getFilename(), [$directoryEntry->getPathname()]);
$subNamespaceElementsPrefix = mb_substr($subNamespaceReflection->getName(), mb_strlen($namespaceName . '\\')) . '\\';
$elements = array_merge(
$elements,
array_map(
function (string $subNamespaceElement) use ($subNamespaceElementsPrefix): string {
return $subNamespaceElementsPrefix . $subNamespaceElement;
},
$subNamespaceReflection->getValidElements(true)
)
);
unset($subNamespaceReflection);
//endregion
continue;
}
//endregion
//region Ignore elements that are not files
if (!$directoryEntry->isFile()) {
continue;
}
//endregion
//region Check file name against PSR-4 (one class by file, file name is class name)
if (preg_match('#^\s*(?<elementName>[A-Z][A-Za-z0-9_]+)\s*\.php\s*$#u', $directoryEntry->getFilename(), $match) !== 1) {
continue;
}
//endregion
$elements[] = $match['elementName'];
}
//endregion
}
//endregion
return $elements;
}
/**
* Normalize a namespace name : remove initial and final "\" if present
*
* @param string $name The name
*
* @return string The normalized name
*/
public static final function normalizeNamespaceName (string $name): string {
return preg_replace('#^\\\\|\\\\$#u', '', $name);
}
/**
* Normalize a directory path : ensure use of {@see DIRECTORY_SEPARATOR} and remove final separator
*
* @param string $path The path
*
* @return string The normalized path
*/
public static final function normalizeDirectoryPath (string $path): string {
return realpath(
preg_replace(
[
'#[/\\\\]#u',
'#[/\\\\]$#u',
],
[
DIRECTORY_SEPARATOR,
'',
],
$path
)
);
}
/**
* Normalize a list of directory path : ensure use of {@see DIRECTORY_SEPARATOR} and remove final separator
*
* @param string[] $paths The path list
*
* @return string[] The normalized path list
*/
public static final function normalizeDirectoriesPath (array $paths): array {
return array_map(
[static::class, 'normalizeDirectoryPath'],
$paths
);
}
}

@ -0,0 +1,8 @@
<?php
namespace Tests\SubTests1;
enum Enum1 {
case One;
case Two;
}

@ -0,0 +1,8 @@
<?php
namespace Tests\SubTests1\SubSubTests11;
enum Enum11: int {
case One = 1;
case Two = 2;
}

@ -0,0 +1,7 @@
<?php
namespace Tests\SubTests2;
interface ISubTests2 {
}

@ -0,0 +1,7 @@
<?php
namespace Tests\SubTests2\SubSubTests21;
class SubSubTests211 {
}

@ -0,0 +1,7 @@
<?php
namespace Tests\SubTests2\SubSubTests22;
class SubSubTests221 {
}

@ -0,0 +1,7 @@
<?php
namespace Tests\SubTests2;
trait TSubSubTests2 {
}

@ -0,0 +1,34 @@
<?php /** @noinspection PhpUnhandledExceptionInspection */
use jrosset\Reflection\ReflectionNamespace;
require_once __DIR__ . '/../vendor/autoload.php';
$tests = ReflectionNamespace::createFromComposerMapping('\\Tests', __DIR__ . '/../vendor');
echo $tests->getShortName() . ' (' . $tests->getName() . ')' . PHP_EOL . PHP_EOL;
echo '===== NAMESPACES =====' . PHP_EOL;
foreach ($tests->getSubNamespaces(true) as $subNamespace) {
echo "\t - " . $subNamespace->getName() . PHP_EOL;
}
echo '===== CLASSES =====' . PHP_EOL;
foreach ($tests->getClasses(null, true) as $class) {
echo "\t - " . $class->getName() . PHP_EOL;
}
echo '===== INTERFACES =====' . PHP_EOL;
foreach ($tests->getInterfaces(true) as $interface) {
echo "\t - " . $interface->getName() . PHP_EOL;
}
echo '===== TRAITS =====' . PHP_EOL;
foreach ($tests->getTraits(true) as $trait) {
echo "\t - " . $trait->getName() . PHP_EOL;
}
echo '===== ENUMS =====' . PHP_EOL;
foreach ($tests->getEnums(true) as $enum) {
echo "\t - " . $enum->getName() . ($enum->isBacked() ? ' (backed)' : '') . PHP_EOL;
}
Loading…
Cancel
Save