diff --git a/README.md b/README.md index 5ca5b3a..105c15e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ClassFinder =========== -A dead simple utility to identify classes in a given namespace. +A dead simple utility to identify classes, traits and interfaces 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. @@ -50,7 +50,14 @@ Examples require_once __DIR__ . '/vendor/autoload.php'; +// Will default to ClassFinder::ALLOW_CLASSES $classes = ClassFinder::getClassesInNamespace('TestApp1\Foo'); +$classes = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::ALLOW_CLASSES); +$interfaces = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::ALLOW_INTERFACES); +$traits = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::ALLOW_TRAITS); + +// You can combine any of the flags +$interfacesTraits = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::ALLOW_TRAITS | ClassFinder::ALLOW_INTERFACES); /** * array( @@ -60,6 +67,27 @@ $classes = ClassFinder::getClassesInNamespace('TestApp1\Foo'); * ) */ var_dump($classes); +/** + * array( + * 'TestApp1\Foo\BarInterface', + * 'TestApp1\Foo\FooInterface' + * ) + */ +var_dump($interfaces); +/** + * array( + * 'TestApp1\Foo\BazTrait', + * ) + */ +var_dump($traits); +/** + * array( + * 'TestApp1\Foo\BarInterface', + * 'TestApp1\Foo\FooInterface', + * 'TestApp1\Foo\BazTrait', + * ) + */ +var_dump($interfacesTraits); ``` **Recursive Mode** *(in v0.3-beta)* @@ -69,7 +97,15 @@ var_dump($classes); require_once __DIR__ . '/vendor/autoload.php'; +// As before, will default to ClassFinder::ALLOW_CLASSES $classes = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE); +$classes = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE | ClassFinder::ALLOW_CLASSES); +$interfaces = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE | ClassFinder::ALLOW_INTERFACES); +$traits = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE | ClassFinder::ALLOW_TRAITS); + + +// You can combine any of the flags +$classesTraits = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE | ClassFinder::ALLOW_CLASSS | ClassFinder::ALLOW_TRAITS); /** * array( @@ -85,6 +121,11 @@ $classes = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECUR * ) */ var_dump($classes); + +// You get the idea :) +var_dump($interfaces); +var_dump($traits); +var_dump($classesTraits); ``` Documentation diff --git a/src/ClassFinder.php b/src/ClassFinder.php index c20ddc3..f6bc331 100644 --- a/src/ClassFinder.php +++ b/src/ClassFinder.php @@ -13,6 +13,11 @@ class ClassFinder { const STANDARD_MODE = 1; const RECURSIVE_MODE = 2; + const ALLOW_CLASSES = 4; + const ALLOW_INTERFACES = 8; + const ALLOW_TRAITS = 16; + + const MODE_MASK = 3; /** @var AppConfig */ private static $config; @@ -71,6 +76,10 @@ class ClassFinder */ public static function getClassesInNamespace($namespace, $options = self::STANDARD_MODE) { + if (!($options & (self::ALLOW_INTERFACES | self::ALLOW_TRAITS))) { + // Default to classes + $options |= self::ALLOW_CLASSES; + } self::initialize(); $findersWithNamespace = self::findersWithNamespace($namespace); diff --git a/src/Classmap/ClassmapEntryFactory.php b/src/Classmap/ClassmapEntryFactory.php index d1f2539..71ffb7e 100644 --- a/src/Classmap/ClassmapEntryFactory.php +++ b/src/Classmap/ClassmapEntryFactory.php @@ -3,6 +3,7 @@ namespace HaydenPierce\ClassFinder\Classmap; use HaydenPierce\ClassFinder\AppConfig; +use HaydenPierce\ClassFinder\ClassFinder; class ClassmapEntryFactory { @@ -17,12 +18,18 @@ class ClassmapEntryFactory /** * @return ClassmapEntry[] */ - public function getClassmapEntries() + public function getClassmapEntries($allowAdditional = ClassFinder::ALLOW_INTERFACES | ClassFinder::ALLOW_TRAITS) { // 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'); + $classmap = array_filter($classmap, function ($potentialClass) use ($allowAdditional) { + return ($allowAdditional & ClassFinder::ALLOW_CLASSES && class_exists($potentialClass)) + || ($allowAdditional & ClassFinder::ALLOW_INTERFACES && interface_exists($potentialClass)) + || ($allowAdditional & ClassFinder::ALLOW_TRAITS && trait_exists($potentialClass)); + }, ARRAY_FILTER_USE_KEY); + // if classmap has no entries return empty array if(count($classmap) == 0) { return array(); diff --git a/src/Classmap/ClassmapFinder.php b/src/Classmap/ClassmapFinder.php index f5875a4..4a16087 100644 --- a/src/Classmap/ClassmapFinder.php +++ b/src/Classmap/ClassmapFinder.php @@ -38,7 +38,7 @@ class ClassmapFinder implements FinderInterface */ public function findClasses($namespace, $options) { - $classmapEntries = $this->factory->getClassmapEntries(); + $classmapEntries = $this->factory->getClassmapEntries($options); $matchingEntries = array_filter($classmapEntries, function(ClassmapEntry $entry) use ($namespace, $options) { return $entry->matches($namespace, $options); diff --git a/src/Files/FilesEntry.php b/src/Files/FilesEntry.php index c1a27b5..06a8b5c 100644 --- a/src/Files/FilesEntry.php +++ b/src/Files/FilesEntry.php @@ -2,6 +2,8 @@ namespace HaydenPierce\ClassFinder\Files; +use HaydenPierce\ClassFinder\ClassFinder; + class FilesEntry { /** @var string */ @@ -38,14 +40,14 @@ class FilesEntry } /** - * Gets a list of classes that belong to the given namespace. - * + * Gets a list of classes that belong to the given namespace * @param string $namespace + * @param int $options * @return string[] */ - public function getClasses($namespace) + public function getClasses($namespace, $options) { - $classes = $this->getClassesInFile(); + $classes = $this->getClassesInFile($options); return array_values(array_filter($classes, function($class) use ($namespace) { $classNameFragments = explode('\\', $class); @@ -58,6 +60,19 @@ class FilesEntry })); } + /** + * Execute PHP code and return retuend value + * + * @param string $script + * @return mixed + */ + private function execReturn($script) + { + exec($this->php . " -r \"$script\"", $output); + $classes = 'return ' . implode('', $output) . ';'; + return eval($classes); + } + /** * Dynamically execute files and check for defined classes. * @@ -68,24 +83,35 @@ class FilesEntry * * @return array */ - private function getClassesInFile() + private function getClassesInFile($options = ClassFinder::ALLOW_INTERFACES | ClassFinder::ALLOW_TRAITS) { - // 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); + // get_declared_*() returns a bunch of classes|interfaces|traits that are built into PHP. So we need a control here. + list($initialInterfaces, + $initialClasses, + $initialTraits + ) = $this->execReturn("var_export(array(get_declared_interfaces(), get_declared_classes(), get_declared_traits()));"); // 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); + list($allInterfaces, + $allClasses, + $allTraits + ) = $this->execReturn("require_once '{$this->file}'; var_export(array(get_declared_interfaces(), get_declared_classes(), get_declared_traits()));"); - return array_diff($allClasses, $initialClasses); + $interfaces = array_diff($allInterfaces, $initialInterfaces); + $classes = array_diff($allClasses, $initialClasses); + $traits = array_diff($allTraits, $initialTraits); + + $final = array(); + if ($options & ClassFinder::ALLOW_CLASSES) { + $final = $classes; + } + if ($options & ClassFinder::ALLOW_INTERFACES) { + $final = array_merge($final, $interfaces); + } + if ($options & ClassFinder::ALLOW_TRAITS) { + $final = array_merge($final, $traits); + } + return $final; } /** diff --git a/src/Files/FilesFinder.php b/src/Files/FilesFinder.php index 0370ed2..389c300 100644 --- a/src/Files/FilesFinder.php +++ b/src/Files/FilesFinder.php @@ -52,8 +52,8 @@ class FilesFinder implements FinderInterface { $filesEntries = $this->factory->getFilesEntries(); - return array_reduce($filesEntries, function($carry, FilesEntry $entry) use ($namespace){ - return array_merge($carry, $entry->getClasses($namespace)); + return array_reduce($filesEntries, function($carry, FilesEntry $entry) use ($namespace, $options){ + return array_merge($carry, $entry->getClasses($namespace, $options)); }, array()); } } diff --git a/src/PSR4/PSR4Namespace.php b/src/PSR4/PSR4Namespace.php index 2b2bd9b..6d00a71 100644 --- a/src/PSR4/PSR4Namespace.php +++ b/src/PSR4/PSR4Namespace.php @@ -168,6 +168,9 @@ class PSR4Namespace */ public function findClasses($namespace, $options = ClassFinder::STANDARD_MODE) { + $allowAdditional = $options & ~ClassFinder::MODE_MASK; + $options &= ClassFinder::MODE_MASK; + $relativePath = substr($namespace, strlen($this->namespace)); $self = $this; @@ -194,24 +197,27 @@ class PSR4Namespace }, $potentialClassFiles); if ($options == ClassFinder::RECURSIVE_MODE) { - return $this->getClassesFromListRecursively($namespace); + return $this->getClassesFromListRecursively($namespace, $allowAdditional); } else { - return array_filter($potentialClasses, function($potentialClass) { + return array_filter($potentialClasses, function($potentialClass) use ($allowAdditional) { 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 ($allowAdditional & ClassFinder::ALLOW_CLASSES && class_exists($potentialClass)) + || ($allowAdditional & ClassFinder::ALLOW_INTERFACES && interface_exists($potentialClass)) + || ($allowAdditional & ClassFinder::ALLOW_TRAITS && trait_exists($potentialClass)); } }); } } /** + * @param int $allowAdditional * @return string[] */ - private function getDirectClassesOnly() + private function getDirectClassesOnly($allowAdditional) { $self = $this; $directories = array_reduce($this->directories, function($carry, $directory) use ($self){ @@ -237,27 +243,30 @@ class PSR4Namespace return $selfNamespace . str_replace('.php', '', $file); }, $potentialClassFiles); - return array_filter($potentialClasses, function($potentialClass) { + return array_filter($potentialClasses, function($potentialClass) use ($allowAdditional) { 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 ($allowAdditional & ClassFinder::ALLOW_CLASSES && class_exists($potentialClass)) + || ($allowAdditional & ClassFinder::ALLOW_INTERFACES && interface_exists($potentialClass)) + || ($allowAdditional & ClassFinder::ALLOW_TRAITS && trait_exists($potentialClass)); } }); } /** * @param string $namespace + * @param $allowAdditional * @return string[] */ - public function getClassesFromListRecursively($namespace) + public function getClassesFromListRecursively($namespace, $allowAdditional) { - $initialClasses = strpos( $this->namespace, $namespace) !== false ? $this->getDirectClassesOnly() : array(); + $initialClasses = strpos( $this->namespace, $namespace) !== false ? $this->getDirectClassesOnly($allowAdditional) : array(); - return array_reduce($this->getDirectSubnamespaces(), function($carry, PSR4Namespace $subNamespace) use ($namespace) { - return array_merge($carry, $subNamespace->getClassesFromListRecursively($namespace)); + return array_reduce($this->getDirectSubnamespaces(), function($carry, PSR4Namespace $subNamespace) use ($namespace, $allowAdditional) { + return array_merge($carry, $subNamespace->getClassesFromListRecursively($namespace, $allowAdditional)); }, $initialClasses); } diff --git a/test/unit/Files/FilesEntryTest.php b/test/unit/Files/FilesEntryTest.php index ae02748..20a221a 100644 --- a/test/unit/Files/FilesEntryTest.php +++ b/test/unit/Files/FilesEntryTest.php @@ -46,7 +46,7 @@ EOL $files = new FilesEntry($tmpFilename, $this->findPHP()); - $classes = $files->getClasses($namespace); + $classes = $files->getClasses($namespace, 0); $this->assertEquals($expected, $classes, 'FilesEntry should be able to determine the classes defined in a given file.'); }