goaop / framework

:gem: Go! AOP PHP - modern aspect-oriented framework for the new level of software development
go.aopphp.com
MIT License
1.66k stars 163 forks source link

Having control on the AopComposerLoader #137

Closed bgaillard closed 10 years ago

bgaillard commented 10 years ago

Hi, I'm currently integrating Go AOP in one of our projects.

After few hours I succeeded to create my pointcuts but now I encounter a class loading problem which makes Go Aop load lot of classes (and cache them) using its own class loader.

It seems my problems appears because of the following line in the AopComposerLoader class.

AopComposerLoader::init();

... 

public static function init()
{
    $loaders = spl_autoload_functions();

    foreach ($loaders as &$loader) {
        $loaderToUnregister = $loader;
        if (is_array($loader) && ($loader[0] instanceof ClassLoader)) {
            $originalLoader = $loader[0];

            // Configure library loader for doctrine annotation loader
            AnnotationRegistry::registerLoader(function($class) use ($originalLoader) {
                $originalLoader->loadClass($class);
                return class_exists($class, false);
            });
            $loader[0] = new AopComposerLoader($loader[0]);
        }
        spl_autoload_unregister($loaderToUnregister);
    }

    unset($loader);

    foreach ($loaders as $loader) {
        spl_autoload_register($loader);
    }
}

This function is called automatically because the following instructions are placed in the composer.json file (see https://getcomposer.org/doc/04-schema.md#files).

"autoload": {
    "psr-0": {
        "Go": "src/"
    },
    "files": ["src/Go/Instrument/ClassLoading/AopComposerLoader.php"]
},

The AopComposerLoader is a wrapper around the standard Composer autoloader so Go AOP generates a cache directory which is huge and contains almost all the classes used by my ZF2 application.

To prevent this I've implemented the following function to make the Go AOP autoloader only load and cache the classes which are "weaved" :


private function initAopAutoloader() {

    $loaders = spl_autoload_functions();

    foreach ($loaders as &$loader) {

        if (is_array($loader) && ($loader[0] instanceof AopComposerLoader)) {

            $reflectionMethod = new \ReflectionProperty($loader[0], 'original');
            $reflectionMethod -> setAccessible(true);
            $composerAutoloader = $reflectionMethod -> getValue($loader[0]);
            $reflectionMethod -> setAccessible(false);

            // Unregister the Go AOP standard autoloader
            spl_autoload_unregister($loader);

            // Re-register the standard composer autoloader
            spl_autoload_register(array($composerAutoloader, 'loadClass'));

            // Re-create a Go AOP autoloader which wraps a composer autoloader used to load only the classes which
            // are "weaved"
            $classLoader = new ClassLoader();
            $classLoader -> addClassMap(array(
                'Application\\Service\\Impl\\StatisticReportService' => __DIR__ . '/src/Application/Service/Impl/StatisticReportService.php'
            ));
            $aopAutoloader = new AopComposerLoader($classLoader);
            spl_autoload_register(array($aopAutoloader, 'loadClass'));

        }

    }

}

Here I'm canceling what's automatically performed by the framework to manually configure a new Go AOP Autoloader.

Is their a better solution to do that ?

If not I think it would be prefarable to remove the AopComposerLoader::init(); instruction and ask developers to manually call it instead.

What do you think about this modification ? What are the best practices to apply with Go AOP and projects having lot of classes autoloaded by Composer ?

Thanks.

lisachenko commented 10 years ago

Hi! Thank you for choosing my framework :)

I have many ideas about usage of autoloading, but none of them can give me a fast and reliable solution to implement. So, your issue is perfectly valid and should be discussed.

BTW, latest version of framework doesn't use files section in the autoload section in composer.json: https://github.com/lisachenko/go-aop-php/commit/db5665de57de3ce4d4a47768d8bdff588fd5aba5. AopComposerLoader is only initialized within AOP kernel and doesn't affect execution flow of application when kernel is not loaded, this was my requirement for transparent AOP.

Main issue with userland AOP is a fact that it can be easily broken. To do the job, AOP should always use cached version of files to skip analysis phase. No cached file = no knowledge about class structure => token analysis, source code adjustment, etc => poor performance. Library carefully looks for include|require|require_once|include_once keywords to replace paths with filtered version and tries to do this as fast as possible. But when cache is ready, everything is working normally. To solve this issue with slow cache generation, I have added a cache warmer command that can be used to prepare a cache for application before first run or before deploying to the production environment.

Additionally, I want to extend the logic of prebuiltCache option to create an additional class loader that will not replace an original composer class, but prepend to it, so it will check the presence of woven class and can load it, preventing the load of original class with composer. For the case when woven class is not present, standard composer loader will load it.

lisachenko commented 10 years ago

Closed as inactive. Feel free to reopen if needed.