goaop / goaop-yii3-bridge
Integration bridge for Go! AOP framework and Yii3
Fund package maintenance!
Requires
- php: ^8.4
- goaop/framework: ^4.0
- 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-11-08 14:33:06 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, listed in the bridge parameters: 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.0 (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).
Start the kernel from the entry script
Add one line to every entry script (public/index.php, yii, and the bootstrap of your functional tests), between the
creation of the runner and its run. public/index.php of the application template
becomes:
use App\Environment; use Go\Yii3\GoAopBridge\AopBootstrap; use Yiisoft\Yii\Runner\Http\HttpApplicationRunner; // ... $runner = new HttpApplicationRunner( rootPath: dirname(__DIR__), debug: Environment::appDebug(), checkEvents: Environment::appDebug(), environment: Environment::appEnv(), ); AopBootstrap::init($runner->getConfig(), rootPath: dirname(__DIR__)); $runner->run();
and yii does the same with its ConsoleApplicationRunner (rootPath: __DIR__).
AopBootstrap::init() reads the bridge parameters and the aliases from the config of the runner:
the parameters it sees are the ones of that runner (params-web or params-console, with params nested), the
container definitions are evaluated with them, as the runner does later. The runner keeps that config, so the
parameters are not built twice.
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
List the aspect classes in the aspects parameter, e.g. in config/common/params.php:
'goaop/goaop-yii3-bridge' => [ 'aspects' => [ App\Aspect\LoggingAspect::class, App\Aspect\AuditAspect::class, ], ],
Every aspect is a regular service of the Yii container. Its constructor arguments that can't be autowired come from a container definition, as for any other service:
use App\Aspect\AuditAspect; use Yiisoft\Definitions\Reference; return [ AuditAspect::class => [ '__construct()' => [ 'logger' => Reference::to('auditLogger'), 'level' => $params['app']['auditLevel'], ], ], ];
Tagging aspects (opt-in)
Aspects can also be registered by a container tag. Set 'aspectTag' => 'goaop.aspect', then tag the definitions of
the common di config group ('tags' => ['goaop.aspect'], also with a dedicated definition key), or list them in
a common di-tags tag map (['goaop.aspect' => [LoggingAspect::class]]). Tagged services must be registered under
their class name, the way the container validates tags. Both ways can be combined, and an aspect found by both is
registered once.
The lookup is off by default because of what it costs. The Yii container can only return tagged services by
creating all of them, so the bridge reads the tag declarations from the configuration instead. These files are PHP,
and the entry line evaluates them before the kernel starts: every class they reference, e.g. by a class constant or
an enum case, is loaded at that moment and can't be woven any more. Only the common groups are read: web and console
processes share the woven cache, so they must weave the same aspects. Tags of di-web/di-console definitions,
di-tags-web/di-tags-console maps and service providers are not seen.
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.
An aspect is always the instance of the Yii container. The weaver never creates an aspect, so this also holds for an aspect without instance properties (which PHP can't make lazy) and for classes woven while the container is still being built, as in debug mode, where the container loads every class with an array definition to validate it.
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 contributes its aspects 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 |
null |
Woven sources, proxies and advisor caches. null follows the GOAOP_CACHE_DIR environment variable (an alias allowed), then '@runtime/aop'. Keep it out of shared and persistent volumes, see Deployment. |
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: PREBUILT_CACHE (see Deployment), PARTIAL_REFLECTION (weave classes whose parent class, interface or trait Composer can't locate; members inherited from a missing ancestor aren't intercepted), INTERCEPT_INCLUDES (weave files loaded with include/require). |
includePaths |
['@src'] |
Only these directories are woven. Keep it narrow. |
excludePaths |
[] |
Never woven. The cache directory and the framework are always excluded. |
parsedFilesLimit |
null |
Number of parsed files kept in memory while weaving, also the default of aop:warmup --cached-files: more is faster on a large code base, fewer uses less memory. A positive integer or digits; null means the kernel default (100). |
aspects |
[] |
Aspect classes, resolved lazily from the Yii container. |
aspectTag |
null |
Opt-in container tag registering aspects (e.g. 'goaop.aspect'), read from the common di and di-tags config groups before the kernel starts. See Tagging aspects. |
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. Once the bootstrap callback ran, the aspect container delegates to
the Yii container: get() and has() resolve an id it does not know from the Yii container.
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. --workers=N weaves in N parallel processes, --cache-dir=DIR builds the cache in another directory, --preload[=FILE] writes an opcache.preload script, --cached-files=N overrides parsedFilesLimit. |
./yii aop:opcache |
Primes the opcache file cache (opcache.file_cache) with the AOP cache, the application and its configuration, after aop:warmup. |
./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 (cache:warmup:aop, cache:warmup:opcache, debug:*), registered under names
of the aop namespace; aop:warmup and aop:opcache are extended for Yii3. They run on the kernel the entry line
initialized: leave out their optional loader argument.
The workers of aop:warmup --workers=N run the entry script again (./yii, with the hidden aop:warmup-worker
command), so they boot the application with the same environment, configuration and aspects. That needs the entry
line: when the kernel was started by the bootstrap fallback, the command weaves in its own process and says so.
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 --workers=4
--cache-dir=DIR builds the cache in another directory while the kernel keeps using its own, e.g. to build a fresh
cache and swap it in atomically: the cache files are location independent, so the directory can be moved into place.
The cache belongs to the release it was built from: keep it out of shared and persistent volumes. The production setup
of the Yii3 application templates (docker/prod/compose.yml of yiisoft/app and yiisoft/app-api) mounts a named
volume on the runtime directory:
services: app: volumes: - runtime:/app/runtime
With the default @runtime/aop, a cache built into the image with ./yii aop:warmup then lands in that volume.
Docker copies it there on the first deployment only: every later deployment keeps the volume, so the new containers
serve the woven classes and the preload script of the previous release. Without PREBUILT_CACHE production mode
does not notice the changed sources; with it, a class that is new in the release fails with a WeavingException.
A rolling update (start-first) even runs both releases against the same cache. Build the cache into a directory of
the image that no volume covers, with the environment variable or the parameter:
ENV GOAOP_CACHE_DIR=/app/aop RUN ./yii aop:warmup --workers=4 --preload
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. A request never weaves: a class below the include paths that the
cache doesn't know fails with a Go\Aop\Exception\WeavingException. The process of aop:warmup itself (and of its
workers, and aop:debug:weaving) is exempted: the entry line recognizes the command, also abbreviated or registered
under another name, before it starts the kernel, so the classes the application loads before the command runs are
loaded from their sources there, and the warm-up weaves them into the cache.
With opcache, two more steps save the compilation of the woven classes in every new PHP process:
-
./yii aop:warmup --preloadwrites_preload.phpto the cache directory (or--preload=FILE). Pointopcache.preloadat it (withopcache.preload_user): every woven class is linked once when PHP starts. -
./yii aop:opcachecompiles the AOP cache, Composer's class map, the configuration and the entry points into the opcache file cache. The configuration is the merge plan of yiisoft/config (config/.merge-plan.php) and every file it lists, of every group and environment, the application's and the vendor packages'; the entry points arepublic/index.phpandyii. Pass--entryonly for other files a request includes. It runs in the final image, with the PHP binary and the paths that serve the requests, and in a process started with the file cache settings, which the command prints when they are missing:mkdir -p /var/cache/opcache && php -d opcache.enable_cli=1 -d opcache.file_cache_only=1 \ -d opcache.file_cache=/var/cache/opcache \ ./yii aop:opcache --file-cache=/var/cache/opcache
Performance
Measured with the benchmarks (composer benchmark), on a small synthetic application whose
baseline boots in about 1 ms with opcache:
- In production, with opcache and a warm AOP cache, the bridge adds about 0.3 ms per request without aspects: the entry line reads the params 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 10 µs) only when one of its advices runs for the first time.
- Debug mode recompiles woven classes on every request (about 35 µ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 anything the entry script touches before that line, can't be woven. - Classes the configuration loads while the entry line reads it (the parameters, and with the opt-in tag lookup the
di/di-tagsgroups) can't be woven either. - 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. When creating the aspect runs one of its own advices (it calls an advised method of a dependency while it is built), the advice fails with aGo\Aop\Exception\AspectCircularReferenceExceptionnaming the path from the aspect to the advice: inject that dependency lazily, e.g. as a closure or throughPsr\Container\ContainerInterface, or narrow the pointcut. - 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.