goaop/goaop-symfony-bundle
Go! AOP Symfony Bundle brings aspect-oriented programming to Symfony apps, letting you define aspects and advice to intercept service methods. It adds bundle configuration, AOP cache warming, and console tooling for weaving aspects into your code.
1. Install (mind the version fence). The bundle is frozen at its 2018 release: it targets Symfony 2.8–4.x and PHP 5.6–7.x. On PHP 8 or Symfony 5+ Composer will either refuse or you'll hit fatal errors deep in goaop/framework. Verify with composer require goaop/goaop-symfony-bundle in a throwaway branch before committing to it.
composer require goaop/goaop-symfony-bundle
This pulls in goaop/framework, which does the actual AST rewriting — the bundle is only a thin bridge that wires the AspectKernel to the Symfony container.
2. Register the bundle. Symfony 4/Flex: add Go\Symfony\GoAopBundle\GoAopBundle::class => ['all' => true] to config/bundles.php. Legacy: add it to AppKernel::registerBundles().
3. Minimal config. The bundle exposes a single go_aop tree:
# config/packages/go_aop.yaml
go_aop:
cache_dir: '%kernel.cache_dir%/aspect'
debug: '%kernel.debug%' # keep tied to kernel.debug, never hardcode true
app_dir: '%kernel.project_dir%/src'
include_paths:
- '%kernel.project_dir%/src' # ONLY these files get woven
exclude_paths:
- '%kernel.project_dir%/src/Controller'
- '%kernel.project_dir%/src/Entity'
cache_warmer: true
include_paths is the whole ballgame. Anything outside it loads unmodified, so advice silently does nothing.
4. Your first aspect. Logging is the cheapest end-to-end proof:
namespace App\Aspect;
use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Aop\Annotations\Before;
use Psr\Log\LoggerInterface;
class LoggingAspect implements Aspect
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @Before("execution(public App\Service\Mailer->send(*))")
*/
public function beforeSend(MethodInvocation $invocation)
{
$this->logger->info('Sending mail', [
'method' => $invocation->getMethod()->getName(),
'args' => $invocation->getArguments(),
]);
}
}
App\Aspect\LoggingAspect:
tags: ['goaop.aspect']
5. Confirm it actually wove. Clear the cache, then open the generated file — it contains your class with the advice inlined:
var/cache/dev/aspect/App/Service/Mailer.php
If that file doesn't exist, the class wasn't woven: usually a wrong include_paths, a final class, or the class was loaded before the aspect kernel booted.
Look first at: var/cache/*/aspect/ (the woven source), the go_aop config block, and GoAopBundle\Kernel\AspectSymfonyKernel in the vendor dir to see exactly which kernel options the bundle sets.
Around advice for caching / retry / timing. MethodInvocation::proceed() runs the original method; returning without calling it short-circuits:
/**
* @Around("execution(public App\Service\ReportGenerator->generate*(*))")
*/
public function around(MethodInvocation $invocation)
{
$key = md5(
$invocation->getMethod()->getDeclaringClass()->getName()
. '::' . $invocation->getMethod()->getName()
. serialize($invocation->getArguments())
);
$item = $this->pool->getItem($key);
if ($item->isHit()) {
return $item->get();
}
$result = $invocation->proceed();
$this->pool->save($item->set($result));
return $result;
}
$invocation->getThis() gives you the target object (handy for $target->getId() in cache keys); getArguments() returns the argument list. There is no setArguments() in the public interface, so changing arguments requires a different design — decorate the service instead.
Opt-in via a custom annotation. Instead of matching class names, mark the methods you care about and let the aspect find them. This is the most maintainable pattern because the annotation lives next to the code it affects:
/** @Annotation @Target("METHOD") */
final class Audited {}
/**
* @Before("@annotation(App\Annotation\Audited)")
*/
public function audit(MethodInvocation $invocation) { /* ... */ }
The pointcut only tells you which method matched; read the annotation itself through a Reader if you need its parameters.
Cross-cutting services to delegate to aspects:
AuthorizationCheckerInterface, match execution(public App\Service\Admin\*->*(*)), throw AccessDeniedException before proceeding.@Around wrapping proceed() in $em->transactional(...) for service methods that aren't already annotated with Doctrine's own @Transactional.microtime() around proceed() and push to StatsD; no changes to a single domain class.proceed() and re-invoke with a counter — one of the few things genuinely hard to do without AOP.Keep aspects thin and injectable. Aspects are normal Symfony services resolved through the container, so constructor injection works — but they are instantiated at kernel boot (before request handling), so heavy constructors run on every request. Inject a lazy service or a service locator if a dependency is expensive.
Warm during deploy, not at runtime. With cache_warmer: true, bin/console cache:warmup weaves every class in include_paths up front. Do this in your deploy script before reloading php-fpm, and make sure opcache is reset afterwards. In dev, leave debug: true so the weaver re-checks source mtimes; in prod, stale mtimes are not detected.
Integration with DI-heavy code. The woven class keeps its exact FQCN, so autowiring, type hints, instanceof, and service decoration all keep working. You can AOP a decorated service just like the original — but don't do both for the same concern; pick one mechanism per concern and stick to it.
Testing. KernelTestCase boots the kernel (and therefore the aspect kernel) per test, so aspects run in functional tests as they do in production. Unit tests that instantiate a woven class with new bypass weaving entirely; write those against the aspect class directly with a mocked MethodInvocation.
The package is abandoned. No commits since 2018, no PHP 8 support, no Symfony 5+. Adopt it only on a legacy 3.4/4.x codebase you're not migrating soon, and treat it as technical debt. For new work, Symfony's decorator pattern, event dispatcher, and kernel.* listeners cover 90% of what people reach for AOP to do.
Call-site vs. execution model. Weaving happens by rewriting the target class and prepending Go!'s autoloader. Consequence: any class loaded before the aspect kernel boots is never woven. If a bundle's boot() instantiates one of your services, that instance is plain. If you hit this, override Kernel::boot() and call the aspect kernel's boot() before parent::boot().
Classes that silently refuse to weave:
final classes and final methodsprivate methods (only public/protected)__get, __call, __construct especially — don't advise constructors)No error is thrown; the advice just never fires. Always verify by inspecting var/cache/dev/aspect/.
Entities and Doctrine proxies are a trap. Excluding src/Entity is not optional in practice — woven entities break lazy proxies, get_class() comparisons, and serialization.
Cache invalidation is manual in prod. debug: false means the weaver trusts the cache dir. After changing an aspect's pointcut or the advised class, you must bin/console cache:clear (or delete var/cache/*/aspect) and restart php-fpm; otherwise you're debugging advice that's still the old version. Keep debug: true on any environment where you're editing aspects.
Opcache. With opcache.validate_timestamps=0, rewriting woven files has no effect until the process restarts. Warm then reload, in that order.
Debugging workflow. Read the woven file — with debug: true the framework leaves comments marking where advice was inserted and which aspect produced it. If the framework's console command is registered, goaop:debug lists aspects and matched pointcuts; otherwise register Go\Console\Command\DebugCommand manually to get the same output.
Pointcut collisions. There is no reliable priority/ordering attribute for aspects in this stack; the order is the order services are added to the container. Keep pointcuts disjoint — when two @Around advices match the same method, the nesting becomes effectively opaque and you'll spend hours on it.
Annotations need a reader. @Before/@After/@Around/@AfterThrowing are Doctrine annotations. On older Doctrine versions you may need AnnotationRegistry::registerLoader('class_exists') somewhere in bootstrap, or annotation lookups fail silently.
Never advise the aspect itself. An aspect whose dependencies pull in woven services can recurse, and an aspect matching its own methods will loop. Scope pointcuts to a service namespace, not to App\*.
Xdebug makes warmup glacial. Disable it when running cache:warmup, or a moderately sized src/ turns a 5-second deploy step into minutes.
Performance expectations. Weaving adds a real per-call cost (object allocation for the joinpoint plus advice). It's fine for service-layer methods; it is not fine for tight loops, hot getters, or anything called thousands of times per request. Point them at the service object, not the value object.
How can I help you explore Laravel packages today?