okapi / aop
PHP AOP is a PHP library that provides a powerful Aspect Oriented Programming (AOP) implementation for PHP.
Installs: 4 046
Dependents: 0
Suggesters: 0
Security: 0
Stars: 31
Watchers: 6
Forks: 3
Open Issues: 31
Requires
- php: >=8.1
- nette/php-generator: ^4.0
- okapi/code-transformer: 1.3.7
- okapi/singleton: ^1.0
- okapi/wildcards: ^1.0
- php-di/php-di: ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.3
- symfony/console: ^6.3
- symfony/var-dumper: ^6.3
README
PHP AOP
PHP AOP is a PHP library that provides a powerful Aspect Oriented Programming (AOP) implementation for PHP.
Installation
composer require okapi/aop
Usage
📖 List of contents
- Terminology
- Implicit Aspects
- Class-Level Explicit Aspects
- Method-Level Explicit Aspects
- Features
- Limitations
- How it works
- Testing
- Contributing
Terminology
-
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.
Implicit Aspects
Click to expand
Create a Kernel
<?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, ]; }
Create an Aspect
// 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); } }
Target Classes
// 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
// 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();
Result
<?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];
Class-Level Explicit Aspects
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.
Create an Aspect
// 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); } }
Target Classes
// 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
// 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();
Result
<?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();
Method-Level Explicit Aspects
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.
Create an Aspect
// 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); } }
Target Classes
// Customer Service <?php class CustomerService { #[PerformanceAspect] public function createCustomer(): void { // Logic to create a customer } }
Initialize the Kernel
// 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();
Result
<?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];
Features
-
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)
Limitations
- Internal "private" and "protected" methods cannot be intercepted
How it works
-
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
-
General workflow when a class is loaded
-
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
Testing
- Run
composer run-script test
or - Run
composer run-script test-coverage
Contributing
- 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"; } }
Show your support
Give a ⭐ if this project helped you!
🙏 Thanks
- 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.
📝 License
Copyright © 2023 Valentin Wotschel.
This project is MIT licensed.