【PHP】PHPフレームワーク「Flow」を試してみる～AOP編～

# 初めに
前回は[こちら](https://qiita.com/nnhkrnk/items/77048e47018e602870d1)の記事で参照可能なファイルの扱いについてまとめました。
今回はFlowにおけるAOPの実装についてまとめます。

# （ざっくりと）AOPとは
ざっくりとAOPとは何かを説明します。（「知ってるよ」って方はスキップ推奨ですmm）
chatGPT様に説明をお願いしました（丸投げ）。

> AOP（Aspect-Oriented Programming）は、プログラム内の共通な機能（ログ記録、エラーハンドリングなど）を取り出して、それらを別のモジュールとして管理するプログラミング手法です。これにより、コードがよりシンプルで保守しやすくなります。アスペクトと呼ばれるモジュールが特定の機能を担当し、アドバイスと呼ばれる処理がコードに挿入されるポイントを指定します。 AOPは特に大規模なソフトウェアプロジェクトで有用です。

まとめると『**似たような処理はまとめて、自動で実行されるようにしようね**』ってことですね。

## AOPの例
あるプロジェクトで、**DB接続時には実行するSQLをログに出力する**というルールを決めたとします。それを実現するため**DBアクセスを行う全てのメソッドの頭にログ出力処理を入れる**という実装を考えました。

しかし、この実装には3つの問題点があります。
* **ログ出力の実装を複数の箇所に書かなければならない**

* **1つのメソッドで「SQL実行」と「ログ出力」という複数の役割が生まれてしまう**

* **ログ出力の実装を書き忘れることがある**

上記の問題点を解決するためには以下のような実装にする必要があります。
* **一か所にログ出力のメソッドを書いておく**
* **SQL実行時に自動でログ出力する**

このような**共通的な処理と条件を一か所にまとめる**ようなプログラミング手法のことを**Aspect-Oriented Programming**(AOP)と呼びます。


# FlowにおけるAOP
AOPでは『**どんな条件で**、**何の処理が**実行されるのか』を定義する必要があります。
上記をFlowで実現するために重要な3つの役割を押さえておきましょう。

* Pointcut
* Advice
* JoinPoint

## Pointcut
AOPにおける**どんな条件で**にあたる部分です。
先ほどの例でいうと「DB接続のメソッドを呼び出した時に」という条件になりそうです。

## Advice
AOPにおける**何の処理が**にあたる部分です。
先ほどの例でいうと「ログを出力する」という処理になりますね。

## JoinPoint
AOPにおける**どこでAOPが割り込んだのか**情報を持っている部分です。
Flowでは、Pointcutの条件に当てはまることが検知されるとAOPの処理が割り込みで走ります。JoinPointはその割り込まれたクラスやメソッドの情報を持っています。
先ほどの例でいうと「SQLを実行するメソッド」の部分になります。


# 試してみた
ということで実際に試してみましょう。
今回は『APIのリクエストが来た際、Controllerとメソッドの名前をログに出力する』という処理をAOPで実装してみたいと思います。

プロジェクト構成は以下です。
```
Project/
  ├ Data/
  |   └ Logs/
  |        └ System_Development.log（出力されるログファイル）
  |
  └ Packages/
      ├ Application/
      |    └ Neos.Welcome/
      |         └ Classes/
      |              ├ Aop/
      |              |    └ LogAspect.php（★）
      |              |
      |              └ Controller/
      |                   └ ImageController.php（★）
      |
      ├ Framework/
      └ Libraries/
```


## AOP
AOPのソースです。ポイントは以下（解説は後述）。
* **```@Flow\Aspect```をクラスに付与**
* **```@Flow\Pointcut```でPointcutを定義**
* **```JoinPointInterface```型の変数を引数に、Adviceメソッドを定義**


```php: LogAspect.php
<?php
namespace Neos\Welcome\Aop;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Psr\Log\LoggerInterface;

/**
 * @Flow\Aspect
 */
class LogAspect
{

    /**
     * @var LoggerInterface A logger implementation
     */
    protected $logger;

    public function injectLogger(LoggerInterface $logger): void
    {
            $this->logger = $logger;
    }

    /**
     * @Flow\Pointcut("method(Neos\Welcome\Controller\.*Controller->.*Action())")
     */
    public function controllerOutputLogPointcut(): void {}

    /**
     * @Flow\Before("Neos\Welcome\Aop\LogAspect->controllerOutputLogPointcut")
     */
    public function controllerOutputLog(JoinPointInterface $joinPoint): void
    {
        $className = $joinPoint->getClassName();
        $methodName = $joinPoint->getMethodName();
        $this->logger->info("------class:".$className." method:".$methodName."------");
    }
}
```

### 1. ```@Flow\Aspect```をクラスに付与

```@Flow\Aspect```アノテーションをクラスに付与することで、AOPを実装することが可能になります。

```php
/**
 * @Flow\Aspect
 */
class LogAspect
{
```

### 2. ```@Flow\Pointcut```でPointcutを定義
```@Flow\Pointcut```を付与することでPointcutを定義することができます。
定義したPointcutは後述するAdviceで指定することで呼び出される条件を付与することが可能です。

Pointcutでの条件の設定の仕方にはいくつか種類があります。
今回は```method```を用いてどのメソッドが呼び出された場合に処理を実行するかを定義しています。
引数にはクラス名とメソッド名を記載しています。例のように正規表現を用いることも可能です。

詳しくは[公式ドキュメント](https://flowframework.readthedocs.io/en/9.0/TheDefinitiveGuide/PartIII/AspectOrientedProgramming.html#pointcuts)を参照ください。

```php
    /**
     * @Flow\Pointcut("method(Neos\Welcome\Controller\.*Controller->.*Action())")
     */
    public function controllerOutputLogPointcut(): void {}
```

### 3. ```JoinPointInterface```型の変数を引数に、Advice用のメソッドを定義
```JoinPointInterface```を引数に持つメソッドを作成します。これがAOPにおけるAdviceになります。

アノテーションで付与している```@Flow\Before```というアノテーションはAdviceをいつ実行するのかを定義します。
```Before```の場合、引数で指定したメソッドの実行前にこのAdviceを実行するという処理になります。
こちらについても[公式ドキュメント](https://flowframework.readthedocs.io/en/9.0/TheDefinitiveGuide/PartIII/AspectOrientedProgramming.html#declaring-advice)をご参照ください。


```php
    /**
     * @Flow\Before("Neos\Welcome\Aop\LogAspect->controllerOutputLogPointcut")
     */
    public function controllerOutputLog(JoinPointInterface $joinPoint): void
    {
        $className = $joinPoint->getClassName();
        $methodName = $joinPoint->getMethodName();
        $this->logger->info("------class:".$className." method:".$methodName."------");
    }
```

:::note info
PointcutとAdviceは以下のように一つにまとめることも可能です。

```php
    /**
     * @Flow\Before("method(Neos\Welcome\Controller\.*Controller->.*Action())")
     */
    public function controllerOutputLog(JoinPointInterface $joinPoint): void
    {
        // なんか処理
    }
```
:::

## Controller
Controllerを実装します。
AOP側で実行条件や実行内容を記載しているため、Controllerが意識することは特にありません。
特別なことはせず、適当なレスポンスを返すシンプルな作りにしてます。

```php: AopController
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;

class AopController extends ActionController
{

    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @return void
     */
    public function aopTestAction()
    {
        $this->view->assign('value', array('testResponse'));
    }
}
```

## 実行
ということで実行してみました。
問題なく200応答です。
```sh
$ curl -i -X GET http://localhost:8081/Neos.Welcome/Aop/aopTest

HTTP/1.1 200 OK
Host: localhost:8081
Date: Sat, 13 Jan 2024 05:00:37 GMT
Connection: close
X-Powered-By: PHP/8.1.25
Content-Type: application/json
X-Flow-Powered: Flow/8.3
Content-Length: 16

["testResponse"]
```

ログを見たところ、クラス名とメソッド名がちゃんと出力されていることが確認できました。
（意図してなかったBaseクラスのメソッドも出力されちゃいました）

```
24-01-13 04:59:39 18704      DEBUG                          Router route(): Route "Neos.Flow :: default with action and format" matched the request "http://localhost:8081/Neos.Welcome/Aop/aopTest (GET)".
24-01-13 04:59:39 18704      DEBUG                          CSRF: No token required, safe request
24-01-13 04:59:39 18704      DEBUG                          ------class:Neos\Welcome\Controller\AopController method:initializeAction------
24-01-13 04:59:39 18704      DEBUG                          ------class:Neos\Welcome\Controller\AopController method:aopTestAction------
```

# 終わりに
今回はFlowにおけるAOPの実装方法について記載しました。
Reflectionと組み合わせればもっといろいろなことができそうで面白そうです。
もう少し複雑な処理なんかも試してみたいので、うまくできたらまた記事書きます。

ここまでご覧いただきありがとうございました！

# 参考
https://flowframework.readthedocs.io/en/9.0/TheDefinitiveGuide/PartIII/ResourceManagement.html