ログ日記

作業ログと日記とメモ

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

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



旧来の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を同じ場所に移動したりして、徐々に変更して試している。

Debian stretch で MySQL 5.6 のアップグレード

本家のリポジトリを使ったMySQLの5.6.47 から 5.6.48 のアップグレードが失敗する。


/usr/bin/mysqld_safe のスクリプトが間違っているっぽい。
データディレクトリが /usr/data になってしまう。
OSもMySQLも古いバージョンだから、メンテされていないのかもしれない。

ファイルを書き換えたり色々試したけど、インストールの前後の処理で上書きされたり、post install が止まったりして不安定だった。

ln -s /var/lib/mysql /usr/data

取り急ぎリンクを張ってやり過ごした。


apt でエラーが出たときのコマンドメモ。

dpkg --audit
dpkg --configure --pending
apt --fix-broken install

色々案内されたけど、インストールスクリプトが間違ってたらどうしようもないよね。


apt install --reinstall mysql-server mysql-community-server ではうまくいかず、一旦removeしてinstallした。
パッケージを消してもデータはそのまま残ってるので後でinstallすれば使える。

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

ジェネリクスの実装が

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

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

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

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

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

Let's Encryptで後からサブドメインを変更する

既に取得済みのSSLに、後からサブドメインを追加したくなった場合。

削除して再取得するしかないのかなと思ったけど、ドメイン名 common name の更新もできるようだ。
github.com

certbot certonly --force-renew --cert-name example.com -d example.com -d '*.example.com'

www.example.com を消してワイルドカードに変更した。
ドメインリストの最初のドメインがcommon name。

コマンドは素っ気ないけれど、最初に所有者チェックの方法の選択肢を聞かれて、その後に追加されるドメインと削除されるドメインの確認が表示される。

certbot certificates

このコマンドで確認ができる。

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日以内。

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

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

DBでSQLのテーブル名に別名を付けるときのルールと複数形

データベースのテーブル名を複数形にするかどうか、作るたびに悩む。
Qiita の記事 https://qiita.com/siinai/items/d4274c95fcdde3fd7295 のコメント欄に良いリンクがあった。

SELECT id, name, description FROM products product
WHERE product.name = ‘foo’ AND product.description = ‘bar’
https://medium.com/@fbnlsr/the-table-naming-dilemma-singular-vs-plural-dc260d90aaff


過去に書いた記事*1 を見返していて、そこのリンクにも同じような記事があった。

テーブルに別名を付ける場合、テーブル名の単数形を使用する

SQL の命名規約とフォーマット - ぐるぐる~

Railsとは無関係の文脈だと思うけど、こういう派閥も昔からあったのか。このときは流し読みでスルーしていた。
 
T1 とか tbl_kbn とかは以ての外だけど、この複数形を単数形にする別名のルールは良いかもしれない。

 

SELECT * FROM Customers AS Customer WHERE Customer.FirstName = 'John' - Customers refers to the whole table while Customer refers to the current row.

visual studio - Database tables naming, plural or singular - Stack Overflow

こちらも古いけど複数形テーブルに単数形の別名を付けている。

sql - Table Naming Dilemma: Singular vs. Plural Names - Stack Overflow
こっちは単数形推しで理由がまとまっている。


自分は最初のQiitaの

「テーブル」自体が「複数」そのものではない

テーブルの名前って複数形?単数形? - Qiita

とか
最後のstackoverflowの

Reason 1 (Concept). You can think of bag containing apples like "AppleBag", it doesn't matter if contains 0, 1 or a million apples, it is always the same bag. Tables are just that, containers, the table name must describe what it contains, not how much data it contains. Additionally, the plural concept is more about a spoken language one (actually to determine whether there is one or more).

sql - Table Naming Dilemma: Singular vs. Plural Names - Stack Overflow

とかと同じで単数形にしている。


かと言って複数形にすることは無いかというと、そうでもない。
例えば何らかの理由でファイルに書くようなアプリケーションの設定項目をDBに保存するときは application_settings というただ一つの行のみを持つテーブルを作るかもしれない。
これは the application settings であり、一つの行に複数の設定が詰まっているので意味的に複数形がしっくりくる。行が複数ということではない。

他にも、category_attributesのように何かの情報を詰め込んだ行を持つテーブルは複数形にするかもしれない。
そう考えると全体的に単数形や複数形に統一するという考えとは相反することになりそうだ。