Подтвердить что ты не робот

PHP - получить все имена классов внутри определенного пространства имен

Я хочу получить все классы внутри пространства имен. У меня есть что-то вроде этого:

#File: MyClass1.php
namespace MyNamespace;

class MyClass1() { ... }

#File: MyClass2.php
namespace MyNamespace;

class MyClass2() { ... }

#Any number of files and classes with MyNamespace may be specified.

#File: ClassHandler.php
namespace SomethingElse;
use MyNamespace as Classes;

class ClassHandler {
    public function getAllClasses() {
        // Here I want every classes declared inside MyNamespace.
    }
}

Я пробовал get_declared_classes() внутри getAllClasses(), но MyClass1 и MyClass2 не были в списке.

Как я могу это сделать?

4b9b3361

Ответ 1

Общий подход заключается в том, чтобы получить все полностью определенные имена классов (класс с полным пространством имен) в вашем проекте, а затем выполнить фильтрацию по требуемому пространству имен.

PHP предлагает некоторые встроенные функции для получения этих классов (get_declared_classes и т.д.), Но они не смогут найти классы, которые не были загружены (include/require), поэтому он не будет работать, как ожидается, с автозагрузчиками (например, Composer для пример). Это серьезная проблема, поскольку использование автозагрузчиков очень распространено.

Таким образом, ваше последнее средство - найти все файлы PHP самостоятельно и проанализировать их, чтобы извлечь их пространство имен и класс:

$path = __DIR__;
$fqcns = array();

$allFiles = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
$phpFiles = new RegexIterator($allFiles, '/\.php$/');
foreach ($phpFiles as $phpFile) {
    $content = file_get_contents($phpFile->getRealPath());
    $tokens = token_get_all($content);
    $namespace = '';
    for ($index = 0; isset($tokens[$index]); $index++) {
        if (!isset($tokens[$index][0])) {
            continue;
        }
        if (T_NAMESPACE === $tokens[$index][0]) {
            $index += 2; // Skip namespace keyword and whitespace
            while (isset($tokens[$index]) && is_array($tokens[$index])) {
                $namespace .= $tokens[$index++][1];
            }
        }
        if (T_CLASS === $tokens[$index][0] && T_WHITESPACE === $tokens[$index + 1][0] && T_STRING === $tokens[$index + 2][0]) {
            $index += 2; // Skip class keyword and whitespace
            $fqcns[] = $namespace.'\\'.$tokens[$index][1];

            # break if you have one class per file (psr-4 compliant)
            # otherwise you'll need to handle class constants (Foo::class)
            break;
        }
    }
}

Если вы следуете стандартам PSR 0 или PSR 4 (ваше дерево каталогов отражает ваше пространство имен), вам не нужно ничего фильтровать: просто укажите путь, который соответствует требуемому пространству имен.

Если вы не любите копировать/вставлять приведенные выше фрагменты кода, вы можете просто установить эту библиотеку: https://github.com/gnugat/nomo-spaco. Если вы используете PHP> = 5.5, вы также можете использовать следующую библиотеку: https://github.com/hanneskod/classtools.

Ответ 2

Обновление: так как этот ответ стал несколько популярным, я создал упаковочный пакет, чтобы упростить вещи. Он содержит в основном то, что я описал здесь, без необходимости добавлять класс самостоятельно или настраивать $appRoot вручную. Это может в конечном итоге поддерживать больше, чем просто PSR-4.

Этот пакет можно найти здесь: haydenpierce/class-finder.

$ composer require haydenpierce/class-finder

Смотрите больше информации в файле README.


Я не был доволен ни одним из решений здесь, поэтому я решил построить свой класс, чтобы справиться с этим. Это решение требует, чтобы вы были:

  • Использование Composer
  • Использование PSR-4

Короче говоря, этот класс пытается выяснить, где классы фактически живут в вашей файловой системе, на основе пространств имен, которые вы определили в composer.json. Например, классы, определенные в пространстве имен Backup\Test находятся в /home/hpierce/BackupApplicationRoot/src/Test. Этому можно доверять, потому что сопоставление структуры каталога с пространством имен требуется PSR-4:

Смежные имена подпространств имен после "префикса пространства имен" соответствуют подкаталогу в "базовом каталоге", в котором разделители пространства имен представляют разделители каталога. Имя подкаталога ДОЛЖНО совпадать с регистром имен под-пространства имен.

Возможно, вам придется настроить appRoot чтобы он указывал на каталог, содержащий composer.json.

<?php    
namespace Backup\Util;

class ClassFinder
{
    //This value should be the directory that contains composer.json
    const appRoot = __DIR__ . "/../../";

    public static function getClassesInNamespace($namespace)
    {
        $files = scandir(self::getNamespaceDirectory($namespace));

        $classes = array_map(function($file) use ($namespace){
            return $namespace . '\\' . str_replace('.php', '', $file);
        }, $files);

        return array_filter($classes, function($possibleClass){
            return class_exists($possibleClass);
        });
    }

    private static function getDefinedNamespaces()
    {
        $composerJsonPath = self::appRoot . 'composer.json';
        $composerConfig = json_decode(file_get_contents($composerJsonPath));

        //Apparently PHP doesn't like hyphens, so we use variable variables instead.
        $psr4 = "psr-4";
        return (array) $composerConfig->autoload->$psr4;
    }

    private static function getNamespaceDirectory($namespace)
    {
        $composerNamespaces = self::getDefinedNamespaces();

        $namespaceFragments = explode('\\', $namespace);
        $undefinedNamespaceFragments = [];

        while($namespaceFragments) {
            $possibleNamespace = implode('\\', $namespaceFragments) . '\\';

            if(array_key_exists($possibleNamespace, $composerNamespaces)){
                return realpath(self::appRoot . $composerNamespaces[$possibleNamespace] . implode('/', $undefinedNamespaceFragments));
            }

            array_unshift($undefinedNamespaceFragments, array_pop($namespaceFragments));            
        }

        return false;
    }
}

Ответ 3

Довольно интересно, что не существует никакого метода отражения, который делает это для вас. Однако я придумал небольшой класс, способный читать информацию о пространстве имен.

Чтобы сделать это, вам нужно пройти через все определенные классы. Затем мы получаем пространство имен этого класса и сохраняем его в массиве вместе с самим именем класса.

<?php

// ClassOne namespaces -> ClassOne
include 'ClassOne/ClassOne.php';

// ClassOne namespaces -> ClassTwo
include 'ClassTwo/ClassTwo.php';
include 'ClassTwo/ClassTwoNew.php';

// So now we have two namespaces defined 
// by ourselves (ClassOne -> contains 1 class, ClassTwo -> contains 2 classes)

class NameSpaceFinder {

    private $namespaceMap = [];
    private $defaultNamespace = 'global';

    public function __construct()
    {
        $this->traverseClasses();
    }

    private function getNameSpaceFromClass($class)
    {
        // Get the namespace of the given class via reflection.
        // The global namespace (for example PHP predefined ones)
        // will be returned as a string defined as a property ($defaultNamespace)
        // own namespaces will be returned as the namespace itself

        $reflection = new \ReflectionClass($class);
        return $reflection->getNameSpaceName() === '' 
                ? $this->defaultNamespace
                : $reflection->getNameSpaceName();
    }

    public function traverseClasses()
    {
        // Get all declared classes
        $classes = get_declared_classes();

        foreach($classes AS $class)
        {
            // Store the namespace of each class in the namespace map
            $namespace = $this->getNameSpaceFromClass($class);
            $this->namespaceMap[$namespace][] = $class;
        }
    }

    public function getNameSpaces()
    {
        return array_keys($this->namespaceMap);
    }

    public function getClassesOfNameSpace($namespace)
    {
        if(!isset($this->namespaceMap[$namespace]))
            throw new \InvalidArgumentException('The Namespace '. $namespace . ' does not exist');

        return $this->namespaceMap[$namespace];
    }

}

$finder = new NameSpaceFinder();
var_dump($finder->getClassesOfNameSpace('ClassTwo'));

Выход будет:

array(2) { [0]=> string(17) "ClassTwo\ClassTwo" [1]=> string(20) "ClassTwo\ClassTwoNew" }

Конечно, все, кроме класса NameSpaceFinder, если оно собрано быстро и грязно. Поэтому не стесняйтесь очищать беспорядок include, используя автозагрузку.

Ответ 4

Я думаю, что у многих людей может быть такая проблема, поэтому я решил использовать ответы от @hpierce и @loïc-faugeron для решения этой проблемы.

В классе, описанном ниже, вы можете иметь все классы в пространстве имен или соблюдать определенный термин.

<?php

namespace Backup\Util;

final class ClassFinder
{
    private static $composer = null;
    private static $classes  = [];

    public function __construct()
    {
        self::$composer = null;
        self::$classes  = [];

        self::$composer = require APP_PATH . '/vendor/autoload.php';

        if (false === empty(self::$composer)) {
            self::$classes  = array_keys(self::$composer->getClassMap());
        }
    }

    public function getClasses()
    {
        $allClasses = [];

        if (false === empty(self::$classes)) {
            foreach (self::$classes as $class) {
                $allClasses[] = '\\' . $class;
            }
        }

        return $allClasses;
    }

    public function getClassesByNamespace($namespace)
    {
        if (0 !== strpos($namespace, '\\')) {
            $namespace = '\\' . $namespace;
        }

        $termUpper = strtoupper($namespace);
        return array_filter($this->getClasses(), function($class) use ($termUpper) {
            $className = strtoupper($class);
            if (
                0 === strpos($className, $termUpper) and
                false === strpos($className, strtoupper('Abstract')) and
                false === strpos($className, strtoupper('Interface'))
            ){
                return $class;
            }
            return false;
        });
    }

    public function getClassesWithTerm($term)
    {
        $termUpper = strtoupper($term);
        return array_filter($this->getClasses(), function($class) use ($termUpper) {
            $className = strtoupper($class);
            if (
                false !== strpos($className, $termUpper) and
                false === strpos($className, strtoupper('Abstract')) and
                false === strpos($className, strtoupper('Interface'))
            ){
                return $class;
            }
            return false;
        });
    }
}

В этом случае вы должны использовать Composer для выполнения автозагрузки класса. Используя доступную на нем ClassMap, решение упрощается.

Ответ 5

Найти классы

Класс может быть локализован в файловой системе по имени и пространству имен, как это делает автозагрузчик. В нормальном случае пространство имен должно указывать относительный путь к файлам классов. Входящие пути являются отправными точками относительных путей. Функция get_include_path() возвращает список включенных путей в одну строку. Каждый включенный путь может быть протестирован, существует ли относительный путь, который соответствует пространству имен. Если найден подходящий путь, вы узнаете расположение файлов классов.

Получить имена классов

Как только местоположение файлов классов известно, классы могут быть извлечены из имен файлов, потому что имя файла класса должно состоять из имени класса, за которым следует .php.

Пример кода

Вот пример кода, чтобы получить все имена классов пространства имен foo\bar в виде массива строк:

$namespace = 'foo\bar';

// Relative namespace path
$namespaceRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $namespace);

// Include paths
$includePathStr = get_include_path();
$includePathArr = explode(PATH_SEPARATOR, $includePathStr);

// Iterate include paths
$classArr = array();
foreach ($includePathArr as $includePath) {
    $path = $includePath . DIRECTORY_SEPARATOR . $namespaceRelativePath;
    if (is_dir($path)) { // Does path exist?
        $dir = dir($path); // Dir handle     
        while (false !== ($item = $dir->read())) {  // Read next item in dir
            $matches = array();
            if (preg_match('/^(?<class>[^.].+)\.php$/', $item, $matches)) {
                $classArr[] = $matches['class'];
            }
        }
        $dir->close();
    }
}

// Debug output
var_dump($includePathArr);
var_dump($classArr);

Ответ 6

Я приведу пример, который фактически используется в нашем приложении Laravel 5, но его можно использовать почти везде. В примере возвращаются имена классов с пространством имён, которые можно легко извлечь, если это не требуется.

Легенда

  • {{1}} - Путь к удалению из текущего пути к файлу для доступа к папке приложения
  • {{2}} - путь к папке из папки приложения, в которой существуют целевые классы.
  • {{3}} - путь пространства имен

Код

$classPaths = glob(str_replace('{{1}}', '',__DIR__) .'{{2}}/*.php');
$classes = array();
$namespace = '{{3}}';
foreach ($classPaths as $classPath) {
    $segments = explode('/', $classPath);
    $segments = explode('\\', $segments[count($segments) - 1]);
    $classes[] = $namespace . $segments[count($segments) - 1];
}

Люди Laravel могут использовать app_path() . '/{{2}}/*.php' в glob().

Ответ 7

class_parents, spl_classes() и class_uses могут использоваться для извлечения всех имен классов

Ответ 8

Вы можете использовать get_declared_classes, но с небольшой дополнительной работой.

$needleNamespace = 'MyNamespace';
$classes = get_declared_classes();
$neededClasses = array_filter($classes, function($i) use ($needleNamespace) {
    return strpos($i, $needleNamespace) === 0;
});

Итак, сначала вы получите все объявленные классы, а затем проверьте, какие из них начинаются с вашего пространства имен.

Примечание: вы получите массив, где ключи не начинаются с 0. Чтобы достичь этого, вы можете попробовать: array_values($neededClasses);.

Ответ 9

Самый простой способ - использовать собственную функцию автозагрузчика __autoload, а внутри нее сохранить имена загруженных классов. Вам это подходит?

В противном случае, я думаю, вам придется иметь дело с некоторыми методами отражения.

Ответ 11

Я только что сделал нечто подобное, это относительно просто, но может быть построено из.

  public function find(array $excludes, ?string $needle = null)
  {
    $path = "../".__DIR__;
    $files = scandir($path);
    $c = count($files);
    $models = [];
    for($i=0; $i<$c; $i++) {
      if ($files[$i] == "." || $files[$i] == ".." || in_array($dir[$i], $excludes)) {
        continue;
      }
      $model = str_replace(".php","",$dir[$i]);
      if (ucfirst($string) == $model) {
        return $model;
      }
      $models[] = $model;
    }
    return $models;
  }

Ответ 12

Довольно много интересных ответов выше, некоторые на самом деле необычайно сложны для предлагаемой задачи.

Чтобы добавить различные возможности к возможностям, здесь есть быстрая и простая неоптимизированная функция, позволяющая делать то, что вы просите, с использованием самых простых приемов и общих утверждений, которые я мог придумать:

function classes_in_namespace($namespace) {
      $namespace .= '\\';
      $myClasses  = array_filter(get_declared_classes(), function($item) use ($namespace) { return substr($item, 0, strlen($namespace)) === $namespace; });
      $theClasses = [];
      foreach ($myClasses AS $class):
            $theParts = explode('\\', $class);
            $theClasses[] = end($theParts);
      endforeach;
      return $theClasses;
}

Используйте просто как:

$MyClasses = classes_in_namespace('namespace\sub\deep');

var_dump($MyClasses);

Я написал эту функцию, чтобы предположить, что вы не добавляете последнюю "косую черту" (\) в пространство имен, поэтому вам не придется удваивать ее, чтобы избежать. ;)

Обратите внимание, что эта функция является только примером и имеет много недостатков. На основании приведенного выше примера, если вы используете ' namespace\sub ' и существует ' namespace\sub\deep ', функция вернет все классы, найденные в обоих пространствах имен (ведя себя так, как если бы она была рекурсивной). Тем не менее, было бы просто настроить и расширить эту функцию для гораздо большего, чем обычно, требуя пару настроек в блоке foreach.

Возможно, это не вершина кода-ар-нуво, но, по крайней мере, он делает то, что было предложено, и должен быть достаточно простым, чтобы говорить само за себя.

Я надеюсь, что это поможет вам добиться того, что вы ищете.

Примечание: PHP 5 и 7 дружелюбны.

Ответ 13

Попробовав вышеописанные решения для композиторов, он не был удовлетворен временем, которое потребовалось для получения рекурсивных классов внутри пространства имен, до 3 секунд, но на некоторых машинах это занимало 6-7 секунд, что было недопустимо. Ниже класс отображает классы в ~ 0,05 в обычной структуре каталогов глубиной 3-4 уровня.

namespace Helpers;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

class ClassHelper
{
    public static function findRecursive(string $namespace): array
    {
        $namespacePath = self::translateNamespacePath($namespace);

        if ($namespacePath === '') {
            return [];
        }

        return self::searchClasses($namespace, $namespacePath);
    }

    protected static function translateNamespacePath(string $namespace): string
    {
        $rootPath = __DIR__ . DIRECTORY_SEPARATOR;

        $nsParts = explode('\\', $namespace);
        array_shift($nsParts);

        if (empty($nsParts)) {
            return '';
        }

        return realpath($rootPath. implode(DIRECTORY_SEPARATOR, $nsParts)) ?: '';
    }

    private static function searchClasses(string $namespace, string $namespacePath): array
    {
        $classes = [];

        /**
         * @var \RecursiveDirectoryIterator $iterator
         * @var \SplFileInfo $item
         */
        foreach ($iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($namespacePath, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        ) as $item) {
            if ($item->isDir()) {
                $nextPath = $iterator->current()->getPathname();
                $nextNamespace = $namespace . '\\' . $item->getFilename();
                $classes = array_merge($classes, self::searchClasses($nextNamespace, $nextPath));
                continue;
            }
            if ($item->isFile() && $item->getExtension() === 'php') {
                $class = $namespace . '\\' . $item->getBasename('.php');
                if (!class_exists($class)) {
                    continue;
                }
                $classes[] = $class;
            }
        }

        return $classes;
    }
}

Использование:

    $classes = ClassHelper::findRecursive(__NAMESPACE__);
    print_r($classes);

Результат:

Array
(
    [0] => Helpers\Dir\Getters\Bar
    [1] => Helpers\Dir\Getters\Foo\Bar
    [2] => Helpers\DirSame\Getters\Foo\Cru
    [3] => Helpers\DirSame\Modifiers\Foo\Biz
    [4] => Helpers\DirSame\Modifiers\Too\Taz
    [5] => Helpers\DirOther\Modifiers\Boo
)