PHP autoloader pointing to a dynamic method name

Viewed 24

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.

0 Answers
Related