goaop / goaop-yii3-bridge
Integration bridge for Go! AOP framework and Yii3
Fund package maintenance!
Requires
- php: ^8.4
- goaop/framework: ^4.0@dev
- psr/container: ^2.0
- yiisoft/aliases: ^3.0
- yiisoft/config: ^1.6
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12.5 || ^13.0
- psr/log: ^3.0
- symfony/console: ^7.4 || ^8.0
- symfony/event-dispatcher-contracts: ^3.5
- yiisoft/arrays: ^3.2
- yiisoft/definitions: ^3.4
- yiisoft/di: ^1.4
- yiisoft/strings: ^2.6
- yiisoft/yii-console: ^2.4
- yiisoft/yii-runner-console: ^2.2
Suggests
- yiisoft/yii-console: Adds the aop:warmup and aop:debug:* console commands
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-16 19:54:44 UTC
README
Integration bridge for the Go! AOP framework and Yii3. Declare cross-cutting behavior once, as an aspect with PHP 8 attributes, and the engine weaves it into the matching methods of your services when their classes are loaded. You don't run a code generation step or write proxy wrappers, and services are created and injected exactly as before.
Why Aspect-Oriented Programming in Yii3?
Some logic doesn't belong to any single class, yet ends up copied into many of them: logging, caching, metrics, transactions, authorization checks, retry policies. PSR-15 middleware solves this for HTTP requests. AOP solves it for any method of any service: define the behavior once as an aspect, say where it applies with a pointcut expression, and the engine weaves it in. Your services stay plain business logic.
Highlights
- 🪡 Transparent weaving. Classes are transformed when they are loaded. Your services don't extend a base class, implement an interface or get wrapped in a proxy, and the container builds and injects them as usual.
- 🏷️ PHP 8 attributes. Advices are declared with
#[Before],#[After],#[Around]and#[AfterThrowing]on the methods of your aspect, using pointcut expressions (execution(public App\Service\**->*(*))). - 💤 Lazy aspects with Yii DI. Every aspect is a regular service of the Yii container, registered by tagging it
goaop.aspect: constructor injection, autowiring and definitions work as for any other service. The engine resolves an aspect only when one of its advices runs for the first time. Until then the aspect class is not even loaded in production. - 🧩 Yii3-native configuration. The bridge plugs into yiisoft/config groups.
Settings live under
params['goaop/goaop-yii3-bridge'], accept aliases such as@root,@runtimeand@src, differ per environment like any other parameter, and can be extended by other packages. Theaop:*commands are added to./yii. - 🚀 Production-friendly. Run
./yii aop:warmupat deploy time. Withdebugoff, woven classes are served from the cache through Composer's class map, which works well with opcache. - 🔬 Pure PHP. Built on Go! AOP 4.x: no PECL extension, no
eval(), and every transformation produces readable static PHP files.
Requirements
- PHP >= 8.4
- Yii3 packages: yiisoft/config, yiisoft/di, yiisoft/aliases (as in the application template)
- goaop/framework 4.x (attribute-based aspects)
Installation
composer require goaop/goaop-yii3-bridge
The yiisoft/config plugin picks up the configuration of the bridge (run composer yii-config-rebuild if you
installed it with the plugin disabled).
Note
Until goaop/framework 4.0 is tagged, the bridge depends on the unreleased 4.0-dev line, so the composer.json
of your application needs:
"minimum-stability": "dev", "prefer-stable": true
Start the kernel from the entry script
Add one line to src/bootstrap.php (the file that public/index.php, yii and the test bootstrap require), right
after Composer's autoloader:
<?php declare(strict_types=1); use App\Environment; use Go\Yii3\GoAopBridge\AopBootstrap; require_once dirname(__DIR__) . '/vendor/autoload.php'; // ... .env loading ... Environment::prepare(); AopBootstrap::init(rootPath: dirname(__DIR__), environment: Environment::appEnv());
AopBootstrap::init() reads the bridge parameters and the aliases from the merged params config group, the same
configuration the runner uses. Pass it the same rootPath, environment (and configDirectory, vendorDirectory,
mergePlanFile, if you changed them) as your runners.
Why the entry script? Weaving only applies to classes loaded after the kernel starts. The Yii runner autoloads
classes while it builds the container, before any bootstrap callback runs. In debug mode, for example, every
class with an array definition is reflected for validation, and the HTTP runner creates the error handler early.
If you skip this step, the bootstrap config group of the bridge still starts the kernel. Weaving then starts late:
autowired services are woven, but classes loaded while the container was built are not. In debug mode that
includes every class with an array definition, so behavior would differ between development and production.
Defining an aspect
An aspect is a plain class implementing Go\Aop\Aspect, with advice methods declared by attributes. It is a
regular service of the Yii container, so its constructor dependencies are injected:
<?php declare(strict_types=1); namespace App\Aspect; use Go\Aop\Aspect; use Go\Aop\Intercept\MethodInvocation; use Go\Lang\Attribute\Around; use Go\Lang\Attribute\Before; use Psr\Log\LoggerInterface; final readonly class LoggingAspect implements Aspect { public function __construct(private LoggerInterface $logger) {} #[Before('execution(public App\Service\**->*(*))')] public function logCall(MethodInvocation $invocation): void { $this->logger->debug('Calling {method}', ['method' => $invocation->getMethod()->getName()]); } #[Around('execution(public App\Service\PaymentService->charge(*))')] public function measureCharge(MethodInvocation $invocation): mixed { $start = hrtime(true); try { return $invocation->proceed(); } finally { $this->logger->info('charge() took {ms}ms', ['ms' => (hrtime(true) - $start) / 1e6]); } } }
Registering aspects
Tag the aspect with goaop.aspect in a container definition of the common di config group, e.g. in
config/common/di/aspects.php. The same definition configures the constructor arguments that can't be autowired:
use App\Aspect\AuditAspect; use App\Aspect\LoggingAspect; use Yiisoft\Definitions\Reference; return [ LoggingAspect::class => [ 'tags' => ['goaop.aspect'], ], AuditAspect::class => [ '__construct()' => [ 'logger' => Reference::to('auditLogger'), 'level' => $params['app']['auditLevel'], ], 'tags' => ['goaop.aspect'], ], ];
A definition with a dedicated definition key (['definition' => static fn() => ..., 'tags' => ['goaop.aspect']])
and a tag map in a common di-tags config group (['goaop.aspect' => [LoggingAspect::class]]) work too.
The Yii container can only return tagged services by creating all of them. So the bridge reads the tag declarations
from the configuration instead and creates no aspect until one of its advices runs. Keep these tags in the common
groups: web and console processes share the woven cache, so they must weave the same aspects. Tags of
di-web/di-console definitions and of service providers are not read.
Alternatively, list the aspect classes in the aspects parameter of config/common/params.php:
'goaop/goaop-yii3-bridge' => [ 'aspects' => [ App\Aspect\LoggingAspect::class, ], ],
Both ways can be combined, and an aspect found by both is registered once. Tagged services must be registered under their class name, the way the container validates tags.
See the Go! AOP documentation on advices and pointcuts for the full syntax.
How aspects are resolved
The bridge registers every tagged or listed class as a deferred service of the aspect container. The factory of that service asks the Yii container for the aspect:
- The class names are only validated syntactically, so nothing is autoloaded when the kernel starts.
- When the weaver needs the advices of an aspect (on the first load of a matching class, or by
aop:warmup), it reads them from the class through reflection. The aspect object is a native lazy proxy and is not constructed yet. - On the first call of one of its advices, the proxy is initialized with
$container->get(AspectClass::class). That is the shared instance of the Yii container, with all its dependencies.
So an aspect depending on a database connection, an HTTP client or a heavy service doesn't create it on requests that never reach an advised method.
PHP can't make a class without instance properties lazy. The weaver creates such an aspect when it reads the advices,
and if the Yii container is not attached yet it does so directly, with new. A stateless aspect has nothing to
inject, so this only fails, with a descriptive error, when its constructor requires arguments.
In debug mode the bridge checks that every tagged or listed class exists and implements Go\Aop\Aspect, and the error
names the tag or parameter it comes from. Otherwise the aspect container would silently ignore it.
Aspects contributed by packages
A package tags its aspects in its own di config the same way, or contributes them from its own config/params.php
(the params group is merged recursively and lists are appended):
return [ 'goaop/goaop-yii3-bridge' => [ 'aspects' => [ Vendor\Package\Aspect\CacheAspect::class, ], ], ];
Duplicates are registered once.
Configuration
The defaults live in the params group of the bridge. Override them in your config/common/params.php, or per
environment in config/environments/<env>/params.php:
| Parameter | Default | Purpose |
|---|---|---|
debug |
null |
Re-weave when sources or aspects change, breakpoints work in the original sources. null follows the GOAOP_DEBUG, then the APP_DEBUG environment variable. Keep it off in production. |
appDir |
'@root' |
Application root, only files below it are woven. |
cacheDir |
'@runtime/aop' |
Woven sources, proxies and advisor caches. |
cacheFileMode |
null |
Permissions of cache files: an integer (0770) or octal digits ('770', e.g. from an environment variable). null means 0770 minus the umask. |
features |
0 |
Bitmask of Go\Aop\Features flags, e.g. Features::PREBUILT_CACHE. |
includePaths |
['@src'] |
Only these directories are woven. Keep it narrow. |
excludePaths |
[] |
Never woven. The cache directory and the framework are always excluded. |
aspects |
[] |
Aspect classes, resolved lazily from the Yii container. |
aspectTag |
'goaop.aspect' |
Container tag registering aspects, read from the common di and di-tags config groups. null disables it. |
A malformed parameter fails fast with a message naming it.
The kernel is available in the container as Go\Core\AspectKernel (and Go\Yii3\GoAopBridge\Kernel\AspectYii3Kernel),
its aspect container as Go\Core\AspectContainer.
Console commands
With yiisoft/yii-console installed, the bridge adds:
| Command | Purpose |
|---|---|
./yii aop:warmup |
Weaves every class below the include paths into the cache. Fails if a file can't be woven; --fail-fast stops at the first error. |
./yii aop:debug:aspect |
Lists the aspects with their pointcuts and advisors (--aspect=App\\Aspect\\LoggingAspect for one). |
./yii aop:debug:advisor |
Lists the advisors, or the classes and members one advisor matches (--advisor=...). |
./yii aop:debug:weaving |
Weaves twice and reports proxies that differ, which reveals circular dependencies between aspects and woven classes. |
They are the framework's own commands, with the kernel injected by the container instead of a loader argument.
Deployment
Weaving happens on the first load of each class. In production, turn debug off and build the cache at deploy time:
./yii aop:warmup
On read-only file systems add Features::PREBUILT_CACHE to features. The engine then trusts the cache completely
and never writes to it, so rebuild it on every deploy.
Performance
Measured with the benchmarks (composer benchmark), on a small synthetic application whose
baseline boots in about 1.5 ms with opcache:
- In production, with opcache and a warm AOP cache, the bridge adds about 0.5 ms per request without aspects: the entry line reads the params, the tagged aspects and starts the kernel. No aspect class is loaded at boot, and the weaver and the parsers are never loaded once the cache is warm.
- Registering aspects costs a few µs per request at most; an aspect is resolved from the container (about 15 µs) only when one of its advices runs for the first time.
- Debug mode recompiles woven classes on every request (about 70 µs each), and the first request after a deployment
weaves every class it loads: keep
debugoff in production and run./yii aop:warmupat deployment.
The full results, per mode and phase, are in benchmarks/RESULTS.md.
Caveats
- Only classes under
appDirandincludePathsare woven. Vendor code is not. - Classes loaded before
AopBootstrap::init(), such asApp\Environmentand anythingsrc/bootstrap.phptouches before that line, can't be woven. - Aspects need no weaving themselves: exclude their directory (e.g.
'excludePaths' => ['@src/Aspect']). An aspect that depends on a class it advises creates a circular dependency, whichaop:debug:weavingreports. - The aspect kernel is process-global. Long-running workers (RoadRunner, Swoole, FrankenPHP) initialize it once per worker, and every request reuses the woven classes and the aspect instances. Aspects holding per-request state need to be reset like any other shared service.
Contributing
See CONTRIBUTING.md.
License
MIT, see LICENSE.