首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 面向切面編程(AOP)入門:使用 Go! AOP 框架實現橫切關注點分離

     編輯:PHP 相關知識

前言

AOP(Aspect-Oriented Programming)讓你在不修改業務代碼的前提下,將日誌、事務、權限、效能監控等「橫切關注點」模組化。PHP 官方雖無內建 AOP,但 Go! AOP PHP Library 提供純 PHP 實作,無需擴展即可在共享主機部署。本文以實戰範例帶你入門。

一、AOP 核心概念

術語說明類比
Aspect(切面)橫切邏輯的模組化單元「日誌切面」、「事務切面」
Pointcut(切點)定義「何時、何地」織入的規則「所有 Public 方法執行前」
Advice(通知/增強)在切點執行的具體代碼「記錄日誌」、「開啟事務」
Join Point(連接點)程式執行中可被攔截的點方法呼叫、屬性存取、異常拋出
Weaving(織入)將 Advice 注入 Join Point 的過程編譯期/載入期/執行期

二、Go! AOP 安裝與配置

1. Composer 安裝

composer require lisachenko/go-aop-php

2. 建立 AspectKernel(引導核心)

<?php
// app/ApplicationAspectKernel.php
namespace App;

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

class ApplicationAspectKernel extends AspectKernel {
    protected function configureAop(AspectContainer $container) {
        // 註冊切面
        $container->registerAspect(new \App\Aspect\LoggingAspect());
        $container->registerAspect(new \App\Aspect\TransactionAspect());
    }

    protected function getApplicationLoaderPath() {
        return __DIR__ . '/../vendor/autoload.php'; // Composer 自動載入
    }
}
?>

3. 建立載入器(入口檔最早引入)

<?php
// public/index.php 或 bootstrap.php
require_once __DIR__ . '/../vendor/autoload.php';
\App\ApplicationAspectKernel::getInstance()->init([
    'debug' => true,                    // 開發模式
    'cacheDir' => __DIR__ . '/../var/cache/aop', // 織入快取目錄
    'includePaths' => [__DIR__ . '/../src'],     // 掃描範圍
]);
?>

三、實戰範例:經紀人交易系統

1. 業務類別(無任何 AOP 痕跡)

<?php
// src/Broker.php
namespace App;

class Broker {
    private string $name;
    private string $id;

    public function __construct(string $name, string $id) {
        $this->name = $name;
        $this->id = $id;
    }

    public function getId(): string { return $this->id; }

    public function buy(string $symbol, int $volume, float $price): float {
        $value = $volume * $price;
        if ($value > 1000) {
            throw new \App\Exception\SpentTooMuchException("超額:$value");
        }
        return $value;
    }

    public function sell(string $symbol, int $volume, float $price): float {
        return $volume * $price;
    }
}
?>

2. 日誌切面(@Before、@After、@Around)

<?php
// src/Aspect/LoggingAspect.php
namespace App\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Around;

class LoggingAspect implements Aspect {
    /** @Before("execution(public App\Broker->*(*))") */
    public function beforeMethod(MethodInvocation $invocation) {
        echo "[LOG] 進入方法: {$invocation->getMethod()->getName()}()\n";
        echo "[LOG] 參數: " . implode(', ', $invocation->getArguments()) . "\n";
    }

    /** @After("execution(public App\Broker->*(*))") */
    public function afterMethod(MethodInvocation $invocation) {
        echo "[LOG] 結束方法: {$invocation->getMethod()->getName()}()\n\n";
    }

    /** @Around("execution(public App\Broker->buy(*))") */
    public function aroundBuy(MethodInvocation $invocation) {
        $result = $invocation->proceed(); // 執行原方法
        echo "[LOG] buy() 回傳: $result\n";
        return $result; // 必須返回,否則調用者拿不到結果
    }
}
?>

3. 事務/折扣切面(@Around 修改行為)

<?php
// src/Aspect/DiscountAspect.php
namespace App\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Around;

class DiscountAspect implements Aspect {
    /** @Around("execution(public App\Broker->buy(*))") */
    public function applyVipDiscount(MethodInvocation $invocation) {
        $broker = $invocation->getThis(); // 取得目標物件實例
        $result = $invocation->proceed();

        // ID 為 "2" 的 VIP 享 8 折
        if ($broker->getId() === '2') {
            return $result * 0.8;
        }
        return $result;
    }
}
?>

4. 異常處理切面(@AfterThrowing)

<?php
// src/Aspect/ExceptionAspect.php
namespace App\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\AfterThrowing;

class ExceptionAspect implements Aspect {
    /** @AfterThrowing("execution(public App\Broker->buy(*))") */
    public function logException(MethodInvocation $invocation) {
        // 此處可記錄異常、發送告警、補償邏輯
        echo "[EXCEPTION] buy() 拋出異常,已記錄並處理。\n";
        // 注意:@AfterThrowing 不會阻止異常繼續冒泡
    }
}
?>

四、切點表達式語法速查

模式範例匹配目標
executionexecution(public App\Service->*(*))所有 public 方法執行
executionexecution(public App\Service->set*(*))所有以 set 開頭的 public 方法
executionexecution(* App\Service->*(*))所有可見度方法
withinwithin(App\Module\*)指定命名空間下所有類別
annotation@annotation(App\Annotation\Transactional)標註特定註解的方法

限制: 每個模式段僅支援一個 * 萬用字元(如 App\Broker* 可行,App\*ker* 不可)。

五、單元測試驗證(PHPUnit)

<?php
// tests/BrokerTest.php
namespace App\Tests;

use App\Broker;
use App\Exception\SpentTooMuchException;
use PHPUnit\Framework\TestCase;

class BrokerTest extends TestCase {
    public function testBuyNormal() {
        $broker = new Broker('John', '1');
        $this->assertEquals(500, $broker->buy('GOOGL', 100, 5));
    }

    public function testVipDiscount() {
        $broker = new Broker('Finch', '2');
        $this->assertEquals(80, $broker->buy('MS', 10, 10)); // 100 * 0.8
    }

    public function testBuyTooMuchThrows() {
        $this->expectException(SpentTooMuchException::class);
        $broker = new Broker('Finch', '2');
        $broker->buy('MS', 10000, 8);
    }
}
?>

六、最佳實踐與避坑指南

  1. 切面職責單一: 日誌切面只做日誌,事務切面只管事務,勿混雜業務邏輯。
  2. 謹慎使用 @Around 修改回傳值: 易造成隱性 Bug,文檔化說明並編寫測試。
  3. 避免過度 AOP: 核心業務規則(如折扣計算)應留在 Domain Model,AOP 適合真正橫切的基礎設施關注點。
  4. 快取預熱: 生產環境設定 'debug' => false,部署時預先執行織入生成快取。
  5. 效能考量: 織入會產生 Proxy 類別,大量切面可能影響啟動速度,監控 cacheDir 大小。
  6. 除錯技巧: 開啟 debug 會輸出織入資訊,結合 Xdebug 逐行除錯 Proxy 代碼。

七、替代方案對比

方案類型優點缺點適用場景
Go! AOP純 PHP 庫無擴展、共享主機可用、註解驅動執行期織入有效能開銷中小專案、快速原型
PHP-AOP 擴展C 擴展編譯期織入、效能極佳需編譯安裝、共享主機不可用高效能生產系統
Laravel 服務容器/中介層框架內建原生整合、開發體驗佳綁定 Laravel 生態Laravel 專案
Symfony EventDispatcher事件驅動鬆耦合、標準化非真正 AOP、需手動觸發事件Symfony/通用 PHP 專案

結語

AOP 是處理橫切關注點的利器,Go! AOP 以純 PHP 實現降低了門檻。掌握「切面、切點、通知」三要素,配合註解驅動與單元測試,即可在專案中引入 AOP。記住:AOP 是基礎設施的補充,而非業務邏輯的替代品。合理劃分邊界,你的代碼將更乾淨、更模組化、更易維護。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved