Skip to content

AOP

Aspect oriented programming (AOP) allows you to intercept method calls on objects managed by the container, without modifying the target class.

This implementation uses proxy-based AOP, the same model as Spring Framework's default AOP support. Rather than modifying class bytecode at compile time (as AspectJ weaving does), the container generates a decorator class that wraps the target object and delegates to registered aspects at each join point. This means only objects resolved through the container are subject to interception — direct new instantiation bypasses it entirely.

Concepts

Aspect — a class that contains the cross-cutting logic to be applied to a target.

Pointcut — the rule that defines which class (or class hierarchy) an aspect applies to.

Advice — a method on the aspect that runs at a specific point relative to the target method call (Before, After, Around, AfterThrow).

Join point — a JoinPoint object passed to every advice method, giving access to the intercepted subject, the method name, and the call arguments.

Registering an aspect

Register an aspect class through the DI configurator using the aspect method on the container.

Vendor/ModuleName/Configuration/Di.php
<?php

declare(strict_types=1);

namespace Vendor\ModuleName\Configuration;

use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Contract\Di\DiConfiguratorInterface;
use Amarant\Framework\Data\Set;
use Vendor\ModuleName\Aspect\SomeClassAspect;
use Override;

class Di implements DiConfiguratorInterface
{
    #[Override] public function configure(ConfigurableContainerInterface $container, Set $configurators): void
    {
        $container->aspect(SomeClassAspect::class);
    }
}

The aspect class itself is resolved through the container, so constructor dependencies are injected normally.

To remove a previously registered aspect by its id:

$container->removeAspect('vendor_module_some_class');

Defining an aspect class

An aspect class must carry the #[Aspect] attribute and declare one or more advice methods.

Vendor/ModuleName/Aspect/SomeClassAspect.php
<?php

declare(strict_types=1);

namespace Vendor\ModuleName\Aspect;

use Amarant\Framework\Attribute\Aop\After;
use Amarant\Framework\Attribute\Aop\Aspect;
use Amarant\Framework\Attribute\Aop\PointCut;
use Amarant\Framework\Data\Aop\JoinPoint;
use Amarant\Framework\Enum\AspectEnum;
use Vendor\ModuleName\Service\SomeService;

#[Aspect(
    id: 'vendor_module_some_class',                                      // (1)
    pointCut: new PointCut(AspectEnum::POINTCUT_CLASS, SomeService::class), // (2)
    priority: 0                                                          // (3)
)]
final class SomeClassAspect
{
    #[After('doSomething')]                                              // (4)
    public function afterDoSomething(JoinPoint $joinPoint, mixed $returnValue): mixed
    {
        // modify or replace the return value
        return $returnValue;
    }
}
  1. A unique string identifier for this aspect. Used when removing the aspect via removeAspect.
  2. The pointcut specifies which class this aspect applies to. See Pointcuts below.
  3. Optional priority. When multiple aspects target the same method, higher priority aspects run first.
  4. The advice attribute targets the method named doSomething on the intercepted class.

Pointcuts

A pointcut is constructed with an AspectEnum strategy and a class name.

Strategy Constant Behavior
Exact class AspectEnum::POINTCUT_CLASS Applies only to that specific class.
Class hierarchy AspectEnum::POINTCUT_SUBCLASS Applies to the class and all its subclasses.
use Amarant\Framework\Attribute\Aop\PointCut;
use Amarant\Framework\Enum\AspectEnum;
use Vendor\ModuleName\Service\SomeService;

// exact match
new PointCut(AspectEnum::POINTCUT_CLASS, SomeService::class)

// SomeService and any class that extends it
new PointCut(AspectEnum::POINTCUT_SUBCLASS, SomeService::class)

Advice types

Before

Runs before the target method. Receives a MutableArgumentJoinPoint, allowing the advice to modify the arguments that will be passed to the original method.

1
2
3
4
5
6
7
8
9
use Amarant\Framework\Attribute\Aop\Before;
use Amarant\Framework\Data\Aop\MutableArgumentJoinPoint;

#[Before('processOrder')]
public function beforeProcessOrder(MutableArgumentJoinPoint $joinPoint): void
{
    // read or change arguments before the method executes
    $joinPoint->setArg('flag', true);
}

Modifications to $joinPoint args are applied before the original method is called.

After

Runs after the target method returns. Receives the immutable JoinPoint and the return value. Must return the (potentially modified) return value.

use Amarant\Framework\Attribute\Aop\After;
use Amarant\Framework\Data\Aop\JoinPoint;

#[After('getItems')]
public function afterGetItems(JoinPoint $joinPoint, array $items): array
{
    // extend or filter the result
    $items[] = $this->extraItem;
    return $items;
}

Around

Wraps the target method entirely. Receives JoinPoint and a Closure that calls the original method. Must return a value. If the closure is not called, the original method is skipped and the chain stops.

use Amarant\Framework\Attribute\Aop\Around;
use Amarant\Framework\Data\Aop\JoinPoint;
use Closure;

#[Around('calculate')]
public function aroundCalculate(JoinPoint $joinPoint, Closure $proceed): mixed
{
    if ($this->shouldSkip($joinPoint)) {
        return null; // (1)
    }

    $result = $proceed(); // (2)

    return $this->postProcess($result);
}
  1. Returning without calling $proceed() skips the original method entirely.
  2. Call $proceed() to execute the original method and receive its return value.

AfterThrow

Runs when the target method throws an exception. Receives JoinPoint and the Throwable. The exception is always re-thrown after the advice runs.

The target argument defaults to '*', which matches any method on the intercepted class.

use Amarant\Framework\Attribute\Aop\AfterThrow;
use Amarant\Framework\Data\Aop\JoinPoint;
use Throwable;

// (1)
#[AfterThrow]
public function onAnyException(JoinPoint $joinPoint, Throwable $throwable): void
{
    $this->logger->error($joinPoint->getMethod(), ['exception' => $throwable]);
}

// (2)
#[AfterThrow('processPayment')]
public function onProcessPaymentException(JoinPoint $joinPoint, Throwable $throwable): void
{
    $this->notifier->alert($throwable);
}
  1. No argument (defaults to '*') — intercepts exceptions from any method.
  2. Explicit method name — intercepts exceptions only from processPayment.

The JoinPoint

Every advice method receives a JoinPoint as its first parameter.

Method Description
getSubject(): object The intercepted object instance.
getMethod(): string The method name being intercepted.
getArgs(): array Named argument array (['argName' => $value, ...]).
getArg(string $key): mixed Get a single argument by parameter name.

MutableArgumentJoinPoint (used exclusively in Before advice) extends JoinPoint with:

Method Description
setArg(string $key, mixed $value): void Set a single argument.
setArgs(array $args): void Replace all arguments.
removeArg(string $key): void Remove an argument.

Execution order

For a single method call, the advice types run in this order:

  1. Before — arguments may be mutated
  2. Around (if present, replaces the original call; otherwise the original method runs directly)
  3. After — return value may be replaced
  4. AfterThrow — only if an exception was thrown (after steps 1–3)

When multiple aspects target the same method, aspects with higher priority run first in each phase.