初めに
前回はこちらの記事で参照可能なファイルの扱いについてまとめました。
今回は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
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を実装することが可能になります。
/**
* @Flow\Aspect
*/
class LogAspect
{
2. @Flow\Pointcut
でPointcutを定義
@Flow\Pointcut
を付与することでPointcutを定義することができます。
定義したPointcutは後述するAdviceで指定することで呼び出される条件を付与することが可能です。
Pointcutでの条件の設定の仕方にはいくつか種類があります。
今回はmethod
を用いてどのメソッドが呼び出された場合に処理を実行するかを定義しています。
引数にはクラス名とメソッド名を記載しています。例のように正規表現を用いることも可能です。
詳しくは公式ドキュメントを参照ください。
/**
* @Flow\Pointcut("method(Neos\Welcome\Controller\.*Controller->.*Action())")
*/
public function controllerOutputLogPointcut(): void {}
3. JoinPointInterface
型の変数を引数に、Advice用のメソッドを定義
JoinPointInterface
を引数に持つメソッドを作成します。これがAOPにおけるAdviceになります。
アノテーションで付与している@Flow\Before
というアノテーションはAdviceをいつ実行するのかを定義します。
Before
の場合、引数で指定したメソッドの実行前にこのAdviceを実行するという処理になります。
こちらについても公式ドキュメントをご参照ください。
/**
* @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."------");
}
PointcutとAdviceは以下のように一つにまとめることも可能です。
/**
* @Flow\Before("method(Neos\Welcome\Controller\.*Controller->.*Action())")
*/
public function controllerOutputLog(JoinPointInterface $joinPoint): void
{
// なんか処理
}
Controller
Controllerを実装します。
AOP側で実行条件や実行内容を記載しているため、Controllerが意識することは特にありません。
特別なことはせず、適当なレスポンスを返すシンプルな作りにしてます。
<?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応答です。
$ 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と組み合わせればもっといろいろなことができそうで面白そうです。
もう少し複雑な処理なんかも試してみたいので、うまくできたらまた記事書きます。
ここまでご覧いただきありがとうございました!
参考