PHP AOP is a PHP library that provides a powerful Aspect Oriented Programming (AOP) implementation for PHP.
composer require okapi/aop
- Terminology
- Implicit Aspects
- Class-Level Explicit Aspects
- Method-Level Explicit Aspects
- Features
- Limitations
- How it works
- Testing
- Contributing
-
AOP: Aspect Oriented Programming - A programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.
-
Aspect: A class that implements the logic that you want to apply to your target classes. Aspects must be annotated with the
#[Aspect]
attribute. -
Advice: The logic that you want to apply to your target classes. Advice methods must be annotated with the
#[Before]
,#[Around]
or#[After]
attributes. -
Join Point: A point in the execution of your target classes where you can apply your advice. Join points are defined by the
#[Before]
,#[Around]
or#[After]
attributes. -
Pointcut: A set of join points where you can apply your advice. Pointcuts are defined by the
#[Pointcut]
attribute. -
Weaving: The process of applying your advice to your target classes.
-
Implicit Aspects: The aspects are applied without any modification to the target classes. The aspect itself specifies the classes or methods it applies to.
-
Class-Level Explicit Aspects: The aspects are applied by modifying the target classes, typically by adding the aspect as an attribute to the target class.
-
Method-Level Explicit Aspects: The aspects are applied by modifying the target classes, typically by adding the aspect as an attribute to the target method.
Click to expand
<?php
use Okapi\Aop\AopKernel;
// Extend from the "AopKernel" class
class MyKernel extends AopKernel
{
// Define a list of aspects
protected array $aspects = [
DiscountAspect::class,
PaymentProcessorAspect::class,
];
}
// Discount Aspect
<?php
use Okapi\Aop\Attributes\Aspect;
use Okapi\Aop\Attributes\After;
use Okapi\Aop\Invocation\AfterMethodInvocation;
// Aspects must be annotated with the "Aspect" attribute
#[Aspect]
class DiscountAspect
{
// Annotate the methods that you want to intercept with
// "Before", "Around" or "After" attributes
#[After(
// Use named arguments
// You can also use Wildcards (see Okapi/Wildcards package)
class: Product::class . '|' . Order::class,
method: 'get(Price|Total)',
// When using wildcards you can also use some of these options:
onlyPublicMethods: false, // Intercepts only public methods and ignores protected and private methods (default: false)
interceptTraitMethods: true, // Also intercepts methods from traits (default: true)
)]
public function applyDiscount(AfterMethodInvocation $invocation): void
{
// Get the subject of the invocation
// The subject is the object class that contains the method
// that is being intercepted
$subject = $invocation->getSubject();
$productDiscount = 0.1;
$orderDiscount = 0.2;
if ($subject instanceof Product) {
// Get the result of the original method
$oldPrice = $invocation->proceed();
$newPrice = $oldPrice - ($oldPrice * $productDiscount);
// Set the new result
$invocation->setResult($newPrice);
}
if ($subject instanceof Order) {
$oldTotal = $invocation->proceed();
$newTotal = $oldTotal - ($oldTotal * $orderDiscount);
$invocation->setResult($newTotal);
}
}
}
// PaymentProcessor Aspect
<?php
use InvalidArgumentException;
use Okapi\Aop\Attributes\After;
use Okapi\Aop\Attributes\Around;
use Okapi\Aop\Attributes\Aspect;
use Okapi\Aop\Attributes\Before;
use Okapi\Aop\Invocation\AroundMethodInvocation;
use Okapi\Aop\Invocation\AfterMethodInvocation;
use Okapi\Aop\Invocation\BeforeMethodInvocation;
#[Aspect]
class PaymentProcessorAspect
{
#[Before(
class: PaymentProcessor::class,
method: 'processPayment',
)]
public function checkPaymentAmount(BeforeMethodInvocation $invocation): void
{
$payment = $invocation->getArgument('amount');
if ($payment < 0) {
throw new InvalidArgumentException('Invalid payment amount');
}
}
#[Around(
class: PaymentProcessor::class,
method: 'processPayment',
)]
public function logPayment(AroundMethodInvocation $invocation): void
{
$startTime = microtime(true);
// Proceed with the original method
$invocation->proceed();
$endTime = microtime(true);
$elapsedTime = $endTime - $startTime;
$amount = $invocation->getArgument('amount');
$logMessage = sprintf(
'Payment processed for amount $%.2f in %.2f seconds',
$amount,
$elapsedTime,
);
// Singleton instance of a logger
$logger = Logger::getInstance();
$logger->log($logMessage);
}
#[After(
class: PaymentProcessor::class,
method: 'processPayment',
)]
public function sendEmailNotification(AfterMethodInvocation $invocation): void
{
// Proceed with the original method
$result = $invocation->proceed();
$amount = $invocation->getArgument('amount');
$message = sprintf(
'Payment processed for amount $%.2f',
$amount,
);
if ($result === true) {
$message .= ' - Payment successful';
} else {
$message .= ' - Payment failed';
}
// Singleton instance of an email queue
$mailQueue = MailQueue::getInstance();
$mailQueue->addMail($message);
}
}
// Product
<?php
class Product
{
private float $price;
public function getPrice(): float
{
return $this->price;
}
}
// Order
<?php
class Order
{
private float $total = 500.00;
public function getTotal(): float
{
return $this->total;
}
}
// PaymentProcessor
<?php
class PaymentProcessor
{
public function processPayment(float $amount): bool
{
// Process payment
return true;
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
<?php
use MyKernel;
require_once __DIR__ . '/vendor/autoload.php';
// Initialize the AOP Kernel
$kernel = MyKernel::init();
<?php
// Just use your classes as usual
$product = new Product();
// Before AOP: 100.00
// After AOP: 90.00
$productPrice = $product->getPrice();
$order = new Order();
// Before AOP: 500.00
// After AOP: 400.00
$orderTotal = $order->getTotal();
$paymentProcessor = new PaymentProcessor();
// Invalid payment amount
$amount = -50.00;
// Before AOP: true
// After AOP: InvalidArgumentException
$paymentProcessor->processPayment($amount);
// Valid payment amount
$amount = 100.00;
// Value: true
$paymentProcessor->processPayment($amount);
$logger = Logger::getInstance();
$logs = $logger->getLogs();
// Value: Payment processed for amount $100.00 in 0.00 seconds
$firstLog = $logs[0];
$mailQueue = MailQueue::getInstance();
$mails = $mailQueue->getMails();
// Value: Payment processed for amount $100.00 - Payment successful
$firstMail = $mails[0];
Click to expand
Adding the custom Aspect to the Kernel is not required for class-level explicit aspects as they are registered automatically at runtime.
// Logging Aspect
<?php
use Attribute;
use Okapi\Aop\Attributes\Aspect;
use Okapi\Aop\Attributes\Before;
use Okapi\Aop\Invocation\BeforeMethodInvocation;
// Class-Level Explicit Aspects must be annotated with the "Aspect" attribute
// and the "Attribute" attribute
#[Attribute]
#[Aspect]
class LoggingAspect
{
// The "class" argument is not required
// The "method" argument is optional
// Without the argument, the aspect will be applied to all methods
// With the argument, the aspect will be applied to the specified method
#[Before]
public function logAllMethods(BeforeMethodInvocation $invocation): void
{
$methodName = $invocation->getMethodName();
$logMessage = sprintf(
"Method '%s' executed.",
$methodName,
);
$logger = Logger::getInstance();
$logger->log($logMessage);
}
#[Before(
method: 'updateInventory',
)]
public function logUpdateInventory(BeforeMethodInvocation $invocation): void
{
$methodName = $invocation->getMethodName();
$logMessage = sprintf(
"Method '%s' executed.",
$methodName,
);
$logger = Logger::getInstance();
$logger->log($logMessage);
}
}
// Inventory Tracker
<?php
// Custom Class-Level Explicit Aspect added to the class
#[LoggingAspect]
class InventoryTracker
{
private array $inventory = [];
public function updateInventory(int $productId, int $quantity): void
{
$this->inventory[$productId] = $quantity;
}
public function checkInventory(int $productId): int
{
return $this->inventory[$productId] ?? 0;
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
// The kernel must still be initialized, even if it has no Aspects
<?php
use MyKernel;
require_once __DIR__ . '/vendor/autoload.php';
// Initialize the AOP Kernel
$kernel = MyKernel::init();
<?php
// Just use your classes as usual
$inventoryTracker = new InventoryTracker();
$inventoryTracker->updateInventory(1, 100);
$inventoryTracker->updateInventory(2, 200);
$countProduct1 = $inventoryTracker->checkInventory(1);
$countProduct2 = $inventoryTracker->checkInventory(2);
$logger = Logger::getInstance();
// Value:
// Method 'updateInventory' executed. (4 times)
// Method 'checkInventory' executed. (2 times)
$logs = $logger->getLogs();
Click to expand
Adding the custom Aspect to the Kernel is not required for method-level explicit aspects as they are registered automatically at runtime.
// Performance Aspect
<?php
use Attribute;
use Okapi\Aop\Attributes\Around;
use Okapi\Aop\Invocation\AroundMethodInvocation;
use Okapi\Aop\Attributes\Aspect;
// Method-Level Explicit Aspects must be annotated with the "Aspect" attribute
// and the "Attribute" attribute
#[Attribute]
#[Aspect]
class PerformanceAspect
{
// The "class" argument is not required
// The "method" argument is optional
// Without the argument, the aspect will be applied to all methods
// With the argument, the aspect will be applied to the specified method
#[Around]
public function measure(AroundMethodInvocation $invocation): void
{
$start = microtime(true);
$invocation->proceed();
$end = microtime(true);
$executionTime = $end - $start;
$class = $invocation->getClassName();
$method = $invocation->getMethodName();
$logMessage = sprintf(
"Method %s::%s executed in %.2f seconds.",
$class,
$method,
$executionTime,
);
$logger = Logger::getInstance();
$logger->log($logMessage);
}
}
// Customer Service
<?php
class CustomerService
{
#[PerformanceAspect]
public function createCustomer(): void
{
// Logic to create a customer
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
// The kernel must still be initialized, even if it has no Aspects
<?php
use MyKernel;
require_once __DIR__ . '/vendor/autoload.php';
// Initialize the AOP Kernel
$kernel = MyKernel::init();
<?php
// Just use your classes as usual
$customerService = new CustomerService();
$customerService->createCustomer();
$logger = Logger::getInstance();
$logs = $logger->getLogs();
// Value: Method CustomerService::createCustomer executed in 0.01 seconds.
$firstLog = $logs[0];
-
Advice types: "Before", "Around" and "After"
-
Intercept "private" and "protected" methods (Will show errors in IDEs)
-
Access "private" and "protected" properties and methods of the subject (Will show errors in IDEs)
-
Intercept "final" methods and classes
-
Use Transformers from the "Okapi/Code-Transformer" package in your Kernel to modify and transform the source code of a loaded PHP class (See "Okapi/Code-Transformer" package for more information)
- Internal "private" and "protected" methods cannot be intercepted
-
This package extends the "Okapi/Code-Transformer" package with Dependency Injection and AOP features
-
The
AopKernel
registers multiple services-
The
TransformerManager
service stores the list of aspects and their configuration -
The
CacheStateManager
service manages the cache state -
The
StreamFilter
service registers a PHP Stream Filter which allows to modify the source code before it is loaded by PHP -
The
AutoloadInterceptor
service overloads the Composer autoloader, which handles the loading of classes
-
-
The
AutoloadInterceptor
service intercepts the loading of a class -
The
AspectMatcher
matches the class and method names with the list of aspects and their configuration -
If the class and method names match an aspect, query the cache state to see if the source code is already cached
-
Check if the cache is valid:
- Modification time of the caching process is less than the modification time of the source file or the aspect file
- Check if the cache file, the source file and the aspect file exist
-
If the cache is valid, load the proxied class from the cache
-
If not, return a stream filter path to the
AutoloadInterceptor
service
-
-
The
StreamFilter
modifies the source code by applying the aspects- Convert the original source code to a proxied class (MyClass -> MyClass__AopProxied)
- The proxied class should have the same amount of lines as the original class (because the debugger will point to the original class)
- The proxied class extends a woven class which contains the logic of applying the aspects
- The woven class will be included at the bottom of the proxied class
- The woven class will also be cached
- Run
composer run-script test
or - Run
composer run-script test-coverage
- To contribute to this project, fire up an aspect in any application that works or has 100% working tests, and match every class and method with '*' with any advice type.
- If the application throws an error, then it's a bug.
- Example:
<?php
use Okapi\Aop\Attributes\After;
use Okapi\Aop\Attributes\Aspect;
use Okapi\Aop\Invocation\AfterMethodInvocation;
#[Aspect]
class EverythingAspect
{
#[After(
class: '*',
method: '*',
)]
public function everything(AfterMethodInvocation $invocation): void
{
echo $invocation->getClassName() . "\n";
echo $invocation->getMethodName() . "\n";
}
}
Give a β if this project helped you!
- Big thanks to lisachenko for their pioneering work on the Go! Aspect-Oriented Framework for PHP. This project drew inspiration from their innovative approach and served as a foundation for this project.
Copyright Β© 2023 Valentin Wotschel.
This project is MIT licensed.