CakePHP の Command のテストで Mock を使うのは、 DI を利用するのがよさそう #cakephp

概要

Mock はできるだけ使いたくない派なんだけれど、外部API利用時なんかはどうしても必要になる時がある。

Controller の場合はやりようがあるが、 Command はちょっと迷った。

CakePHP の Command で使うようなときに Mock をするには DI を使うのがよさそう。

Controller の場合はどうするか

Controller の場合は \Cake\TestSuite\IntegrationTestTrait::controllerSpy という用途にあったメソッドが提供されている。

https://api.cakephp.org/4.2/trait-Cake.TestSuite.IntegrationTestTrait.html#controllerSpy()

また、 Controller.initialize のように、初期化時のフックイベントが元から用意されているので、そこに差し込むようなことも可能。

https://book.cakephp.org/4/en/controllers.html#event-list

DI がよさそうな理由

Command 用には \Cake\TestSuite\ConsoleIntegrationTestTrait が用意されているものの、 controllerSpy に相当するようなメソッドは用意されていないようだ。

Command 関連のイベントには Console.buildCommands が用意されているが、これはどちらかというとコマンドの名前変更等が目的なようで、このイベント内でどうにかするにはかなり無理がありそう(イベントが発火されるのも、Command のインスタンス生成前)。

自前で差し込み用のイベントを作成することも考えたが、テストのためだけに新しいイベントを作成するのは躊躇われる。

そういった理由から DI がよさそうと判断した。DI 自体は CakePHP の中ではまだ実験的な位置づけらしいので、大きな変更や使えなくなったりする可能性はあると思う。

DI でやってみる

まず Command のコンストラクタで、 Mock したいクラスを受け取るようにする。 受け取った引数をメンバ変数に格納する

<?php

class HelloCommand extends Command
{
    public $externalApiService;

    public function __construct(ExternalApiService $externalApiService)
    {
        parent::__construct();
        $this->externalApiService = $externalApiService;
    }

    public function execute(Arguments $args, ConsoleIo $io)
    {
        // モックしたい処理
        $this->externalApiService->doSomething();
    }
}

\App\Application::services で、 DI コンテナに登録しておく。

Command の場合は次のようになる。

<?php

public function services(ContainerInterface $container): void
{
    parent::services($container);
    $container
        ->add(HelloCommand::class)
        ->addArgument(ExternalApiService::class);
}

ここまでで DI されるようになったので、テストで Mock に差し替えてみる。

差し替えには \Cake\TestSuite\ContainerStubTrait::mockService を使う。

ConsoleIntegrationTestTrait は ContainerStubTrait を実装しているので、別途 use する必要はない。

<?php

class HelloCommandTest extends TestCase
{
  public function testExecute(): void
    {
        // モックに差し替える
        $this->mockService(ExternalApiService::class, function () {
            $mock = $this->createPartialMock(ExternalApiService::class, ['doSomething']);
            $mock->expects($this->atLeastOnce())->method('doSomething');

            return $mock;
        });

        $this->exec('hello');
        $this->assertExitCode(CommandInterface::CODE_SUCCESS);
    }
}

参考

CakePHP4 で、 Controller 単位で認証をスキップする #cakephp

概要

CakePHP3 の AuthComponent は非推奨になり、 CakePHP4 では AuthenticationComponent を使うことになる。

認証スキップのやり方が変わったのでメモしておく。

CakePHP3 の場合

\Cake\Controller\Component\AuthComponent::allow を使えば良い。

Controller の initialize で、認証不要のアクションを指定してあげればいい。

<?php

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('Auth');

    // index アクションは認証不要
    $this->Auth->allow('index');
}

CakePHP4 の場合

\Authentication\Controller\Component\AuthenticationComponent::allowUnauthenticated を使う。

AuthComponent では複数アクション指定の場合は配列、単独の場合は文字列で指定できたが、 AuthenticationComponent の場合は必ず配列で指定する必要がある。

<?php

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('Authentication.Authentication');

    // 必ず配列で指定する
    $this->Authentication->allowUnauthenticated(['index']);
}

Controller 単位で認証不要にするには?

Cake3 (AuthComponent) の場合は、 allow の引数を指定しないことで全アクションを対象にできた。

<?php

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('Auth');

    // Controller 内すべてのアクションで認証不要になる
    $this->Auth->allow();
}

ただし Cake4 (AuthenticationComponent) では設定でチェックを無効化する形になる。

<?php

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('Authentication.Authentication', [
        // requireIdentity で無効化する
        'requireIdentity' => false,
    ]);
}

実際に AuthenticationComponent を利用する際は AppController など基底クラスで load することになると思うので、各コントローラで設定を切り替えてやればよい。

<?php

public function initialize(): void
{
    parent::initialize();
    // 認証不要
    $this->Authentication->setConfig('requireIdentity', false);
}

参考

PHP でメモリが足りなくなったら #php

メモリが足りなくて死んじゃう

↓みたいなメッセージが出て死んじゃうことがある。

PHP Fatal error: Out of memory (allocated 39845888) (tried to allocate 512000 bytes)...
Fatal error : Allowed memory size of 2147483648 bytes exhausted (tried to allocate 80 bytes) in ・・・

メモリがどれくらい使われてるのか調べる

memory_get_usage や memory_get_peak_usage を使うと、使用しているメモリを調べられる。

real_usage を引数で指定することにより、確保しているメモリか実際に使っているメモリか判別できる。

echo "memory_get_usage: " . memory_get_usage() / (1024 * 1024) . "MB";
echo "memory_get_peak_usage: " . memory_get_peak_usage() / (1024 * 1024) . "MB";

getrusage というのもあるらしい

メモリ上限の設定を確認する、変更する

php のメモリ上限は設定ファイルで定義されているので、値を変更することで上限を引き上げられる。

-1 にすると上限がなくなる(必要なら使えるだけ使う)。

$ php -i | grep memory_limit
memory_limit => 128M => 128M

php.ini を修正してあげればOK

; Maximum amount of memory a script may consume  
; http://php.net/memory-limit      
; memory_limit = 128M 
memory_limit = 256M

メモリの上限を動的に書き換える

ini_set で memory_limit を変更することによって、プログラムからメモリ上限を引き上げることができる。

// 取得
$memory_limit = ini_get('memory_limit');

// 設定
ini_set('memory_limit', '1G');

GCを強制的に実行する

メモリリークをしているような処理はないのに、まるでメモリリークをしているような現象に遭遇することがある。

そういう場合は gc_collect_cycles で解決するかもしれない。

gc_collect_cycles();

PHPの循環参照の検出はルートが満杯になると動作するが、その件数が1万件に固定されている。

そのため、「件数は少ないがメモリ消費量が大きい」状態になると、GC が動くより先にメモリ上限に達してしまうことがある(と理解している)。

なので、手動で実行すると問題が解消することがあった。

GCの呼び出しを完全に手動制御する gc_enable や gc_disable といった API もあるが、パフォーマンス上の止むにやまない問題がある場合を除き、あえて使うリスクを犯す必要はないと思う( gc_enable でも gc_collect_cycles は使える)。

おまけ: ECSのメモリ制限でOOM

AWS の ECS タスクで PHP プログラムを実行したところ、 ECS 側のメモリ制限に引っかかって OOM になる事象が発生した。

memory_get_peak_usage で取得したメモリ使用量上は問題はなさそうに見えたのだが、プロセスが実際に使用するメモリはまた別のようだ。

なので、 memory_get_peak_usage だけを元にプロセス自体のメモリ制限をカツカツにしないほうがよい。

stackoverflow.com

参考

チーム横断での定例的なやつについて思うこと

なぜ定例が生まれるのか

組織が一定の規模以上になってチームが分割されてきたりすると、どうしても横の繋がりが弱くなってくる傾向がある。

リモートワークしてたりすると、よりその力学は強くなることが多い。

そうすると「定期的に連携しましょう、コミュニケーションとりましょう」ということになり、定例が生まれる。

あるあるな課題

じゃあ横断的な定例を始めようとするとどうなるかというと、普段密にコミュニケーション取っているわけじゃないので、「共有のための共有」になりがち。

前提のコンテキストが足りないので、どうしても説明がほとんどになってしまう。

そうするとまぁ、「資料を共有してくればいい」「後で読んでおく」「全員が集まる必要はない」といった声が上がってくる。

定例をやめると

じゃあ定例をやめて資料を読んでおくことにするとどうなるかというと、読む人と読まない人が出てくるし、当然読むタイミングにもズレが生じる。

そもそも横の繋がりが弱くなっている現状があるので、優先順位が落とされる構造にある。

自分はできるだけ読もうとするタイプだと思うが、忙しかったら後回しにすると思う。

読まないやつが悪いといえばその通りなのだが、そういっても課題は解決されない。

仮に全員が目を通したとしても、そこで感じた疑問・感想・その他フィードバックが可視化されることは稀に思う。

明確な課題ならそもそも定例を待たずに顕在化してくるものだし。

どうするといいのかなぁ

個人的には共有のための共有の場はあってもいいと思う。

ただ中身の設計は工夫する必要があって、参加人数・組織構造・事業フェーズに左右されるので、開催しない方がよい場合もあるだろう。

そういった場合に、資料共有などで同期コストを最小にするなら、たとえば感想や疑問を気軽にフィードバックできるような、ネットワークが相互にリンクするような仕組みが必要なんだろうなと思う。

それは別に定例を開く場合でも同じなんだけど、同期コミュニケーションは自然と行いやすいので(質疑応答のタイミングがあったり)。

CakePHP3 から CakePHP4 に移行したら Middleware が動かなくなった #cakephp

現象

CakePHP3 から CakePHP4 に移行した際に、CakePHP がデフォルトで提供しているミドルウェアを拡張して利用していた Middleware が動かなくなった。(正確には意図しない挙動になった)

原因

CakePHP3 ではミドルウェア__invoke($request, $response, $next) を実装すればよかった。

しかし PSR 15 対応が行われたことにより、 __invoke メソッドは呼ばれなくなってしまった。

github.com

// Cake の提供している Middleware を拡張
class MyMiddleware extends ErrorHandlerMiddleware
{
    // 呼ばれなくなってしまった。。
    public function __invoke($request, $response, $next)
    {
        // do something
    }
}

対応

CakePHP4 では、 ミドルウェアに MiddlewareInterface を実装する必要がある。

ただし今回の場合は拡張なので、 ( __invoke ではなく) process($request, $handler) をオーバーライドすればよい。

// Cake の提供している Middleware を拡張
class MyMiddleware extends ErrorHandlerMiddleware
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // do something
    }
}

github.com

感想

拡張していることにより拡張元のメソッドが呼ばれることによって、パッと見動いているようになっていた。

それゆえに少し原因特定に時間がかかってしまった。

移行ガイドでも互換性があるように読み取れるし、拡張であったがゆえに見逃しやすい気がする。

参考

「リモートワークの達人」を読んだ

「リモートワークの達人」を読んだ。

以前「小さなチーム、大きな仕事」を読んでいいなぁ〜と思っていたんだけど、同じくベースキャンプ社 (元37シグナルズ) の本ということで長く気になってた本。

元々「強いチームはオフィスを捨てる」というタイトルだったが、改題したらしい。

タイトルからしてちょっとポジショントークに偏りそうだなぁとの印象があったので、なかなか手をつけていなかった。

ただコロナ禍もあって自分の職場もリモートワークが中心になり、なにか参考になることがあればなぁとようやく読んでみた。

本のタイトル変わったのもきっと時勢の影響だよね。

自分もリモートしたから共感できるところが多い

書籍の内容としては当然リモートワーク推しなんだけれども、実際に自分もやってみたからこそ共感できるような内容も多かった。

リモートでも一定同じ時間は一緒に働いたほうがいいとか、お互いの様子が分かるような工夫をした方がいいとか。

バーチャルな雑談の話とか、孤独に気をつけようといった、ちょっとウェットなことにも言及しているのは意外だったかも。

本質的にはリモートワークかどうかに関わらないような問題も多くて、リモートワークにすることで顕在化するような話もあるよなぁと思った。

過度な管理とか承認制とか。

ただちょっとずるいなぁと思うのは、リモートワークにおいて仕事の話とプライベートの話を区別せずに言及していること。

福利厚生的な側面と仕事の成果的な側面は、コンテキストが違いすぎるので、同列にされると違和感があるんだよな。

どうするのがいいのかなぁ

ボク個人としては、通勤がなくなったり、(今となっては)オフィスより快適な作業環境を整備したりしたことで、出社へのモチベーションは以前よりもだいぶ低くなった。

ただそれでも、この働き方が必ずしも「より良いものだ」とは言い切れない部分もある。

本の中でも「リモートワークは0か1ではない」と書かれていたけれども、試行錯誤していくしかないんだろうな。

個人としても、組織としても、社会としても。

「プロダクトマネジメント」を読んだ

プロダクトマネジメントを読んだ。*1

あんまり優先的に読む気もなかったんだけども、同僚たちの間で流行っていたおり、この本を前提に話されることが多くなりそうな雰囲気を感じたので目を通しておくか、という消極的なモチベーションで読んだ笑

言葉が先行しがちなプロダクトマネージャーやプロダクトマネジメントに網羅的に触れられている。

プロダクトマネージャーのキャリアパスであったり、プロダクトマネジメントについても組織戦略の話から現場でのプロセスやノウハウなど幅広い。

プログラマや開発者視点の本ではないので、共通言語を作りやすい面もありそう。「ビルドトラップ」なんかはそのものズバリだ。

この本を正解にする必要はないけど、取っ掛かりにして考えるのはよさそうだなと思った。

プロダクトマネジメント 読書メモ · GitHub

*1:実は読んだのは結構前なのだが、アウトプットをサボっていた