Back in 2007, when the main body of work on the Magento core was being done, Autoloading still was a rather new concept for PHP.
The idea that more then a single active autoloader would be necessary seemed far fetched.
Fast forward to present day. Almost all libraries use an autoloader. The "PHP Framework Interoperability Group" (FIG) has released the autoloader standard PSR-0 years ago. Almost all polar libraries conform to PSR-0 today. The Magento autoloader also conforms to this standard.
So whats the problem?
What does an autoloader do?
- Map a PHP class name to a file
- Include that file
That is exactly what the Varien_Autoload::autoload() method does. But, as so often, the problem lies in the implementation details. After mapping the class name to a file name, the Magento autoloader doesn't check if the file actually exists, it simply calls include $classFile;.
If the file is missing, this will trigger a Warning.
Warning: include(The/Class/To/Be/Loaded.php): failed to open stream: No such file or directory
The problem about this becomes obvious considering when autoloading is used: whenever a class name is referred to that isn't known to PHP. This typically happens during class instantiation, loading parent classes, or when using class constants. But it also is used when the PHP function class_exists() is called.
bool class_exists( string $class_name[, bool $autoload = true] );
And it is this last use case that creates an issue. The Magento core code only calls class_exists() with the second parameter set to false , or within a try/catch block.
But some libraries use class_exists() to check which library components are installed.
They don't expect a Warning to be triggered during autoloading. One example for that is PHPUnit_Util_GlobalState , which is used by PHPUnit_TextUI_ResultPrinter while displaying test results. If the PHPUnit extensions phpunit/DbUnit, phpunit/PHPUnit_Selenium or phpunit/PHPUnit_Story are not present, breakage occurs:
Warning: include(): Failed opening 'PHPUnit/Extensions/Story/TestCase.php for inclusion
Often autoloader issues are resolved by setting the third argument to spl_autoload_register to true, which causes the autoloader to be prepended to the list of existing autoloaders.
But simply prepending the library autoloader before the Magento autoloader doesn't resolve the issue, since class_exists should be safe to use without warnings or exceptions, even in the case the class doesn't exist.
I mean, if we know it exists, why use class_exist in the first place, right?
If a class_exist is used with a class that doesn't exist, the Magento autoloader would still be triggered and thus issue the include warning.
To summarize:
Autoloaders should not trigger errors or exceptions if the class file does not exist.
This was even included as a requirement in the recently released PSR-4 recommendation (in section 2.4):
Autoloader implementations MUST NOT throw exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value.
From Warning to Exception
Whats worse, the problem is emphasized by the Magento error handler.
If developer mode is enabled, the error handler will throw an exception on any Warning or Error!
if (Mage::getIsDeveloperMode()) {
throw new Exception($errorMessage);
} else {
Mage::log($errorMessage, Zend_Log::ERR);
}
Instead of a Warning, the result now is a fatal exception:
Fatal error: Uncaught exception 'Exception' with message 'Warning: include(PHPUnit/Extensions/Story/TestCase.php): failed to open stream: No such file or directory
No more PHPUnit test results! :(
Solutions (umm... workarounds)
The lame solutions would be to either disable the developer mode (really?!?), or install all required library extensions. The proper solution would be to patch Varien_Autoload::autoload to check if a file exists before attempting to include it.
if (stream_resolve_include_path($classFile)) {
return include $classFile;
} else {
return false;
}
Fixing the Autoloader
Changing the file lib/Varien/Autoload.php is no option, because it would be overwritten during upgrades.
This leaves us with having to choose the least ugly approach how to get rid of the include Warnings and Exceptions.
Include Path Hack
The usual approach to amend the Magento autoloader is to copy the autoloader to app/code/community/Varien/Autoload.php . This works, but since a number of great extensions already use that approach, this might lead to conflicts (for example the excellent Aoe_ClassPathCache).
Event Observer
Alternatively the autoloader can be changed in an event observer.
Arguably the best event for that purpose is resource_get_tablename (in scope), because it is the first event dispatched during the Magento initialization. Also, it is dispatched regardless if Magento is processing a browser request, a cron job or a custom shell script.
On the downside, the event is dispatched very often. So the observer has to be smart enough only to run it's business logic once, to keep the overhead as low as possible.
This approach is also used by some extensions to modify the autoloader, for example the Magento-PSR-0-Autoloader.
Hack: using reflection to unset
Mage_Core_Model_App::_events['global']['resource_get_tablename']['observers']['your_observer']
is very ugly but works great - if you're into such things :)
In a bootstrap script
Maybe the autoloader exceptions only are an issue in external scripts (like PHPUnit), and you would like to keep things simple. In cases such as this it might be enough to wrap the autoloader in a closure inside a bootstrap script. The following will negate autoloader include exceptions when added to a script after including app/Mage.php .
spl_autoload_unregister(array(Varien_Autoload::instance(), 'autoload'));
spl_autoload_register(function($class) {
try {
return Varien_Autoload::instance()->autoload($class);
} catch (Exception $e) {
if (false !== strpos($e->getMessage(), 'Warning: include(')) {
return null;
} else {
throw $e;
}
}
Changing the Error Handler
Instead of fixing the autoloader, it also works to change the error handler to swallow include warnings from Varien_Autoload.
// Get the error handler by pushing a dummy handler on the stack.
// Then, set the real handler wrapping the original.
$mageHandler = set_error_handler(function () {});
set_error_handler(function ($errno, $errstr, $errfile, $errline) use ($mageHandler) {
if (E_WARNING === $errno
&& 0 === strpos($errstr, 'include(')
&& substr($errfile, -19) == 'Varien/Autoload.php'
){
return null;
}
return call_user_func($mageHandler, $errno, $errstr, $errfile,$errline);
});
I prefer this approach simply because it targets the exact issue, and doesn't cause issues when Varien_Autoload already has been altered.
Summary
Currently there is no perfect way to changing the Magento 1 autoloader. Each workaround works, even if all of them are rather hackish. It depends on your personal preferences which one to choose, and on how and where external libraries are used in a Magento project.
Until Magento 2 arrives we will have to make do with such interim solutions. In the mean time, lets hack on and enjoy finding creative solutions :)
We might miss them in the time to come, when everything can be done in a standard way!