Search by

goaop / goaop-yii3-bridge

lisachenko

Integration bridge for Go! AOP framework and Yii3

Package info

github.com/goaop/goaop-yii3-bridge

pkg:composer/goaop/goaop-yii3-bridge

Fund package maintenance!

lisachenko

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 2

1.0.0 2026-11-07 21:46 UTC

This package is auto-updated.

Last update: 2026-11-08 14:33:06 UTC


README

CI Codecov PHPStan Badge GitHub release Total Downloads Minimum PHP Version License Sponsor

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, @runtime and @src, differ per environment like any other parameter, and can be extended by other packages. The aop:* commands are added to ./yii.
  • 🚀 Production-friendly. Run ./yii aop:warmup at deploy time. With debug off, 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:

  1. The class names are only validated syntactically, so nothing is autoloaded when the kernel starts.
  2. 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.
  3. 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 --preload writes _preload.php to the cache directory (or --preload=FILE). Point opcache.preload at it (with opcache.preload_user): every woven class is linked once when PHP starts.

  • ./yii aop:opcache compiles 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 are public/index.php and yii. Pass --entry only 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 debug off in production and run ./yii aop:warmup at deployment.

The full results, per mode and phase, are in benchmarks/RESULTS.md.

Caveats

  • Only classes under appDir and includePaths are woven. Vendor code is not.
  • Classes loaded before AopBootstrap::init(), such as App\Environment and 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-tags groups) 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, which aop:debug:weaving reports. 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 a Go\Aop\Exception\AspectCircularReferenceException naming the path from the aspect to the advice: inject that dependency lazily, e.g. as a closure or through Psr\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.