For testing/debugging/evaluation purposes I want to write wrappers for selected entries of the PHP autoloader chain. As autoloader methods are restricted to a single argument with a given value (the class name) the only way to tell the wrapper what to do seems to be the method name. I thus iterate over spl_autoload_functions(), replace the selected items with dynamically created calls of methods like Wrapper::handler_xxxx and save the original information in that class. Wrapper has a method public static function __callStatic() which should evaluate the incoming requests, do its testing/debugging/evaluating work and otherwise redirect to the (saved) original handler. However, this does not work but leads to errors like this one:
PHP Fatal error: Uncaught LogicException: Passed array does not specify an existing method (class 'Wrapper' does not have a method 'autoload_mdGVzdG1ldGhvZAo') in [somewhere]
Seems that the autoloader needs real methods and does not work with PHP-magic. Whether this is a bug or a feature (if the latter: why?), is there any way to work around it?
Edit: some code example upon request, I am not sure if this helps understanding the problem; this is by far not finished, but just an attempt to get the basic idea up and running. Calling the magic method manually (by the name given above) works fine - it is just that the autoloader-logic is not willing to call it.
class Wrapper {
const AUTOLOAD_PREFIX = 'autoload';
public static function __callStatic(String $name, Array $arguments) {
if (strpos($name, self::AUTOLOAD_PREFIX) === 0) {
try {
# valid signature found
if (preg_match(sprintf('/^%s(_c([^_]+))?_m([^_]+)$/', self::AUTOLOAD_PREFIX), $name, $matches)) {
# call via class/method
$method = self::decodeMethodName($matches[3]);
if ($matches[2]) {
$class = self::decodeMethodName($matches[2]);
$class::$method($arguments);
}
else {
# call global function
$method($arguments);
}
}
else {
# invalid name
throw new \Exception(sprintf('Invalid static call of %s::%s', __CLASS__, $name));
}
}
catch (\Throwable $e) {
echo "I got you.";
die;
}
}
else {
throw new \Exception(sprintf('Invalid static call of %s::%s', __CLASS__, $name));
}
return;
}
private static function encodeMethodName(String $method) : String {
# protect and encode method names
return str_replace(array('+', '/', '='), array(0x81, 0x82, ''), base64_encode($method));
}
private static function decodeMethodName(String $method) : String {
# reconstruct method names
return base64_decode(str_replace(array(0x80, 0x81), array('+', '/'), $method));
}
public function protectAutoloader(?String $originalClass, String $originalMethod) : Void {
$stack = array();
$autoLoaders = spl_autoload_functions();
while (!$done && count($autoLoaders) > 0) {
$item = array_pop($autoLoaders);
if (
is_string($item) &&
is_null($originalClass) &&
($originalMethod === $item)
) {
# global function
$replacement = array(
__CLASS__,
sprintf('%s_m%s', self::AUTOLOAD_PREFIX, self::encodeMethodName($item)),
);
$done = true;
}
elseif (
is_array($item) &&
($item[0] === $originalClass) &&
($item[1] === $originalMethod)
) {
# static method
$replacement = array(
__CLASS__,
sprintf('%s_c%s_m%s', self::AUTOLOAD_PREFIX, self::encodeMethodName($item[0]), self::encodeMethodName($item[1])),
);
$done = true;
}
else {
# don't touch anything else (closures)
$replacement = $item;
}
# remove item and push to the stack
spl_autoload_unregister($item);
array_push($stack, $replacement);
}
# restore autoloader chain
while (count($stack) > 0) {
$item = array_pop($stack);
spl_autoload_register($item, true);
}
return;
}
I'd activate this for a specific autoloader:
$wrapper->protectAutoloader(NULL, 'testmethod');
Now testmethod() (if existing in the chain) will be replaced by Wrapper::autoload_mdGVzdG1ldGhvZAo() and if e.g. a syntactically broken file were autoloaded the message "I got you" would be printed.