okapi/aop

PHP AOP 是一个 PHP 库,它为 PHP 提供了一个强大的面向切面编程(AOP)实现。

1.2.15 2024-09-07 17:19 UTC

This package is auto-updated.

Last update: 2024-09-07 17:21:49 UTC


README

PHP AOP

License: MIT Twitter: @WalterWoshid PHP: >=8.1 Packagist Build

Coverage - PHP 8.1 Coverage - PHP 8.2

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";
    }
}

展示你的支持

如果这个项目对你有帮助,请给它一个 ⭐!

🙏 感谢

📝 许可证

版权所有 © 2023 Valentin Wotschel
本项目采用 MIT 许可证。