BEAR.Sunday Advent Calendar 2021 day14の記事です!
カルテット開発部では、大半のWebアプリケーションはSymfonyで作るのですが、小さなコマンドラインアプリケーションにはRay.DIを使っています。
今年はRay.DIと組み合わせて使えるAOPライブラリ、Ray.Aopを使ってみたので紹介します!
BEFORE
取り扱いのある各広告媒体ごとにとあるチェックを行うコマンドラインアプリケーションを作りました。
<?php
// ...
class YahooChecker implements CheckerInterface
{
// ...
public function check(TargetAdAccount $account, AuthenticationInterface $auth): void
{
if (!$auth instanceof RefreshTokenAuthentication) {
throw new \LogicException(get_class($auth).'is not expected for Yahoo');
}
// do actual check here
}
}
広告媒体のAPIを叩くのに必要な認証情報は媒体によって異なります。たとえば、Yahoo広告APIであればリフレッシュトークン(+ client_id, client_secret)を使う典型的なOAuth認証方式です。
CheckerInterface
は全媒体に共通のinterfaceなので、checkメソッドで受け取る認証情報も全媒体に共通の AuthenticationInterface
となっていますが、実際にYahoo広告のAPIコールに必要な認証情報の具象は RefreshTokenAuthentication
なので最初にガード節でAuthenticationの型をチェックしています。
違う型のAuthenticationインスタンスを渡すと例外が投げられ、コマンド実行はエラーになります。
xxx@xxx myapp % php bin/console check xxx xxx
In YahooChecker.php line 16:
GoogleRefreshTokenAuthentication is not expected!
check <ad-account> <ad-account-name>
このCheckerをRay.Aopを使ってAOPに書き換えてみたいと思います!
step0. ray/aopを入れる
このCheckerアプリケーションでは ray/di
を使っていたので既に ray/aop
に依存しており、新たに足す必要はありませんでした。
step1. AOP用のAttributeを作る
※ 今年の前半〜開発開始したCheckerアプリケーションなので、PHP8を使っています。PHP7以下の方はAnnotationで読み替えてください。
RefreshTokenAuthentication
であることを保証したいAttributeなので RefreshTokenAuthenticationRequired
と名付けてみました。
<?php
#[\Attribute(\Attribute::TARGET_METHOD)]
class RefreshTokenAuthenticationRequired
{
}
step2. AOP用のMethodInterceptorを作る
メソッドの引数 $auth
が RefreshTokenAuthentication
のインスタンスでなければLogicExceptionを投げるMethodInterceptorを作りました。
ちょうどBEFOREのガード節部分ですね。
<?php
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
class AuthenticationTypeBlocker implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
$auth = $invocation->getNamedArguments()['auth'];
if (!$auth instanceof RefreshTokenAuthentication) {
throw new \LogicException(get_class($auth).' is not expected!');
}
return $invocation->proceed();
}
}
step3. DIモジュールでAOPを使うよう設定する
ドキュメント https://github.com/ray-di/Ray.Di#aspect-oriented-programing の通りに従来のbindの他にbindInterceptor()を追加しました。
どのクラスでも、 RefreshTokenAuthenticationRequired
のAttributeがついているメソッドに対して、AuthenticationTypeBlockerで $auth
引数が RefreshTokenAuthentication
のインスタンスかどうかチェックする指定です。
<?php
class YahooAppModule extends AbstractModule
{
public function configure()
{
$this->bind(CheckerInterface::class)->to(YahooChecker::class);
+ $this->bindInterceptor(
+ $this->matcher->any(),
+ $this->matcher->annotatedWith(RefreshTokenAuthenticationRequired::class),
+ [AuthenticationTypeBlocker::class]
+ );
}
}
実行してみる!
xxx@xxx myapp % php bin/console check xxx xxx
In AuthenticationTypeBlocker.php line 16:
GoogleRefreshTokenAuthentication is not expected!
check <ad-account> <ad-account-name>
例外の発生場所が In AuthenticationTypeBlocker.php line 16:
と変わり、ガード節でなくInterceptorでチェックできていることがわかります。
まとめ
いままで長年(?)ray/diを使っていながら、aopは食わず嫌いをしていました。
アドベントカレンダーに登録しちゃった勢いで試してみたら、とても簡単・安全にガード節が除去できることがわかりました
今後はもうちょっと積極的に使ってみようと思いますー!