Open8

[Laravel] 私の現場でのディレクトリ構成の模索

こーひーあーるこーひーあーる

対象とするシステムとか状況とか

  • とある通販事業 年間数十億とかの売り上げ
  • 基幹システムが中心にあり、その周辺の注文インターフェースとして、Webサイト・電話・FAX・ハガキ、など
  • 基幹システムのリソースを操作するWebAPIをLaravelPHPで作ろうとした
    • 元々SOAP WebAPIがあったが、開発環境の構築ができなかったり、本番環境と開発環境でコードが違かったり、あと15年前に書かれていて、仕様がわかる人とかドキュメントがない
    • SOAP WebAPIはPHPではなくC#で実装されていた
      • バグとかがあっても直せないし、新しく追加開発も難しかったのでリプレイス開発
  • リリース当初
    • 開発者が私含めて2名
    • エンドポイントは10個程度
    • 顧客、在庫の機能
  • IT部隊はLaravel未経験 (現場で取り扱ってたのはCodeIgiter3とCake、ノンフレームワーク)
  • 基幹システムが続く限りWebAPIは稼働させ続ける必要があるため、長期的なメンテができるようにしておきたい (最低10年見てる)
  • チームの特徴や実績
    • WebMVCでの開発には慣れている CodeIgniterで実践してきた
    • DDDっぽい感じ?で開発を進めたAppが1つあったが...
      • 全然ドメイン駆動じゃない 画面駆動
      • ディレクトリの形だけDomain/Application/Infra/Presentation
      • SOLID原則が私含めて理解が薄い気がする
        • 単一責任の「単一」の定義
        • PHPの言語仕様としてあるinterfaceが全く使われてない
    • テストコードを書く文化がない
    • コードレビューの文化はない
    • プルリク(マージリクエスト)は特にレビューや承認受けずに、プルリク作った人がマージ実行
    • ソフトウェアアーキテクトやれるスーパープログラマとかスーパーエンジニアはいない
    • セルフホストのGitLabはあるが、CI/CDはない
      • Jenkinsで手動でjob叩いてデプロイしてる
    • コーディングスキルレベルはよくわからない
      • 絶対継承しないのにprotected使ってたり
      • 波動拳コードもある
        • でも本人のスキル、というより、通販事業が数十年やってたものなので、古い資産捨てきれないのもあるよなあ.と
      • コードを読んで仕様を理解する力はそれなりにあるはず
    • IT部門のうちWebApp作る人数は私含めて8人くらい
  • インフラ事情
    • Webサーバ系は全部オンプレに載せてる
    • ハイパーバイザの上に仮想マシン立てて
    • 基幹システムのDBはMS
  • 初期開発のスケジュールや作業など
    • 2024/9 ~ 2025/3くらいの規模 ( かなりゆったりしてたと思う)
    • 顧客系endpointが3個、在庫系endpointが4個くらい
    • 初期開発メンバのうち、私はフルコミット、もう一人はマネージャのため1日3hくらいしか作業取れない
    • CodeIgniter3のメンテがされてないため、CodeIgniter4や、Laravel、 あとGolangとかに私が想いを寄せていた
      • 技術的なプロトタイプを作成
      • 社内で活用していたCentOS問題もあり、AlmaLinuxとかでいろんなAppが動くかとか
      • 10年メンテしていくことを見据えて、PHPUnitありきで、サーバモジュールを差し替えやすいコンテナ(Podman)も調査
  • 非機能要件
    • 過去のSOAP APIが毎秒1リクエストくらいだった気がする
    • 顧客情報取得APIで0.05sec出せればいい感じだった
  • DBの設計開発はない(すでに基幹システムに数百テーブルがある)、データの変換とか移行は必要なし
  • 移行予定
    • とりあえずECWebクライアントから切り替え
      • SOAP FQDNを新WebAPIのFQDNに切り替え
      • 腐敗防止層とかないんでAPIの結果が使われているところを諸々書き換え
      • 新WebAPIを本番デプロイしてから、クライアント側を差し替え予定
こーひーあーるこーひーあーる

フレームワークや言語とか

  • CodeInger3
    • メンテされてない、セキュリティ的に脆弱
    • $this-load->library('xxx/yyy')
      • $this->library タイプヒンティングが辛い
    • REST WebAPIが(私の観測範囲では)あんまりサポートされてないイメージ
    • 使うならCodeIgniter4なはず
  • CodeIgniter4
    • CodeIgniter3から結構書き方変わってた印象
    • あんまり深くは見てない
      • 私が個人開発でLaravel触りまくってて、Laravelでの遂行イメージができたのでLaravelを中心に触ってた
    • Laravelよりちょっとパフォーマンスがいいところは気に入った
      • PHPもOPCacheとJITで頑張っているけど、パフォーマンスファーストなら、GolangとかRustとかでよくない?って思った
    • 頑張ればなんかいけるかもしれない
  • Laravel11
    • なんか作るときのレールが敷いてくれている
      • FormRequest, APIResource
    • なんかデファクトスタンダード感ある
      • 困ったらググれば (オリジナルアーキテクチャとか組んでなければ) 調べられる
      • 学習したい人は本もある
    • チームメンバに、昔触ったことあるが、めちゃくちゃ重い...って話があり、今の現場で避けられていた理由の一つになっていたが
      • 私が検証した時にはPHPは8系の世界、OPCacheもJITもあった
      • Laravelのconfigとかもキャッシュさせれば0.1sec以下のAPIは作れた
    • 私が慣れていたのでリードできそう
  • Golang (Echo)
    • コンテナのメモリ効率がNGINX+PHPFPMと比べて軽量
      • でも非機能要件がシビアじゃないしオンプレにサーバリソース余ってるので...
    • パフォーマンスもPHPより速く見える (実装次第かも)
      • でも非機能要件...
    • サードパーティのオープンソースとかの選定が私にとって未知数だったので結構勉強する必要ありそう
    • Golang自体私もあんま触ったことないので、リードしづらい
  • C#
    • 現場にプロダクションコード書ける人があんまいないイメージ
    • 私はUnityC#を趣味開発
    • App開発とは別に運用チームがいて、そちらではC#詳しい人もちょっといるが、あくまで運用がメイン
こーひーあーるこーひーあーる

インフラ

  • 基幹システムのDBもオンプレ上にある
  • 今回のWebAPIは社内システム用、インターネットに公開しない
  • 仮想マシンにNGINX + PHPFPM入れるか、仮想マシンの上にPodmanコンテナ載せるか? で迷った
    • 仮想マシンの上にコンテナ乗せたらオーバヘッド無駄では
    • それよりも、10年メンテしたいという背景で、アプリ開発者の方でモジュール差し替えとかdbドライバのバージョンアップとかしたかった
      • これとPHPUnitを整備して組み合わせて、安心してバージョンアップさせたかった
    • Podmanコンテナが使いたいです、なぜなら使いたいからです、で押し通した (ちょっと無理矢理だったかも)
    • でもPodman自体のバージョンアップは必要だよね
  • 社内文化的に、Webサーバであれば他のWebサーバでも同じ技術を活用するスタンス (AサーバではPHP、BサーバではRails、CサーバではGolangとか... ) をあんまり許容してないが...
こーひーあーるこーひーあーる

Laravelのディレクトリ構成どうするか 2024/冬

Laravel使い、多分みんな見てる? ものをお手本にした

https://zenn.dev/mpyw/articles/ce7d09eb6d8117

  • Laravel Wayに乗っかる
    • 困ったら公式ドキュメントを案内
    • 今のチームメンバでもできるはず
  • UseCaseのところも1クラスに__invoke()
    • Eloquentに依存しちゃってOK
  • Enumsを切った
    • 定数とか離散値
- app
    - Http
        - Controllers
        - Requests
        - Resources
    - Models
    - Rules
    - Enums
    - UseCases
        - xxxAction
        - xxxInput (ControllerからUseCase Actionに渡すDTO)
        - xxxOutput (UseCase ActionからControllerに返すDTO)

xxxInputとかxxxOutputなんですけど
現場のチームのコード見ると、連想配列を引数にします!返り値にします!みたいなコードが9割で。
それVSCodeとかPHPStormで追うのキツくないか?っていう思いで
タイプヒンティングで色々追えたり補完できるようにしたほうがいいでしょ?っていう感じで。
あと、xxxInputの方は、パラメタ追加されても、Actionのシグネチャ変えなくていいかなと思いました。

UseCaseの例

class StockCancelAction
{
    public function __invoke(StockCancelInput $input) : StockCancelOutput
    {
        // 引き当てを取得
        $allocation = Allocation::query()->firstOrFail($input->xxx);

        // 当日の引き当てのみキャンセル可とか
        if ($allocation->date != $now) { abort();}

        // 引き当て削除
        $allocation->delete();
    }
}
こーひーあーるこーひーあーる

メンバーの開発の感想、初期リリース、追加の話...

  • 私含めて2名で開発してたけど、そのうち私じゃない方の開発の感覚
    • UseCase以外はすんなり開発できている感じ、だいたい書き方が統一できてるし、どこに何を書けばいいかもOK
    • UseCaseの中でActionを細分化した
      • この時の切り方がわからない
    • PHPUnit書いてくれない
      • OpenAPIと実装があっているか?は書いてくれたが...?
        • UseCaseのPHPUnitがない
          • Controllerのテストセットアップ面倒だから、UseCaseこそ充実して欲しかった
    • 「共通」の処理を書きたいという要望
      • Commonとか...
      • 私の直感というか感覚で、いろんなところで共通して使える処理というのはなるべく避けてたが、なぜダメかが説明できなかった
    • 連想配列を引数とか返り値にしちゃう傾向がまだある
    • SQLの書き方がEloquent,クエリビルダ、PDOとかあって悩んだ
  • 迎えた初期リリース
    • クライアントとなってるECサイトでログインできないバグ発生
      • SORP APIで不整合データをケアするロジックがあったりしたが、今回消したので..
        • 歪な形で修正を施す
    • それ以外はやばいやつは起きなかった
  • 追加の話
    • 当初は顧客と在庫endpointだけだったが、通販事業の拡張のために、商品と受注も作りたくなった
    • 在庫endpointも受け払い表とかが追加したり
    • ある商品コードだけの商品売り上げ代金を取りたいAPIとかの話も
    • クラウド上のAIチャットbotから、このapi叩いて情報取りたいって話も出てきた
    • とりあえず見えている範囲で20endpointくらいにはなりそうだが、10年メンテするからなあ...
    • 商品とか受注作るにあたって、メンバーが2人追加され、合計4人に
こーひーあーるこーひーあーる

受注WebAPI作っている人からの質問とか困り事とか

class OrderStoreAction
{
    public function __invoke(xxxModelData $data)
    {
        // workテーブル作成
        // 配送指示パターン
        // 在庫引き当て
        ...
        ...
        ...
        ...
        ...
        // ブラックリスト
        // 受注登録
    }
}
  • Actionの条件分岐を減らしたい?
    • Interfaceに振る舞い定義して、Factoryパターンで引数に応じてインスタンス変えてください、って回答
    • 読み手がついて来れるか...?
  • Actionの引数がInputでなくなってる、なぜ
  • Setterがある小さいクラスの使用
    • 直感的にダメだと思ったが、なぜダメかを説明できない...
    • イミュータブルではなくなるよね
    • でもイミュータブルで亡くなった時の欠点を伝えられない...
  • 参照渡しで引数のデータを変更する
    • これもあんまり見通し良くなくない?って思ったが説明ができなかった...
  • OrderData $orderDataっていうデータクラスの変数を繰り返し上書きしたり
    • $orderData1, $orderData2, ... っていうわけにはいかないと思うが...
    • $orderDataっていう命名とかデータクラスがあまりよろしくない??
  • クラス名が class EC とかになってる
    • namespaceあるから app/UseCases/Order/Device/ECとか 大丈夫っていう説明受けたがどうなんだろ
  • Factoryパターンとかの標準デザパタに、独自のクラス名をつける
    • ModeとかSwitcherとか
    • それは読めるのか?
    • Modeとかの方がわかりやすいというのだが、それは誰にとってか?
  • 在庫endpointで使用しているUseCaseから、受注で必要な処理を切り出してCommonとして使いたい
    • 在庫と受注だけだと確実にわかっているならいいと思うが、これ領域が増えたら「共通」の範囲が変わらないか..?
  • 生成AIの回答では...こうするのが正しい 的な回答がくる
    • この現場の特性や制約を正しくプロンプトできていれば良さそうに見えるが...
    • どう反論したり説明すれば..?

あと、自分が感じていることが全て正義とも限らないので
その人の意見に流されがちになる...

こーひーあーるこーひーあーる

質問をしてきた2名とも、「UseCase膨らんできた時にどうすんの?」で迷われていたので
構造の変更を考えてみた。

案1

- app
    - Http
        - Controllers
        - Requests
        - Resources
    - Models
    - Rules
    - Enums
    - Contexts
        - Order
            - UseCase
                - xxxAction
                - xxxInput (ControllerからUseCase Actionに渡すDTO)
                - xxxOutput (UseCase ActionからControllerに返すDTO)
            - Domain
                - xxxValueObject
                - xxxEntity
            - Infra
                - xxxRepository
  • オニオンアーキテクチャLike
  • Domain, UseCaseがクリーンになって、そこテスト書いていけばええやん
  • SOLID原則に基づいているわけで、OOP的なアプローチとしては1つの答えなはず
  • DDDっぽい感じ?で開発を進めたAppが1つあったが...全然ドメイン駆動じゃない 画面駆動

    • あんまりよくない実績があるのでどうするか...?
    • 主導する人がわかってないといけない
    • Eloquent → QueryBuilderへ?
      • それ捨てたらLaravelのいいところ減らないか...?
      • Entityへの詰め替えがActive Recordの良さ消し去りそうで.l.l
    • レビューがない文化をどうにかしないといけないのもある
    • 採用している給与レンジからしても、ここら辺わかる人はとってない
    • 勉強して解決というアプローチもある
  • コーディングレギュレーションが多めだと感じる
    • Laravel Wayに大部分乗っかって、残り1割2割を non Laravel Way が、みんなついていけるレベルな気がしている、感覚論
    • ググれば情報はヒットするし、書籍もあるけど...
      • 正しく理解されるか? ただでさえ私は他の人に言葉でうまく説明できない
  • 10年のメンテナンスを要求されているなら、フレームワークに頼らない道のりもありなのかな

→ レビューがない、スキルレベル、Laravel Way以外で守るレギュレーションに皆がついていけんのか?? などの感覚で、今はその時ではないっていう感じ
→ 2年後とか3年後に必要があれば引っ越しはできるようにしておきたいかな、、、

案2

- app
    - Http
        - Controllers
        - Requests
        - Resources
    - Models
    - Rules
    - Enums
    - Contexts
        - Order
            - UseCase
                - xxxAction
                - xxxInput (ControllerからUseCase Actionに渡すDTO)
                - xxxOutput (UseCase ActionからControllerに返すDTO)
            - Internal
                - xxxValidator
                - xxxTransfer
  • Controller → UseCase依存
  • UseCase → Internal依存
  • Internalは好きにクラス設計して良い、作る機能とかで適用可能なデザパタとか変わるんじゃないかしら
  • SOLID原則に従うのは諦め チームスキルがついて来れない (やれる人だけInternalでやってもらえれば)
  • UseCase(Input) : Output まではレギュレーションにしてしまう
    • 一応私のぞいた2名に途中までコーディングしてもらったが、ここら辺は守られた実績がある
  • ContextAからContextBへの参照は禁止
    • 「共通化」にニーズがあるようだが...
    • これまでのプロジェクトのコードで、ビジネスロジック実装で共通化がうまくいっているケースが思いつかなかった、大体そのクラス変えた時の影響範囲が怖くてさわれない、変更できない (ソフトじゃないよね)
      • 消費税対応とか 期間と%を1箇所変えれば理論上はいいはずだが..?
      • ソースコードgrep検索して調査してたし
    • 冗長なコードが増えるので、修正漏れが起きるんじゃないか?
      • その通りなんだけど、共通化の弊害よりかはまだマシな気がする
      • ロースキルメンバーでもできるし
  • UseCaseとInternalからLaravelへの依存は基本Eloquent/Modelに限定
    • RequestクラスとかAPIResourceとかは関与しないように
    • ビジネスロジックの実現に集中してほしい
  • Internalにただ責任転嫁しているだけなようにも見える
    • このアプローチで解決できるのは、UseCaseの肥大化だけな気がする
  • 依存関係の禁止をphpstanとかで検出したい
  • Internalを改造すれば、追々オニオンアーキテクチャに引っ越し可能な気がする

案3

- app
    - Http
        - Controllers
        - Requests
        - Resources
    - Models
    - Rules
    - Enums
  • Serviceレイヤ的なものを立てるからディレクトリ困るわけで、、、

  • 全部Laravel Wayに乗っかればいいのでは??

  • どこかにビジネスロジックを書くわけだが、、、

    • テストコードを書く観点で、ControllerではOpenAPIのIN/OUTを検証するのがベターな気がしていて、機能単位のテストはやっぱService的なところにおいてテストかきたいモチベがある

    現実的には案2なんじゃないかなーと思いつつ、