Composer autoload always comes first - php

I'm using composer in a legacy project that have lots of classes with no namespace. Refactoring is not an option (it's a very huge application), but all new modules are fully psr-4 compliant. The legacy code has it's own autoload method (using a class map, pretty efficient).
My problem is: no matter in which order I add the autoloader methods, composer autoloader ALWAYS COMES FIRST! This is slowing down the load of each single class: every time I call a class from the legacy code, it first tries to match it against all composer autoload options (including the findFileWithExtension() ), and only then it calls the alternative autoloader.
I've inspected it using PHPStorm+XDebug, and no matter which autoloader I include first, Composer autoload is always called before the legacy one.
Is there a way to change this behaviour??
Thanks in advance!!

Ok, guys, I found the solution and want to share it with you: the spl_autoload_register() function has a third parameter: $prepend. When set to true, it will prepend the autoload function to the autoload queue, instead of appending it (it's actually documented at the official PHP Documentation). Composer always sets it to true, so that its autoloader is always called first. To fix it, I changed the legacy autoloader, setting $prepend to true, and called it AFTER including composer's autoload.
Hope it helps someone! :)

Pass true as a third argument to the spl_autoload_register:
spl_autoload_register(your_autoload_func(), true, true);

You need to be aware that composer use different ways to include php files, check vendor/composer/autoload_real.php for details, and remember that composer can include files directly in the place where you include
require_once('vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
so if you are lucky to have vendor/composer/autoload_static.php remember that adding $prepend parameter to your own spl_autoload_register() may not be enough.

Related

Autoload files for PHP Package - get_declared_classes() not returning expected results

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?
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.
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.
Compare "Inspecting code and dependencies of a composer-based project"

Override vendor autoload composer

Is there a way to have an autoload file you created run before the vendor autoload is called? We seem to be running into an issue with SimpleSAML's autoload overriding one of the autoload files we created. I am new to Composer, and couldn't seem to find any solutions online.
I tried including our autoload file in the file that needs it as well and that still did not work.
A workaround is to just include the files explicitly, but being able to use the autoloader would be preferred.
Yes, you can register an autoloader and prepend it to the queue, for example:
spl_autoload_register(function($class) {
// ...
}, true, true);
The last parameter (true) will prepend this autoloader into the queue, so, it'll be called at first and to do that, you've to register your autoloader at the very early of your script, maybe just right after you include the vendor autoloader. Read more here.

How to check if composer's autoloader is being used?

I'm developing a certain PHP framework for WordPress, and I want to give my users the option to use composer to install it as a package, or to install it manually. If composer is used, then a psr-4 class autoloader handles everything. If not, then all files must be loaded manually during the framework's bootstrapping process.
Is there a safe way to check whether composer's autoloader is being used in a given WordPress environment?
Currently i'm using:
if( !class_exists('Composer\\Autoload\\ClassLoader') )
{
// Manually include files if composer is not used.
require_once 'some/files.php';
}
However, if in a given WordPress environment there is a plugin that uses composer internally, then the above if statement will return true even though other plugins have no access to it.
The solution, as it turns out, is quite simple. You need to create 2 different bootstrapping files, say manual-bootstrap.php and composer-bootstrap.php. Then add the following lines to composer.json:
"autoload": {
"files": ["composer-bootstrap.php"]
}
composer-bootstrap.php will only be called if composer is used to load the framework as a dependency. Users that want to load the framework manually will use manual-bootstrap.php instead.
if(class_exists("\Composer\Autoload\ClassLoader"))
{
// a composer autoload.php has already been included/required
}

composer with files not using psr0 and psr4 autoloading (symfony project)

I have a problem with composer auto loader. Currently working on a application which was developed about 10 years back. The folder structure of 2 libraries which are currently used in the project do not comply the psr0 and psr4 auto loading rules.
The structure after the composer install looks as follows
Example 1
Folder path: /vendor/AppBook/ORS/class/model/Country
Filename: class.Country.php
PHP Class name: Country
Example 2
Folder path: /vendor/AppBook/ORS/class/model/Country
Filename: class.CountryCollection.php
PHP Class name: CountryCollection
Please advise what should I do in order that composer auto loader can detect these files.
Thank you in advance
From the docs:
You can use the classmap generation support to define autoloading for all libraries that do not follow PSR-0/4. To configure this you specify all directories or files to search for classes.
Example:
{
"autoload": {
"classmap": ["src/", "lib/", "Something.php"]
}
}
You can still add composer.json to your legacy libraries and define classmap autoloading type for them.
You can rename these files to make them compatible to PSR-4 (unlikely because that requires using namespaces - in 10 years old code?) or PSR-0. Additionally, you have to remove any explicit loading of these files via include, include_once, require or require_once because the file names changed.
PHP will autoload these classes by their class name. This will possibly run into issues if the case sensitivity in the class name is not respected everywhere. Example:
class UpperCase {}
$a = new upperCase();
The autoloading would try to find a file ending with upperCase.php, which will not match the PSR-0 required UpperCase.php, so the code will fail. However, this will work, making the situation not better:
class UpperCase {}
$b = new UpperCase();
$a = new upperCase();
The reason is that PHP treats class names case insensitive, so once a class is loaded, you can use any case in it's name. It is only the first occurrence in your code path that has to match. The problem is to be sure where this really is, so essentially it has to be correct everywhere.
Yes, the classmap feature is the easier way. But you'd still want to remove include/require calls to optimize the performance a bit, so you have to touch the code anyway. And despite it's age, it has to be maintained - so why not do it fully, switching to a well-known autoloading standard. It will help you in the long run when you have to maintain PSR-0/4 compatible classes and this old code in parallel.

Issue with PSR-0 autoloading

I am trying to use this composer package with a new project I am working on https://packagist.org/packages/activecollab/activecollab-sdk. However when i try and create a new class I keep getting the following errors.
Fatal error: Class 'ActiveCollab\Client' not found
The file that is throwing this error looks like this.
require "vendor/autoload.php";
new ActiveCollab\Client;
Which is just being used to test if the files are being loaded in properly. The composer.json of the file which I am trying to use looks like such. And I have a feeling the problem is in this file but I can't figure out what.
stuff...
"autoload": {
"psr-0": {
"ActiveCollab\\": "ActiveCollab"
}
}
...stuff
Also looking at the autload_namespaces.php file it is being generated as such.
<?php
// autoload_namespaces.php #generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'ActiveCollab' => array($vendorDir . '/activecollab/activecollab-sdk/ActiveCollab'),
);
I have used psr-0 in some composer packages of my own and everything looks to be right except maybe the camel case in the namespace but i don't see this as being disallowed in the php proposal for psr-0.
Thanks for any help this has been driving me crazy.
The thing is: You cannot simply add a composer.json file with a random autoloading configuration and hope that it works - it actually has to match the naming scheme you are using. That is what this project got wrong, and nobody tested it. Which probably means nobody uses this library, and you can expect no support from the creators due to lack of interest.
But let's see how they react on my pull request to get things back to working again.
The composer config looks fine: Is it just the case that you omitted the leading \ from your class name?
new \ActiveCollab\Client;
You'll need that if your code is inside another namespace, as it will load it relative to the current namespace.
EDIT: I've just checked out that library, and even with the above fix, the autoloader wasn't quite working. The autoloader may also be broken due to the composer.json file for the library specifying a PSR0 autoloader, but using ".class.php" extensions (not PSR0 compatible). An autoload.php file is included with the library, so if you just require that file, you should be able to use the classes:
require 'vendor/activecollab/activecollab-sdk/ActiveCollab/autoload.php';
After doing this, I was able to use the class.

Categories