okapi / aop
PHP AOP 是一个 PHP 库,它为 PHP 提供了一个强大的面向切面编程(AOP)实现。
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 是一个 PHP 库,它为 PHP 提供了一个强大的面向切面编程(AOP)实现。
安装
composer require okapi/aop
使用方法
📖 内容列表
术语
-
AOP: 面向切面编程 - 一种编程范式,旨在通过允许分离横切关注点来增加模块化。
-
切面: 一个实现你想要应用于目标类的逻辑的类。切面必须使用
#[Aspect]
属性进行注解。 -
通知: 你想要应用于目标类的逻辑。通知方法必须使用
#[Before]
、#[Around]
或#[After]
属性进行注解。 -
连接点: 在目标类的执行过程中,你可以应用你的通知的点。连接点由
#[Before]
、#[Around]
或#[After]
属性定义。 -
切入点: 可以应用你的通知的连接点集。切入点由
#[Pointcut]
属性定义。 -
织入: 将你的通知应用于目标类的过程。
-
隐式切面: 切面在不修改目标类的情况下应用。切面本身指定了它要应用到的类或方法。
-
类级别显式切面: 通过修改目标类来应用切面,通常是通过将切面作为属性添加到目标类中。
-
方法级别显式切面: 通过修改目标类来应用切面,通常是通过将切面作为属性添加到目标方法中。
隐式切面
点击展开
创建内核
<?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];
类级别显式切面
点击展开
对于类级别显式切面,无需将自定义切面添加到内核中,因为它们在运行时自动注册。
创建切面
// 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();
方法级别显式切面
点击展开
对于方法级别显式切面,无需将自定义切面添加到内核中,因为它们在运行时自动注册。
创建切面
// 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];
特性
-
通知类型: "Before"、"Around" 和 "After"
-
拦截 "private" 和 "protected" 方法(将在 IDE 中显示错误)
-
访问主题的 "private" 和 "protected" 属性和方法(将在 IDE 中显示错误)
-
拦截 "final" 方法和类
-
在您的内核中使用来自 "Okapi/Code-Transformer" 包的 Transformer 来修改和转换加载的 PHP 类的源代码(有关更多信息,请参阅 "Okapi/Code-Transformer" 包)
限制
- 内部 "private" 和 "protected" 方法不能被拦截
工作原理
-
此包通过依赖注入和 AOP 功能扩展了 "Okapi/Code-Transformer" 包
-
AopKernel
注册了多个服务-
TransformerManager
服务存储切面及其配置列表 -
CacheStateManager
服务管理缓存状态 -
StreamFilter
服务注册了一个 PHP 流过滤器,允许在 PHP 加载之前修改源代码 -
AutoloadInterceptor
服务重载了 Composer 加载器,该加载器处理类的加载
-
加载类时的通用工作流程
-
AutoloadInterceptor
服务拦截类的加载 -
AspectMatcher
通过类和方法名称与方面列表及其配置进行匹配 -
如果类和方法名称与某个方面匹配,查询缓存状态以查看源代码是否已经缓存
-
检查缓存是否有效
- 缓存过程的修改时间早于源文件或方面文件的修改时间
- 检查缓存文件、源文件和方面文件是否存在
-
如果缓存有效,则从缓存中加载代理类
-
如果无效,则将流过滤器路径返回给
AutoloadInterceptor
服务
-
-
StreamFilter
通过应用方面修改源代码- 将原始源代码转换为代理类(MyClass -> MyClass__AopProxied)
- 代理类应与原始类具有相同的行数(因为调试器将指向原始类)
- 代理类继承了一个包含应用方面逻辑的编织类
- 编织类将被包含在代理类的底部
- 编织类也将被缓存
测试
- 运行
composer run-script test
或 - 运行
composer run-script test-coverage
贡献
- 要为此项目做出贡献,在任意运行良好或测试通过率为100%的应用中启动一个方面,并将每个类和方法与 '*' 以及任何建议类型进行匹配。
- 如果应用程序抛出错误,那么这是一个错误。
- 示例
<?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"; } }
展示你的支持
如果这个项目对你有帮助,请给它一个 ⭐!
🙏 感谢
- 非常感谢 lisachenko 对 Go! Aspect-Oriented Framework for PHP 的开创性工作。本项目从中汲取了灵感,并为其奠定了基础。
📝 许可证
版权所有 © 2023 Valentin Wotschel。
本项目采用 MIT 许可证。