skip to Main Content

I have a package I am creating here

It’s a standard composer PHP package with a Symfony command to generate Avro files.

When registering the package command in the bin/avro directory I add the following

require file_exists(__DIR__ . '/../vendor/autoload.php')
        ? __DIR__ . '/../vendor/autoload.php'
        : __DIR__ . '/../../../../vendor/autoload.php';

This should, if my understanding is correct, autoload all files for a project where the package is loaded in.

I run the following in an empty Laravel project for example

composer require lukecurtis93/avrogenerate
./vendor/bin/avro generate

The code here which uses get_declared_classes() – excerpt:

// ...
        $arr = [];
        foreach (get_declared_classes() as $className) {
            if (in_array(Avroable::class, class_implements($className))) {
                $arr[] = $className;
            }
        }
// ...

– does not return any files stored in my App/Models directory for example which are in the Laravel App (or any others for that matter).

What am I doing incorrectly? Shouldn’t these files be available from get_declared_classes()? Is there additional configuration I need to do for a package?

2

Answers


  1. This should, if my understanding is correct, autoload all files for a project where the package is loaded in.

    You have misunderstood what "autoload" means in this context. It doesn’t mean "automatically find and load all classes at once"; it means "automatically load classes on demand".

    There is a description of how this works in the official manual. The short version is that whenever you try to use a class that isn’t defined yet, PHP passes the class name to a callback; that callback can do whatever it likes to define that class, normally loading a file based on some naming convention.

    So, until you’ve tried to use a class, it will remain undefined, and get_declared_classes will not know about it.

    The class_exists function will, by default, trigger the autoloader, which might be helpful to you. If you really need to dynamically list all the classes in a package, you’ll need to find some other way of iterating the files on disk.

    Login or Signup to reply.
  2. Is there additional configuration I need to do for a package?

    IMSoP already explained the details for PHP autoload when you want to use get_declared_classes().

    An alternative to that is to re-use the Composer classmap, however it requires to generate it, the --optimize argument of composer dump-autoload does this. Given the Composer Autloader has been dumped that way, the list of "declared" classes is quite easy to obtain:

    $autoload = require file_exists(__DIR__ . '/../vendor/autoload.php')
            ? __DIR__ . '/../vendor/autoload.php'
            : __DIR__ . '/../../../../vendor/autoload.php';
    $classmap = $autoload->getClassMap();
    

    Then $classmap is an associative array with the class-names as key and the (absolute) path (w/ relative segments) to the file.

    This is with the downside that the autoloader must be dumped with a specific configuration. And with the benefit that you’d be ready in no time.

    Better Reflection

    The package roave/better-reflection provides a class-listing for composer.json and //vendor-dir/composer/installed.json without relying on the class-map of the Composer autoloader.

    You could add it as a requirement and then it would be available with the autoloader.

    Getting the list of classes is within their documentation1, it depends on the path to the project. You could make it an argument option for your utility as I don’t know how to obtain the root project path from the autoloader instance, the following would assume the default vendor-dir configuration, YMMV:

    $projectAutoload = file_exists(__DIR__ . '/../vendor/autoload.php')
            ? __DIR__ . '/../vendor/autoload.php'
            : __DIR__ . '/../../../../vendor/autoload.php';
    $autload = require $projectAutoload;
    $projectRoot = dirname($projectAutload, 2);
    

    It does not implement 100% of the Composer autoloader configuration, but I’ve not seen another component that does apart from Composer itself. Just FYI when you wonder in integration testing, it is in the 99.5% camp, you may not even miss anything at all in your use-case.

    $astLocator = (new BetterReflection())->astLocator();
    $reflector  = new DefaultReflector(new AggregateSourceLocator([
        (new MakeLocatorForComposerJsonAndInstalledJson)($projectRoot, $astLocator),
        new PhpInternalSourceLocator($astLocator, new ReflectionSourceStubber())
    ]));
    
    $classes = $reflector->reflectAllClasses();
    $classNames = array_map(fn (ReflectionClass $rfl) => $rfl->getName(), $classes);
    

    Better Reflection also is not the fastest horse in the stable. Have decent CPU and fast disk I/O. The benefit of it are its features and in your specific case you can use it for preview (get_declared_classes() stays empty, no autoloading involved, you may use the gathered information to do your inheritance checks without loading the files into PHP – I can imagine this is probably good to have for your utility).

    Write it yourself

    It is relatively easy to write an ad-hoc loading of all PHP files in the vendor folder so that get_declared_classes() has them afterwards. But with such a direct implementation you can easily run into fatal errors, which don’t help with your cause, also if you want to adhere to the composer packages, you need to give it some love. That depends also on which packages (and package style) you want to support. I’ve not much about a clue of your project, so in case you may still want to try it for yourself, some pointers:

    • The Composer autoloader configuration is in their schema, additional information is available in the optimizing section for auto-loading.
    • Remember that a call of require or include returns the autoloader instance. The class it not @internal, which means the public interface is stable to use.
    • The other thing good to know is that in //vendor-dir/composer/installed.json you find a list of all installed packages and some details about the dev install.
    • The format/layout of the installed.json depends on the composer version, good to know if you want to support all Composer versions.
    • I’m not aware there is a composer configuration matrix / change-log over all versions. Make your own research.
    • There are four kind of autoloaders in Composer: psr-0, psr-4, classmap and files (this is documented).
    • File extensions are *.php and *.inc (this is documented).
    • classmap can have excludes, with * and ** globs, the later implicitly appended if no globs in the exclude option argument (this is documented).
    • Composer also contains the class scanner for class map creation. IIRC it is internal but looked relatively easy to extract as it is merely ~60 lines of straight forward code, most of it regex operations. The regexes and processing around looked quite battle-proven the times I checked. You may need this if you want to come close to --optimize / class-map generation as Composer does.
    • Expect to stumble over Symfony or Phpunit files occasionally.

    1. Compare "Inspecting code and dependencies of a composer-based project"
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search