ログ日記

作業ログと日記とメモ

PHPStanのextensionでnamespace・class名の依存関係チェックする

前に書いた記事
DDDのさわりをやろうとした - ログ日記
ここで作ろうと思っていた拡張を作った。


github.com

サンプル。
https://github.com/nishimura/phpstan-namespace-dependency-sample


実際に作ってみると、各レイヤーごとの依存関係の認識が曖昧なことに気が付いた。

DomainはApplicationServceで生成するけど、生成したDomainのオブジェクトをそのまま返却しても良い?
ドメインのオブジェクトをそのまま返却してしまうと、Presentation層などでドメインの複雑な操作をしてしまえるので、Dtoに詰め替えてApplicationServiceResponse的なオブジェクトを返却すべきだった?
Javaなら例外を各層ごとにわざわざcatchして詰め替えてthrowするのが普通に見られるけど、PHPだとそこまではしないよね?
MVCのモデルをビューにそのまま渡して良い?簡略化したMVC2的なものは、ViewがModelに依存してしまっているよね?Modelのメソッドが変わったらViewに変更が必要だよね?
テンプレートで$entity->save() とかできるオブジェクトを扱うべきではないよね?
とか。


まあPHPだしそこまで細かくインターフェースを分離しなくていいかと思ってたことが色々とあぶり出された。

もっとちゃんと設計している人はこの辺をうまく整理して実装しているんだろうね。
自分は、最初は気を付けていてもいつの間にか忘れてメインのロジックが詰まったオブジェクトをテンプレートに渡したりしていた。
ある程度の規模までとかあまり複雑でない処理ならそっちの方が早いから、問題になることもなかった。

そんな省略したCRUDを早く作るための書き方に慣れている人でも、強制的にDDDできるツールになった。(のかどうかはこれから時間ができたら試す)

PHPの開発環境とライブラリと振り返りと近況

昔のコードを触る機会があった。
PHPの自作フレームワーク現状まとめ - ログ日記
この辺のやつ。
もう7年も前か…。

当時はテンプレートエンジンに変数を渡す場合、アクションコントローラーでメソッドを呼び出すのが流行っていた。

<?php
class MyController {
    public function index() {
        $this->set('foo', 1);
    }
}

<?php
class MyController {
    public function index() {
        $this->foo = 1;
    }
}

のような感じ。
どこでも変数を設定できるから作るときは楽なんだけど、コードが膨れてきたり後から見返すとあちこちでテンプレート変数が設定されていたりして大変だった。
それから、自分の作った機能なのに自動設定しすぎで結構忘れていた。

PHPのフレームワークを作った - ログ日記
@var でリクエスト変数のIDからDBアクセスして取得したオブジェクトをインジェクションするのはやりすぎだったかもわからん。
慣れると開発がめっちゃ早いんだけど。

$db->save($obj) で保存するのは楽だし、where(['col' => $value]) でSQL作るのも楽。
多少遅くてもいいならORMも便利だったなあと思い出してきた。

ただし、arrayや動的生成を多用していて型チェックされないのは不安だし、定義にジャンプできない部分が結構あった。
テンプレートエンジンの箇所でバグが出るのも若干不安。


それで忘れていた考え方の振り返り。
ぼんやり振り返り - ログ日記
この頃は、動的にコードを生成したり、流れるようなインターフェースにしようとしていたりしていた。
静的解析は使っていなかったし、コード補完もほとんど使ってなかった。書き方の問題もあって精度が低かったし。


その後、フルスクラッチの案件はあまりやらなくなって、WordPressとかEC-CUBEとかどこかの独自システムとかの修正等をやっていた。



そして最近、再びフルスクラッチでシステムを作った。
今見たらPHP 6万行、HTML 2万5千行、JavaScript 1万行あった。
PHPのテスト 8千行、CSS 1万行 も入れたら、10万行超えるね。
ここまでやったのは初めてかもしれない。

PHPのツールは
・自作フレームワーク
・自作ORM
・自作テンプレートエンジン
jQuery

から

・自作DIコンテナ PHPのDIコンテナを作った話 - Qiita
・FastRoute
・BladeOne
・Closure Library

に変えた。
速度重視の方針なので、基本ベタ書き。
フレームワークを使うと自動生成に頼っちゃうけど、手書きだとクラスを自分で書くことになる。
その代わりPHPStan のレベルMAXでチェックできて、クラスの定義もすぐ見れて安心感がある。
PHPStanに慣れてきてジェネリクスまで使えるとなると、今はBladeOneのテンプレートエンジンを外したくなってるところ。PHP側でめっちゃ細かく型チェックしてるのにテンプレートエンジンがザルだと意味ないじゃんっていう。

JavaScriptは速度重視なのでClosute Compiler、それならClosure Libraryだろうってことで。
PHPフルスタックは嫌だけど、JavaScriptはnpmの細かいライブラリの方が嫌だ。なので依存はClosure Toolsだけにする。
CSSもClosure Stylesheets。最終更新めっちゃ古いけど。

PHPは、FastRouteとDIコンテナとzend-diactoros とBladeOne、セッションライブラリ、Monologやちょっとしたツール系を繋げる薄いフレームワーク的なものを書いた。1000行いってないのでフレームワークというほどでもない。



開発環境は
・etags, gtags
PHPのモデル、コントローラー、HTML、CSSなどで分けてターミナルからEmacsを複数ウィンドウで立ち上げて並べる
・ローカルのApacheにバーチャルドメインで開発環境を設定、DBその他サービスもローカルにインストール
・GitLabから独自hook呼び出し

から

・gtags, ac-php
purpose-mode でウィンドウを複数立ち上げて並べる
systemd-nspawn で開発サーバーを用意
・GitLab CI

という感じに変わった。
あまり大きくは変わってないか。
Ansibleを使い始めたのが大きいかな。全体的に一周遅れてる感があるけど、KubernetesやDockerはオーバースペックなのでしゃーない。
DockerはCIのテストで使ってる。

サーバーは相変わらずさくらクラウドとさくらVPSコスパが良い。
大量アクセスをさばくのではなくて、1回のリクエストの応答を0.2秒以内にしたいとかいう方向の速度を出したいという要求がある。
個人的にも、最近のスマホ版Webは好きじゃない。細かくリクエストが分かれていてローディングアイコンがいつまでもクルクルしているし、もっさりしているし、スクロールが飛ぶし、リッチさにハードスペックが追いついてない感じがしている。
スケールアップで十分な規模のサイトをスケールアウトするのはナンセンスな感じもある。

あと、ネットショップばっかりやってるっていうのもあるかもね。
トランザクション大事。




そんな感じで最近はやっている。
でもさすがにSQLベタ書きは疲れてきたので、型チェック厳しめのORMを作りたいとか思っている。
関数呼び出しとオブジェクト生成って地味に処理時間取られるからあまり複雑なことはやりたくないんだけどね。管理者側の単純なCRUDならORMの方が開発コストが大幅に低くなるので、迷っている。
逆に、表側は長大なSQLを書いているのでORMは無理。

ひとまず次にやることは、テンプレートエンジンを外したり、コードを整理したり、テストを書いたりかなあ。
テストは金額計算と日付計算しかやってない。これで十分といえば十分だけども。

あとはE2Eテストか…。
何個か書いたけど、重いので増やしたくない方向に意識が傾いてしまう。
push時に実行するテストと毎日1回実行するテストを分ける、とかかなあ。


テンプレートエンジンを外すというのは、
型がある時代のPHP、テンプレートエンジンでも型チェックしたい - Qiita
こういうこと。
あと、これだとhtmlspecialcharsを忘れて危険なので
PHPStanで素のPHPをテンプレートとして使うとき、htmlspecialcharsをチェックする - Qiita
これもセットで。
少しずつやってる。

プログラムの分け方とディレクトリ構造

最近、プログラムのディレクトリレイアウトというかファイルを置く場所を変えようと試みている。



旧来のMVC的な構造だと、ControllerやModelのディレクトリを分ける、レイヤーごとにまとめる、という感じになると思う。
商品情報表示画面、その管理画面、CSVダウンロード機能があるとすると、

old1
├── src
│   ├── Controller
│   │   ├── Admin
│   │   │   ├── ProductController.php
│   │   │   └── ProductCsvController.php
│   │   └── ProductController.php
│   ├── Dto
│   │   └── ProductDto.php
│   ├── Model
│   │   ├── CsvModel.php
│   │   ├── ProductCsvModel.php
│   │   └── ProductModel.php
│   └── ViewModel
│       └── ProductBehavior.php
└── templates
    ├── admin
    │   └── product
    │       └── product.blade.php
    └── product.blade.php

こんな感じになると思う。
ModelはDomainとかServiceとかInfraとか、そういうのでも良い。

ここに、期間限定キャンペーン用商品に関する機能を追加するとしよう。
特殊な画面と特殊な処理なので、DBに数項目追加するといった対応はできないとする。
そうすると

old2
├── src
│   ├── Controller
│   │   ├── Admin
│   │   │   ├── CampaignProductCsvController.php
│   │   │   ├── ProductController.php
│   │   │   └── ProductCsvController.php
│   │   ├── CampaignProductController.php
│   │   └── ProductController.php
│   ├── Dto
│   │   ├── CampaignProductDto.php
│   │   └── ProductDto.php
│   ├── Model
│   │   ├── CampaignProductCsvModel.php
│   │   ├── CsvModel.php
│   │   ├── ProductCsvModel.php
│   │   └── ProductModel.php
│   └── ViewModel
│       ├── CampaignProductBehavior.php
│       └── ProductBehavior.php
└── templates
    ├── admin
    │   └── product
    │       ├── campaign_product.blade.php
    │       └── product.blade.php
    ├── campaign_product.blade.php
    └── product.blade.php

こんな感じのディレクトリレイアウト、クラス名、テンプレート名で機能追加するだろうか。

フレームワークなんかでは専用コマンドが用意されたりしていて、scaffoldと呼ばれたりもする。
これで一気にController、Model、Viewのファイルを生成したりするわけだけれども、そもそもの構造が複雑だからこそ専用コマンドが複雑になるとも言える。

自分はこの自動生成は好きではない。AbstractControllerやAbstractModelも好きではない。できるだけシンプルにしたい。
最近その辺りの構造を大きく変えようと思っている。


基本的には層でディレクトリを分けるのではなくて、画面単位で分ける。

new1
└── src
    ├── Model
    │   ├── CsvModel.php
    │   ├── ProductModel
    │   │   └── Dto.php
    │   └── ProductModel.php
    ├── Page
    │   ├── Admin
    │   │   └── Product
    │   │       ├── CsvModel.php
    │   │       ├── CsvPage.php
    │   │       ├── Dto.php
    │   │       ├── Html.php
    │   │       └── Page.php
    │   └── Product
    │       ├── Dto.php
    │       ├── Html.php
    │       └── Page.php
    └── ViewModel
        └── ProductBehavior.php

こんな感じにする。
ControllerはMVC的なコントロールはしないので単にPageという名前する。
テンプレートエンジンはやめて、Html.php に昔ながらのやり方で書く。とは言ってもネームスペースとクラスは使う。
Page.php から ModelやViewModelを使って何らかの処理をした後に、Dtoに詰め込んでHtml.php に渡す。これを同じディレクトリに置く。
2箇所以上で使う処理は旧来のModelやService等に分ける。1箇所からしか使われない処理はPageクラスの近くに置く。先のことは考えない。他のPageでの処理を使いたくなったその時に共通化して移動する。

キャンペーン機能を追加した後はこんな感じ。

new2
└── src
    ├── Model
    │   ├── CsvModel.php
    │   ├── ProductModel
    │   │   ├── CampaignInterface.php
    │   │   └── Dto.php
    │   └── ProductModel.php
    ├── Page
    │   ├── Admin
    │   │   └── Product
    │   │       ├── Campaign
    │   │       │   ├── CsvModel.php
    │   │       │   ├── CsvPage.php
    │   │       │   └── Html.php
    │   │       ├── CsvModel.php
    │   │       ├── CsvPage.php
    │   │       ├── Dto.php
    │   │       ├── Html.php
    │   │       └── Page.php
    │   └── Product
    │       ├── Campaign
    │       │   ├── Dto.php
    │       │   ├── Html.php
    │       │   └── Page.php
    │       ├── Dto.php
    │       ├── Html.php
    │       └── Page.php
    └── ViewModel
        ├── CampaignProductBehavior.php
        └── ProductBehavior.php

ここでも各レイヤーをまとめて同じディレクトリに追加する。この方が見やすく、依存関係も分かりやすい。
リファクタリングも手軽にできる。


前提として、DBで

select * from product

のようなことはしない、という要因もあるかもしれない。

select product_id, name, description from product

のように、必要な項目だけ取得する。
なおかつ、型指定ありのDtoに詰め替える。
逆に言うと、型指定ありのDtoで必要な項目だけ詰めて渡すようなことをやり始めると、あまり共通化ができない、とも言える。

Html.php もPHPStanで静的解析に含むので、かなり安心感がある。

もし共通化してDBの返却値も共通化したりすると、富豪的なDBアクセスとオブジェクト生成で遅くなる。
各画面でDBの同じテーブルにアクセスしたとしても、必要な項目まで同じということはあまり起こらない。それでも共通化するとしたら、各画面で必要な項目をどんどん追加していくことになってDB処理が膨れていく。
機能を削除したとしても、迂闊に項目を消したりできない。

そういった理由で、今は徐々にテンプレートエンジンを使うのをやめて各ページの処理とHTMLを同じ場所に移動したりして、徐々に変更して試している。

PHPStan バージョン0.12.24から PDOStatementが Traversable でなくなってしまった件

ジェネリクスの実装が

@implements Traversable<array<int|string, mixed>>

で固定されるようになった。

FETCH_CLASSとジェネリクスを組み合わせていい感じにマッピングする方法が使えなくなって、ちょっとめんどくなってしまった。
都度 @var が必要になる。

一応報告しておいたけど。
https://github.com/phpstan/phpstan/issues/3509
closeされてしまった。

素のPDOStatementを使いつつがっつりクラス定義する人が居なくて、あまり認識されてないのかもしれない。

DDDのさわりをやろうとした

ちょっと一部だけ複雑なプログラムがあったので、PHPでDDDっぽいオブジェクト指向をやろうとした。
なかなか大変だった。


まず静的解析による型チェックは必須。
型が自動でチェックされないDDDは、適当に書き捨てたプログラムより分かりにくくなると思った。

パッケージプライベートかfriendが欲しい。インナークラスも欲しい。
油断すると層を飛び越えてnewしちゃう。
例外も層を飛び越えたくないから、一度catchで掴んで新しい例外で包み直してからthrowしたい。でもめんどくさいしもうJavaじゃないかっていうコードになる。

あとテンプレートエンジンは使わず素のPHPで書くことが必要。
元々phpstanを使い始めてからテンプレートエンジンのデメリットが目立ってきたところで、複雑なオブジェクトを扱い出すとテンプレートエンジンのデメリットがますます強調されてきた。
折角の型チェックがテンプレートエンジンを使うことによって全て無に帰す。これはよくない。


それからDIやORMで文字列を指定して何かを取得する処理は全てやめた方が良い。
json_decodeも危険だしデータをarrayに詰め込むなんてもってのほか。


これは結構な思考の転換が求められる。



ひとまずパッケージプライベートのような仕組みは必須レベルで必要な気がする。
余裕ができたらphpstanでパッケージのnamespace階層をチェックするextensionを作ろう。

PHPStan が早い

PHPStanのデフォルトのルールを変更したくて、どうすれば良いか分からなかった。
デフォルトの config.neon を変更する方法を教えてってissueに書いたら10分後に返信来た。
scopeClassを変更すればgetType()で何でもできるよ、でも全体で一つの拡張しか使えないから実験的な機能だよ、だそうだ。

それから次の週に、ArrayAccessのジェネリクスが動かないのでバグレポートした。
これも数時間で返信が来た。
それでバグじゃなくてfeature-requestに分類されて、もしかして結構後回しになっちゃうのかな?と思って次の週に自分でプルリクした。

正直実装方法が分からなくて不完全なコミットだった。プルリクにドラフトの機能があったんだね。そっちにすれば良かったと後で気付いたけど変更出来ず…。
コメントもらったり修正したりしてたら作者が関連コード見つけて解決してくれたっぽい。テストだけ取り込まれた。
その間も1日以内。

それで次の日にはマイナーバージョン上がった版がリリースされてた。(リリースページに名前があるよ!)

進むの早すぎない?というか作者の返信が早いし活動もずっとやってる感じがある。
次回があればもう少しまともなプルリクにしようと思うけども、ソース読んで理解するより早く開発が進んで追いつけない気もする。

フラッシュセッションにハマる

前にも同じ問題でハマったのに忘れていて再びハマったメモ。


リダイレクト前にセッションに値を保存して、リダイレクト後のページで取り出して、削除するような一度だけ使うセッション変数にはフラッシュセッションを使う。
セッション系ライブラリには大抵 flash メソッドがあると思う。
リロードにや更に次のページにも対応するために keep メソッドなんかも用意されている。

で、何故か値が消えていることがある。


詳しく処理を追うと、フロントコントローラーでファイルが存在しない場合は全てPHPが起動するようになっていて、not foundでもPHPが実行される。
このときログインメニューやメッセージ用にnot foundページにも関わらずセッションを使っていたりすると、ここでフラッシュセッションがリセットされる。


画面遷移のどこかで画像などがnot foundになった場合に、一見正常フローのようでも「画像呼び出し=>ファイルがない=>phpでnot found用処理」となって見えない画面遷移とフラッシュセッションのリセットが動いていることになる。


よく考えれば当然のことなんだけれど、スマートな解決方法が思い付かない。
とりあえず404や500のときはkeepする、あるいはフラッシュセッションを消す処理を実行しない、というようなちょっとややこしいプログラムを書いた。