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

sf2 integration issue #212

Closed AlexKovalevych closed 9 years ago

AlexKovalevych commented 9 years ago

AppAspectKernel.php:

<?php

use Demo\Aspect\SecurityAspect;
use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

/**
 * Application Aspect Kernel
 */
class AppAspectKernel extends AspectKernel
{
    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new SecurityAspect());
    }
}

loading kernel in the web/app.php:

...
require_once __DIR__.'/../app/AppAspectKernel.php';
$applicationAspectKernel = AppAspectKernel::getInstance();
$applicationAspectKernel->init([
    'debug' => true,
    'cacheDir'  => __DIR__ . '/../app/cache/aspect/',
    'appDir' => __DIR__ . '/../',
    'includePaths' => [__DIR__ . '/../src/'],
]);
...

the aspect itself:

<?php

namespace Demo\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Before;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Security aspect
 */
class SecurityAspect implements Aspect
{
    /**
     * Method that will be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("@execution(Demo\Aspect\Annotation\Securable)")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        $controller = $invocation->getThis();
        $user = $controller->getUser();
        $role = $invocation->getMethod()->getAnnotation('Demo\Aspect\Annotation\Securable')->role;
        if (!$user->hasAccessToPage($role)) {
            throw new AccessDeniedException();
        }
    }
}

getting error:

2015/04/03 15:07:59 [error] 11226#0: *6454 FastCGI sent in stderr: "PHP message: PHP Fatal error:  Class 'TokenReflection\Exception\RuntimeException' not found in /var/www/vendor/andrewsville/php-token-reflection/TokenReflection/ReflectionNamespace.php on line 156
PHP message: PHP Stack trace:
PHP message: PHP   1. {main}() /var/www/web/app.php:0
PHP message: PHP   2. Symfony\Component\HttpKernel\Kernel->handle() /var/www/web/app.php:48
PHP message: PHP   3. Symfony\Component\HttpKernel\Kernel->boot() /var/www/app/bootstrap.php.cache:2375
PHP message: PHP   4. Symfony\Component\HttpKernel\Kernel->initializeBundles() /var/www/app/bootstrap.php.cache:2343
PHP message: PHP   5. AppKernel->registerBundles() /var/www/app/bootstrap.php.cache:2513
PHP message: PHP   6. spl_autoload_call() /var/www/app/bootstrap.php.cache:11
PHP message: PHP   7. Symfony\Component\Debug\DebugClassLoader->loadClass() /var/www/app/bootstrap.php.cache:11
PHP message: PHP   8. Go\Instrument\ClassLoading\SourceTransformingLoader->filter() /var/www/app/bootstrap.php.cache:151
PHP message: PHP   9. Go\Instrument\ClassLoading\SourceTransformingLoader->transformCode() /var/www/vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/SourceTransformingLoader.php:102
PHP message: PHP  10. Go\Instrument\Transformer\CachingTransformer->transform() /var/www/vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/SourceTransformingLoader.php:146
PHP message: PHP  11. Go\Instrument\Transformer\CachingTransformer->processTransformers() /var/www/vendor/lisachenko/go-aop-php/src/Instrument/Transformer/CachingTransformer.php:117
PHP message: PHP  12. Go\Instrument\Transformer\WeavingTransformer->transform() /var/www/vendor/lisachenko/go-aop-php/src/Instrument/Transformer/CachingTransformer.php:141
PHP message: PHP  13. TokenReflection\ReflectionClass->getParentClassNameList() /var/www/vendor/lisachenko/go-aop-php/src/Instrument/Transformer/WeavingTransformer.php:109

i figured out that it can't find that class only in the closure here https://github.com/lisachenko/go-aop-php/blob/master/src/Core/AspectKernel.php#L243-L260. Outside the closure it is able to find that class. Any ideas? (PHP 5.5.20)

AlexKovalevych commented 9 years ago

ok, after changing kernel parameters to

require_once __DIR__.'/../app/AppAspectKernel.php';
$applicationAspectKernel = AppAspectKernel::getInstance();
$applicationAspectKernel->init([
    'debug' => true,
    'cacheDir'  => __DIR__ . '/../app/cache/aspect/',
]);

its another error:

2015/04/03 16:08:36 [error] 11226#0: *6752 FastCGI sent in stderr: "PHP message: PHP Warning:  filemtime(): stat failed for php://filter/read=go.source.transforming.loader/resource=php://filter/read=go.source.transforming.loader/resource=php://filter/read=go.source.transforming.loader/resource=/var/www/vendor/symfony/symfony/src/Symfony/Component/Debug/Exception/ContextErrorException.php in /var/www/vendor/lisachenko/go-aop-php/src/Instrument/Transformer/CachingTransformer.php on line 108
lisachenko commented 9 years ago

Hello! Glad to see you here )

Could you please tell, which version of framework do you use? And next one advice is to try experimental branch feature/simple-transforming that can give you better result (this will be merged for 1.0.0 soon)

lisachenko commented 9 years ago

Unfortuantely, old versions of framework can have glitches with symfony, because there are a lot of tricks in both frameworks. However, 1.0.0 version should be very lightweight and heavily use composer for better performance and significant simplification. I'm expecting that it will work nicely with symfony

AlexKovalevych commented 9 years ago

@lisachenko, thank you for fast response. I use sf 2.6.4 version. I'd really like to use this library at out project, but can't make it work in any way. Here is what i'm getting after switching to feature/simple-transforming:

Fatal error: Class 'TokenReflection\Broker' not found in /var/www/vendor/lisachenko/go-aop-php/src/Core/AspectKernel.php on line 251
Call Stack
#   Time    Memory  Function    Location
1   0.0001  239216  {main}( )   ../app.php:0
2   0.0034  734096  Go\Core\AspectKernel->init( )   ../app.php:26
3   0.0054  804856  AppAspectKernel->configureAop( )    ../AspectKernel.php:116
4   0.0054  805128  spl_autoload_call ( )   ../AspectKernel.php:19
5   0.0054  805264  Go\Instrument\ClassLoading\AopComposerLoader->loadClass( )  ../AspectKernel.php:19
6   0.0061  824704  Go\Instrument\ClassLoading\SourceTransformingLoader->filter( )  ../AspectKernel.php:90
7   0.0065  837152  Go\Instrument\ClassLoading\SourceTransformingLoader->transformCode( )   ../SourceTransformingLoader.php:102
8   0.0065  837440  Go\Instrument\Transformer\CachingTransformer->transform( )  ../SourceTransformingLoader.php:146
9   0.0071  838088  Go\Instrument\Transformer\CachingTransformer->processTransformers( )    ../CachingTransformer.php:117
10  0.0071  838232  Go\Core\AspectKernel->Go\Core\{closure}( )  ../CachingTransformer.php:138
11  0.0072  843592  spl_autoload_call ( )   ../CachingTransformer.php:251
12  0.0072  843632  Go\Instrument\ClassLoading\AopComposerLoader->loadClass( )  ../CachingTransformer.php:251
13  0.0098  873672  Go\Instrument\ClassLoading\SourceTransformingLoader->filter( )  ../CachingTransformer.php:90
14  0.0099  875864  Go\Instrument\ClassLoading\SourceTransformingLoader->transformCode( )   ../SourceTransformingLoader.php:102
15  0.0099  876168  Go\Instrument\Transformer\CachingTransformer->transform( )  ../SourceTransformingLoader.php:146
16  0.0107  876672  Go\Instrument\Transformer\CachingTransformer->processTransformers( )    ../CachingTransformer.php:117
17  0.0107  876672  Go\Core\AspectKernel->Go\Core\{closure}( )  ../CachingTransformer.php:138

Seems like an issue with autoloader (in the aspect kernel?), class is there of course and available in the aspect kernel, outside the closure i mentioned above.

lisachenko commented 9 years ago

Ugh, weird trace... It starts to process AOP for internal classes, however it shouldn't do this. I need a time to make this working for symfony. Try to use includePaths and excludePaths options to include only src directory and exclude vendor and cache from processing. Probably, this can help.

I'll check sf2 compatibility in nearest time

lisachenko commented 9 years ago

I have a question, did you load a composer autoloader before aspect initialization? I see direct

require_once __DIR__.'/../app/AppAspectKernel.php';

and no include '/../app/autoload.php' or '/../vendor/autoload.php'

AlexKovalevych commented 9 years ago

Autoloader is included in the app/bootstrap.php.cache file which is loaded before all the kernels. Ok, i'll try to play with include/exclude options, maybe that will work somehow.

kellewic commented 9 years ago

Posting here since my issue also relates to Symfony.

I can't get it to work in Symfony 2.6.6; no errors but no aspects work either. The loading code I have is near identical to what is outlined here.

I can see my test aspect being registered in AppAspectKernel->configureAop() and the aspect itself just has a single error_log("") call. There are no files in the configured cache directory that was created (I assume there should be).

I tried messing with the warmup script, but can't figure out what to do with it - all I keep getting is errors. I looked at its code and it seems like it wants the AppAspectKernel.php file, but that results in:

$./bin/warmup goaop:warmup /www/app/AppAspectKernel.php
Loading aspect kernel for warmup...
PHP Fatal error:  Cannot instantiate abstract class Go\Core\AspectKernel in /www/vendor/lisachenko/go-aop-php/src/Core/AspectKernel.php on line 80
PHP Stack trace:
PHP   1. {main}() /www/vendor/lisachenko/go-aop-php/bin/warmup:0
PHP   2. Symfony\Component\Console\Application->run() /www/vendor/lisachenko/go-aop-php/bin/warmup:39
PHP   3. Symfony\Component\Console\Application->doRun() /www/vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:126
PHP   4. Symfony\Component\Console\Application->doRunCommand() /www/vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:195
PHP   5. Symfony\Component\Console\Command\Command->run() /www/vendor/symfony/symfony/src/Symfony/Component/Console/Application.php:874
PHP   6. Go\Console\Command\WarmupCommand->execute() /www/vendor/symfony/symfony/src/Symfony/Component/Console/Command/Command.php:257
PHP   7. Go\Core\AspectKernel::getInstance() /www/vendor/lisachenko/go-aop-php/src/Console/Command/WarmupCommand.php:64

For reference, here is what I have configured:

In composer.json I have "lisachenko/go-aop-php: 1.0.*@dev"

app/AppAspectKernel.php

<?php

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

use Traklight\WebBundle\Aspect as TraklightAspect;

/**
 * Application Aspect Kernel
 */
class AppAspectKernel extends AspectKernel
{
    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     *
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new TraklightAspect\TestAspect());
    }
}

in web/app_dev.php

require_once __DIR__.'/../app/AppAspectKernel.php';
$applicationAspectKernel = AppAspectKernel::getInstance();
$applicationAspectKernel->init(array(
        'debug' => true,
        'cacheDir'  => __DIR__."/../var/cache/$environment/go_aop",
        'appDir' => __DIR__."/../app/",
        'includePaths' => [__DIR__.'/../src/']
));

As noted, there is the following line before Symfony's AppKernel is loaded:

$loader = require_once __DIR__.'/../var/bootstrap.php.cache';

This goes through a winding tour to eventually get to the autoloader.php

TestAspect.php

<?php
namespace Traklight\WebBundle\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;

/**
 *
 */
class TestAspect implements Aspect
{
    public function __construct()
    {
        error_log("FFFFFFF");
    }

    /**
     * Method that will be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     *
     * @Before("within(Traklight\WebBundle\Controller\Ecommerce\*")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        error_log("WOOOOO");
    }
}

I can see this class being created via the error_log in the contructor. I tried all sorts of configurations to get beforeMethodExecution() to run based on examples from https://github.com/lisachenko/go-aop-php/blob/master/tests/Go/Aop/Pointcut/PointcutParserTest.php#L55

The class and method I was targeting is in a controller: Traklight\WebBundle\Controller\Ecommerce\CheckoutController->reviewAction(Request $request)

jg-development commented 9 years ago

Hi, I have the same problem. With the command-line warmup script it always result in:

 Cannot instantiate abstract class Go\Core\AspectKernel in

The command line was:

php -f vendor/bin/warmup goaop:warmup module/Api/src/Aspect/ApplicationAspectKernel.php

The AspectKernel has no Instance and is trying to create itself. Greetings Jan

lisachenko commented 9 years ago

Hello @jg-development, and @kellewic sorry for the late response.

I'm working now on extension for phpStorm for upcoming version 1.0.0, so was a little busy. As I said before, Symfony and Go! AOP framework integration can be cumbersome, because of tricks in both frameworks. As a result, I decided to stabilize it with SF2 after 1.0 will be ready, as I know, no issues with other frameworks like Laravel, ZF2, Yii, FLOW3, etc

However, I'm looking forward to version 1.0.0 which should work more transparent. To use cache warmer, you should specify a file that will prepare a kernel, not the kernel class definition itself, so for @kellewic example, cache warmer should get a path to the web/app_dev.php file where kernel is initialized. Could you please try it with that file?

I also thinking about the configuration of paths and misc options with composer extra section - is this way more suitable for you?

kellewic commented 9 years ago

I tried it with web/app_dev.php but it seems I have to give it a full path or it won't read the file. So I gave it /www/web/app_dev.php and got errors related to $_SERVER['HTTP_X'] variables I use since they don't exist outside the web context (undefined index).

For Symfony, a better solution is probably a bundle. Could then set those configurations inside the app/config/config.yml (or XML) files and create the AOP kernel instead of needing the AppAspectKernel.php. Not sure where in Symfony this could happen, it would have to be early - maybe as a compiler pass? I don't know enough about that aspect of Symfony yet.

No worries from me on putting this off until you have time. I am posting here to provide as much information as I can to assist in getting it to work. I'll be poking around the code to figure out how it works and see if I can come back with additional information. I already have plans for Go AOP in my project so it must work with Symfony :)

kellewic commented 9 years ago

More information...

I got it to work to where the FilterInjectorTransformer returns:

php://filter/read=go.source.transforming.loader/resource=/path/to/MyClass.php

I can see the MyClass.php contents from Instrument\ClassLoading\AopComposerLoader::loadClass() and the call to 'require' there at the end, but MyClass is unchanged - I checked by putting a file_put_contents after the FilterInjectorTransformer::rewrite() call.

EDIT: Dug a bit deeper and see the only transformer processing on MyClass is Go\Instrument\Transformer\CachingTransformer

The problem with putting this in app_dev.php is the Debug::enable() call. Symfony uses this to switch from the normal ClassLoader to Symfony\Component\Debug\DebugClassLoader which unregisters all current autoloaders and then wraps them in a new instance of itself and does not subclass from ClassLoader, which is what AopComposerLoader is looking for to wrap them.

kellewic commented 9 years ago

It was a long winding road, but I finally got it to work. It does not play well at all with the Debug::enable() call from Symfony in the app_dev.php file. Even after disabling that, I had to trace the AOP code down to the actual transforms to make sure my pointcut syntax was correct - it was, but I had to put the namespace in as well; I might have followed the example too closely and didn't realize the namespace was needed. So it ended up as:

@Before("execution(public Traklight\WebBundle\Controller\Ecommerce\CheckoutController->reviewAction(*))")

One odd thing I noticed is that if I clear the Symfony cache (so there is no Go AOP cache directory yet) and then load my app, the Go AOP cache directory is created, but none of my source files are parsed and saved. If I then reload my app, it all works as expected and I see the cached output of my files.

Could the above be due to how the cache is determined when no cache file exists? I saw that it uses the HTTP request time (if I recall) as the cache file mod time when no cache file exists - could this throw it off in the above scenario?

If I run the cache warmer first it works just fine. I do get the following error at the end of the warmer run though:

HP Fatal error:  Class 'PHPUnit_Framework_TestCase' not found in /www/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php on line 23

I don't have PHP Unit installed on my staging or production servers.

kellewic commented 9 years ago

Here is my setup that is working:

_web/app_dev.php_ (note this is not the entire file)

if (isset($_SERVER['HTTP_HOST']) || isset($_SERVER['REMOTE_ADDR']))
{
    ... more code not applicable to Go AOP ...
}

//Debug::enable();
$environment = 'dev';

require_once __DIR__.'/../app/AppAspectKernel.php';
$applicationAspectKernel = AppAspectKernel::getInstance();
$applicationAspectKernel->init(array(
        'debug' => true,
        'cacheDir'  => __DIR__."/../var/cache/$environment/go_aop",
        'appDir' => __DIR__."/../src/",
));

Debug::enable() must be commented out or nothing will work.

If you use any $_SERVER[] variables in app_dev.php, you will need to do something like I did and wrap them in an if{ isset() } since they won't be set when you run the Go AOP cache warmer.

I set 'appDir' to where I have my application code since Go AOP won't process any files outside that "root" directory - this may be what the original poster is running into. If you're wanting to monkey patch vendor code, this won't work and you'll have to mess with the excludes and includes.

_app/AppAspectKernel.php_

<?php

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

use Traklight\WebBundle\Aspect as TraklightAspect;

/**
 * Application Aspect Kernel
 */
class AppAspectKernel extends AspectKernel
{
    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     *
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new TraklightAspect\TestAspect());
    }
}

_src/Traklight/WebBundle/Aspect/TestAspect.php_

<?php
namespace Traklight\WebBundle\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;

/**
 *
 */
class TestAspect implements Aspect
{
    /**
     * Method that will be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     *
     * @Before("execution(public Traklight\WebBundle\Controller\Ecommerce\CheckoutController->reviewAction(*))")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        error_log("WOOOOO");
    }
}

When I run my CheckoutController->reviewAction(), I get the error log output as expected.

Hopefully this helps the others out as well.

jg-development commented 9 years ago

Hi, with a prepared file, it works like a charm ... thanks. From my side, ticket can be closed. Greetings Jan

lisachenko commented 9 years ago

Congratulations for you! I'm happy that you managed to run it with Symfony, however, it wasn't an easy task due to DebugClassLoader, bootstrap files and many other things.

I want to keep this task open for a while until I have a working from the box solution for SF2 framework. I'm thinking about the possible bundle for SF2.

kellewic commented 9 years ago

A bundle would be nice especially if it handled the cache warming on a Symfony clear cache command. Being able to specify Go AOP config in Symfony's configs would be awesome, but not sure how to do that since Symfony needs to process those before Go AOP might have processed the files (a cache warm might help this) - not sure what the earliest kernel event is in Symfony; maybe hook into it with a high priority?

I was playing with initializing Go AOP inside of app/AppKernel.php instead of app_dev.php and app.php by using the constructor and just calling parent::__construct at the end of it to hook into Symfony init. Running into problems and ran out of time this week to keep looking at it. Wouldn't need the app/AppAspectKernel.php either if hooked into AppKernel.php.

maddo commented 9 years ago

I've been working on integrating goaop w/ sf2 recently as well, and came up with this solution, which is still a bit thorny (especially for cache warming)

Was able to accomplish this with an compiler pass:

// AspectCompilerPass.php
/**
 * Lets us add Aspect classes to the AspectKernel by tagging them with `aspect`
 *
 * example:
 *
 * ```
 * <service id="my.aspect"
 *          class="ExampleBundle\Aspect\MyAspect">
 *     <argument type="service" id="my.important.depenency" />
 *     <tag name="aspect"/>
 * </service>
 * ```
 *
 * @see src/ExampleBundle/Resources/config/services/aspects.xml
 *
 */
class AspectCompilerPass implements CompilerPassInterface
{
    /**
     * {@inheritDoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->has('aspect_container')) {
            return;
        }
        $definition = $container->findDefinition(
            'aspect_container'
        );
        $taggedServices = $container->findTaggedServiceIds(
            'aspect'
        );
        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall(
                'registerAspect',
                array(new Reference($id))
            );
        }
    }
}

Register the AspectKernel and Container as services

    <!-- aspects.xml -->
    <service id="my.aspect"
             class="ExampleBundle\Aspect\MyAspect">
        <argument type="service" id="my.important.depenency" />
        <tag name="aspect"/>
    </service>

    <service id="aspect_kernel" class="ExampleBundle\Aspect\AspectKernel">
        <factory class="ExampleBundle\Aspect\AspectKernel" method="getInstance" />
        <call method="init">
            <argument type="collection">
                <argument key="debug">%kernel.debug%</argument>
                <argument key="cacheDir">%kernel.cache_dir%/aspect</argument>
                <argument key="appDir">%kernel.root_dir%/../</argument>
                <argument type="collection" key="includePaths">
                    <argument>%kernel.root_dir%/../vendor/somevendorpath</argument>
                </argument>
            </argument>
        </call>
    </service>

    <service id="aspect_container" class="Go\Core\GoAspectContainer">
        <factory service="aspect_kernel" method="getContainer" />
    </service>

Now we need to make sure that we're initializing the kernel early, the bundles boot method is an early point

// ExampleBundle

public function boot()
{
    // We need to early initialize the AspectKernel (via DI when grabbing the AspectContainer)
    try {
        // If this resource ID exists, then the AspectKernel is already booted
        SourceTransformingLoader::getId();
    } catch (\RuntimeException $e) {
        // If not, it chucks this Exception, and we can safely boot the AspectKernel
        $this->container->get('aspect_container');
    }
}

While this is not idea, it's one way to get sf2 dependencies into the aspect code.

Again, I haven't figured out how to wrap goaop cache warmup into a sf2-compatible warmer, namely because the warmer boots the bundle, and that initializes the AspectKernel with (at this time) the wrong cachedir (the one that's about to be deleted). I would monkey with the cachedir, but it's stored in a constant, making it untouchable.

A bundle would likely be a better solution :)

lisachenko commented 9 years ago

Ok, I want to publish an initial version of symfony bundle soon at goaop/symfony-bundle. However, there are still a lot of tricks and issues to solve:

lisachenko commented 9 years ago

I'll put it there: https://github.com/goaop/goaop-symfony-bundle (just first draft, but working)