web-dev-qa-db-fra.com

PHP - obtenir tous les noms de classes dans un espace de noms particulier

Je veux obtenir toutes les classes dans un espace de noms. J'ai quelque chose comme ça:

#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.
    }
}

J'ai essayé get_declared_classes() dans getAllClasses() mais MyClass1 et MyClass2 n'étaient pas dans la liste.

Comment pourrais-je faire ça?

40
Pedram Behroozi

L'approche générique consisterait à obtenir tous les noms de classe pleinement qualifiés (classe avec un espace de noms complet) dans votre projet, puis à les filtrer en fonction de l'espace de noms souhaité.

PHP offre quelques fonctions natives pour obtenir ces classes (get_declared_classes, etc.), mais ils ne pourront pas trouver de classes qui n’ont pas été chargées (include/require). Par conséquent, cela ne fonctionnera pas comme prévu avec des autoloaders (comme Composer pour exemple) . Il s’agit d’un problème majeur, car l’utilisation des autochargeurs est très courante.

Donc, votre dernier recours est de trouver tous les fichiers PHP par vous-même et de les analyser pour extraire leur espace de noms et leur classe:

$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;
        }
    }
}

Si vous suivez les normes PSR 0 ou PSR 4 (votre arborescence de répertoires reflète votre espace de noms), vous n'avez rien à filtrer: indiquez simplement le chemin qui correspond à l'espace de noms souhaité.

Si vous n'êtes pas amateur de copier/coller les extraits de code ci-dessus, vous pouvez simplement installer cette bibliothèque: https://github.com/gnugat/nomo-spaco . Si vous utilisez PHP> = 5.5, vous pouvez également utiliser la bibliothèque suivante: https://github.com/hanneskod/classtools .

24
Loïc Faugeron

Update: Depuis que cette réponse est devenue un peu populaire, j'ai créé un paquetage pour simplifier les choses. Il contient essentiellement ce que j'ai décrit ici, sans qu'il soit nécessaire d'ajouter la classe vous-même ou de configurer le $appRoot manuellement. Il pourrait éventuellement prendre en charge plus que le PSR-4.

Ce paquet peut être trouvé ici: haydenpierce/class-Finder .

$ composer require haydenpierce/class-Finder

Voir plus d'informations dans le fichier README.


Je n’étais satisfait d’aucune des solutions proposées ici et j’ai donc construit ma classe pour gérer cela. Cette solution nécessite que vous soyez:

  • Utiliser Composer 
  • Utilisation du PSR-4

En un mot, cette classe tente de déterminer l’emplacement réel des classes sur votre système de fichiers en fonction des espaces de noms que vous avez définis dans composer.json. Par exemple, les classes définies dans l'espace de noms Backup\Test se trouvent dans /home/hpierce/BackupApplicationRoot/src/Test. Ceci est fiable car la correspondance d'une structure de répertoire avec un espace de noms est requis par PSR-4

Noms d'espaces de noms contigus après le "préfixe d'espace de noms" correspondent à un sous-répertoire dans un "répertoire de base", dans lequel le Les séparateurs d'espaces de noms représentent des séparateurs de répertoires. Le sous-répertoire name DOIT correspondre à la casse des noms de sous-espaces de noms.

Vous devrez peut-être ajuster appRoot pour qu'il pointe vers le répertoire qui contient 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;
    }
}
17
HPierce

Je pense que beaucoup de gens pourraient avoir un problème comme celui-ci, je me suis donc fié aux réponses de @hpierce et @ loïc-faugeron pour résoudre ce problème.

Avec la classe décrite ci-dessous, vous pouvez avoir toutes les classes d'un espace de noms ou elles respectent un certain terme.

<?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;
        });
    }
}

Dans ce cas, vous devez utiliser Composer pour effectuer le chargement automatique des classes. En utilisant le ClassMap disponible sur celui-ci, la solution est simplifiée.

3

Localiser les cours

Une classe peut être localisée dans le système de fichiers par son nom et son espace de noms, comme le fait l'autoloader. Dans le cas normal, l'espace de noms devrait indiquer le chemin relatif aux fichiers de classe. Les chemins d'inclusion sont les points de départ des chemins relatifs. La fonction get_include_path() renvoie une liste de chemins d’inclusion dans une chaîne. Chaque chemin d’inclusion peut être testé, s’il existe un chemin relatif qui correspond à l’espace de nom. Si le chemin correspondant est trouvé, vous connaîtrez l'emplacement des fichiers de classe.

Obtenir les noms de classe

Dès que l'emplacement des fichiers de classe est connu, les classes peuvent être extraites des noms de fichiers, car le nom d'un fichier de classe doit comprendre le nom de la classe suivi de .php.

Exemple de code

Voici un exemple de code permettant d'obtenir tous les noms de classe de l'espace de nom foo\bar sous forme de tableau de chaînes:

$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);
2
Henrik

Je vais donner un exemple qui est en fait utilisé dans notre application Laravel 5 mais peut être utilisé presque partout. L'exemple retourne les noms de classe avec l'espace de noms qui peuvent être facilement retirés, si non requis. 

Légende

  • {{1}} - Chemin à supprimer du fichier actuel pour accéder au dossier de l'application
  • {{2}} - Le chemin du dossier de l'application dans lequel les classes cibles existent
  • {{3}} - Chemin d'accès à l'espace de noms

Code

$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];
}

Les gens de Laravel peuvent utiliser app_path() . '/{{2}}/*.php' dans glob ().

2
Umair Ahmed

class_parents, spl_classes() et class_uses peuvent être utilisés pour récupérer tous les noms de classe

1
Harsh Chunara

pour symfony, vous pouvez utiliser le composant Finder:

http://symfony.com/doc/current/components/Finder.html

0
Sebastian Viereck

Le moyen le plus simple devrait être d’utiliser votre propre fonction __autoload d’autoloader et d’enregistrer les noms des classes chargées. Cela vous convient-il?

Sinon, je pense que vous devrez faire face à certaines méthodes de réflexion.

0
Uriziel

Vous pouvez utiliser get_declared_classes mais avec un peu de travail supplémentaire.

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

Donc, vous obtenez d’abord toutes les classes déclarées, puis cochez laquelle des deux commence par votre espace de noms.

Remarque : vous obtiendrez un tableau où les touches ne commencent pas par 0. Pour y parvenir, vous pouvez essayer: array_values($neededClasses);.

0
FreeLightman