Composer の自作パッケージの作成を通して php 開発環境を整えてみた

# はじめに

これまではノリと雰囲気で php と関わってきましたが、ついに真っ向から向き合っていかなければいけない時期が来たようです…！

というのも最近、Laravel フレームワークで作成されたアプリの本番コンテナを作成するという業務に関わったことで、アプリを弄れないとインフラ側で強引に頑張らねばならなくなり、労力の割に複雑で認知負荷が高くなってしまったり拡張性に乏しかったりと、非常に辛いことを実感しました。

そこで、PHP の基礎を理解するにあたってとりあえずは Composer の自作パッケージの作成を通して色々と学んでみましたので、得た知見や疑問・課題などを書いてゆければと思います。

ノウハウ共有というよりかは、体験談・感想文 的な要素が強いかもしれませんが、よければご覧下さい。

# ネタとして Composer パッケージ作成を選んだのはなぜ？

PHP のパッケージ管理といえば Composer であり、色々と使いこなせていない機能があることをなんとなく知っていました。
自分は SRE なので、PHP アプリの CI/CD のベストプラクティスを身につけるにあたっても Composer でできることを理解しつつ試して使いこなせるようになるのは、個人的興味と業務どちらも両立できてよいのでは、と考えたためです。

# 成果物

https://github.com/kyohei-harada/composer-tutorial/tree/qiita_articles/2024-03-23

上記コードについて、個人的に拘ったポイントや気付き・学び、苦戦した点などについて解説してゆきます。

# 各ディレクトリ・ファイルの解説

## .devcontainer ディレクトリ

VSCode DevContainer で立てたコンテナ環境を利用しました。
今回は Composer パッケージ開発であり本番デプロイを考慮しなくてよいので [Dev Container Templates](https://containers.dev/templates) のイメージを利用しました。
ですが例えば Web アプリなど本番環境を考慮しなければならないアプリの場合は docker hub のイメージを利用することで開発(＝ローカル)と本番で共用のコンテナイメージを使えるようにすると、環境差異を最小化でき環境固有の不具合の発生を抑えることができそうです。
また注意点として Dev Container Templates でサポートされる各言語のバージョンは比較的新しいものしかサポートされませんので、既存の古いアプリのプログラミング言語のバージョンに合わせる、といった場合も docker hub のイメージを利用するのをおすすめします。

参考に、Dev Container の PHP イメージで有効なタグは下記の通り、本記事作成時点では 7.4 ～ 8.3 のようですね。
https://mcr.microsoft.com/v2/devcontainers/php/tags/list

DevContainer の個人的推しポイントをいくつか挙げておきます。
- Dockerfile や Docker Compose を指定して .devcontainer.json を作成できる
  - 既存の設定を流用しやすい
- コンテナ内にインストールする VSCode Extension も明示できる
- git, awscli などのちょっとしたツールも [Features](https://containers.dev/features) として気軽にインストール可能

VSCode Extension や Features を明示できると、開発環境の再現性・統一性を担保できとても嬉しいですね。

## .vscode/launch.json ファイル

デバッグに関する設定ファイルですね。
VSCode で作成できる PHP のデフォルトから下記を変更しました。
```bash
vscode ➜ /workspaces/composer-tutorial (develop) $ ls -al .vscode/launch*.json
-rw-r--r-- 1 vscode vscode 1548 Mar  9 12:34 .vscode/launch_default.json
-rw-r--r-- 1 vscode vscode 1551 Mar  3 13:43 .vscode/launch.json
vscode ➜ /workspaces/composer-tutorial (develop) $ 
vscode ➜ /workspaces/composer-tutorial (develop) $ cat -n .vscode/launch.json 
     1  {
     2      // IntelliSense を使用して利用可能な属性を学べます。
     3      // 既存の属性の説明をホバーして表示します。
     4      // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
     5      "version": "0.2.0",
     6      "configurations": [
     7          {
     8              "name": "Listen for Xdebug",
     9              "type": "php",
    10              "request": "launch",
    11              "port": 9000
    12          },
    13          {
    14              "name": "Launch currently open script",
    15              "type": "php",
    16              "request": "launch",
    17              "program": "${file}",
    18              "cwd": "${fileDirname}",
    19              "port": 0,
    20              "runtimeArgs": [
    21                  "-dxdebug.start_with_request=yes"
    22              ],
    23              "env": {
    24                  "XDEBUG_MODE": "debug,develop",
    25                  "XDEBUG_CONFIG": "client_port=${port}"
    26              }
    27          },
    28          {
    29              "name": "Launch Built-in web server",
    30              "type": "php",
    31              "request": "launch",
    32              "runtimeArgs": [
    33                  "-dxdebug.mode=debug",
    34                  "-dxdebug.start_with_request=yes",
    35                  "-S",
    36                  "localhost:8080"
    37              ],
    38              "program": "",
    39              "cwd": "${workspaceRoot}",
    40              "port": 9000,
    41              "serverReadyAction": {
    42                  "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
    43                  "uriFormat": "http://localhost:%s",
    44                  "action": "openExternally"
    45              }
    46          }
    47      ]
    48  }vscode ➜ /workspaces/composer-tutorial (develop) $ 
vscode ➜ /workspaces/composer-tutorial (develop) $ diff .vscode/launch_default.json .vscode/launch.json 
11c11
<             "port": 9003
---
>             "port": 9000
36c36
<                 "localhost:0"
---
>                 "localhost:8080"
40c40
<             "port": 9003,
---
>             "port": 9000,
vscode ➜ /workspaces/composer-tutorial (develop) $ 
```
変更差分に関する補足：
- L11, L40: configurations[*].port
  - コンテナイメージ側の XDebug 設定に合わせることで、XDebug 設定を極力しなくて済むようにしました
  - 設定値によっては ini ファイルでしか設定・変更ができなかったりと面倒なので
- L36: configurations[2].runtimeArgs
  - PHP ビルトインサーバの Listen ポートを固定することで、デバッグする度にポートが変動しなくて済むようにしました
    - ポートが変動する度にブラウザのタブを開き直すのが面倒なので

## .vscode/settings.json ファイル

コンテナ内にインストールした VSCode Extension の設定を追加しています。
VSCode の設定はスコープがいくつか分かれていますが、本ファイルでワークスペースのスコープとして VSCode の各種設定を git 管理することで git clone 後の開発環境に反映でき、開発者ごとの開発環境の違いを最小化できます。
参考：https://code.visualstudio.com/docs/getstarted/settings

## composer.json ファイル

`composer validate` でエラーが出ないようにしました。
地味ですがこういうところはきちんとしたい。
また、Composer Script の定義を追加しています。※詳細は後述

## .gitignore ファイル

[gitignore.io](https://www.toptal.com/developers/gitignore) を利用して作成しました。
ファイル・ディレクトリ単位での調整を最小限にできて便利なので、おすすめです。

# テーマごとの解説

## XDebug の設定

コードを書くにはまずきちんとデバッグする為のノウハウを身に付けねばならぬ…
と思い、XDebug の設定を行いました。

`.vscode/launch.json` を作成し動作検証をしたところ、最初は IDE(VSCode) 側と XDebug 側でのポート指定が合っていないことに気付かず、StepDebug が動かずに苦戦しました。
IDE 側の設定は `.vscode/launch.json` に書いているのでわかりやすいですが、XDebug の設定はどこに記載されているか、どの設定が効いているのか、がわかりづらく少し苦戦しました。
今回利用した `mcr.microsoft.com/devcontainers/php` のイメージの場合は `/usr/local/etc/php/` 配下に各種設定ファイルが存在しますので参考にしてみて下さい。

また今回は `xdebug.start_with_request` の設定値はコンテナイメージのデフォルトの `yes` のままにしました。
本当は `trigger` にして環境変数：XDEBUG_TRIGGER の有無でデバッグ実行タイミングをコントロールしたかったのですが、自分が試した限りですとコンテナの環境変数などで気軽にこの設定値を変えることはできず(XDEBUG_CONFIG 変数を試したがだめだった)、php の ini ファイルで変更する必要があり、コンテナ内にファイルを混ぜ込むのが面倒だったのでやめました。

コンテナ内にファイルを混ぜ込む場合、下記いずれかのパターンで実現することとなり、手間だったので…。
- Dockerfile を作成し XDebug 設定ファイルを COPY でコンテナ内に配置
  - `.devcontainer/devcontainer.json` の書き換えが発生し面倒
- `.devcontainer/devcontainer.json` の `runArgs` の値としてバインドマウント設定を追加する
  - こちらは比較的楽だが、Windows でバインドマウント利用はオーナ・パーミッション周りが面倒なので極力使いたくない

※上記以外でよさげなやり方があれば、本記事のコメント欄で教えていただけるととても嬉しいです。

## PHP Class の自作

PHP の Class の基本を学ぶ為、`src/Hoge.php` として適当なクラスの作成を試してみました。
とりあえず下記などの機能を試してみました。
- コンストラクタ関数及び引数
- 静的メソッド
- 引数を必要とする関数
- プライベートプロパティを取得する関数

これまでいくつかのプログラミング言語を触ってきたこともあるのですが、PHP 特有と思われる下記などの記述ルールを把握するのに少し戸惑いました。
- `->` と `=>` それぞれの意味の違い
  - `->`：インスタンスのプロパティやメソッドを指定する
  - `=>`：配列のキーに対応する値を指定する
- クラス内のプロパティを定義・利用する際の "$` の要否
    - プロパティを定義する際は `$` をつける
    - プロパティを利用する際は `$this->property_name` のように `$this` にだけつける

また、クラスを利用する際のコード例として `examples/SimpleUsage.php` というファイルも作成してみました。
こういうのを置いておくと利用する側としては助かるかな、との考えで配置しています。
実用レベルのパッケージの場合は、適宜コメントで補足するとより親切かもしれません。

## Composer Script

Composer Script でできること、可能性を知る為に色々と試してみました。

### Script クラス

下記２つの静的メソッドを定義しました。
- Script::Echo()：引数を単純に出力するだけ
- Script::DumpExtras()：composer.json の extra プロパティを出力するだけ

非常にシンプルなクラスではありますが、メソッドの引数として渡される `Composer\Script\Event` クラスの仕様の理解や、Composer Script への引数の渡し方・使い方、composer.json の [extra プロパティ](https://getcomposer.org/doc/04-schema.md#extra) の使い方などを理解できました。

extra プロパティは、下記条件を満たすスクリプト全てに対してグローバル変数と同等のスコープになりそうなので、使い所が限られる印象を感じました。
- extra プロパティが定義されている composer.json 内の scripts プロパティで定義されている PHP 静的メソッド
- メソッドの引数として `Composer\Script\Event` が定義されている

他の変数の渡し方との使い分けのベストプラクティスなどがあれば、ぜひ知りたいなと感じました。

### ConsoleCommand クラス

前述のやり方で作った Composer Script の場合、引数は渡せますがコマンドラインフラグを渡して利用する際は、ロジックを自前で実装する必要があり面倒そうです。

[Composer の公式ドキュメントのとあるページ](https://getcomposer.org/doc/articles/scripts.md#writing-custom-commands) を見ると symfony/console パッケージの Command クラスを継承することで、コマンドラインフラグをかんたんに実装することができるということなので、実際に試してみました。

コマンドラインフラグを実装できると、Script の活用の幅を広げることができるので有用ですね。

ちなみに COMPOSER_ALLOW_XDEBUG 環境変数を設定しないと Composer Script の StepDebug が動作しないので注意下さい。
参考：https://getcomposer.org/doc/03-cli.md#composer-allow-xdebug

## テスト

きちんとしたプログラムを書くにあたって、テストは避けてはいけない。
ということで、PHPUnit を使ったテストコードの作成やテスト実行のノウハウなどを学びました。

実際にテストコードを書いてテストできるようになると、開発者として成長した実感が湧いて少し嬉しかったです。

試行錯誤する中で経験した学び・知見の一部を共有します。

### risky test として判定されてしまう問題

テストコードをとりあえず書いてみて、いざテストを実行したところ下記のようなメッセージが出てしまいました。
```bash
vscode ➜ /workspaces/composer-tutorial (develop) $ ./vendor/bin/phpunit 
PHPUnit 10.5.11 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.15
Configuration: /workspaces/composer-tutorial/phpunit.xml.dist

RRRRR..                                                             7 / 7 (100%)

Time: 00:00.049, Memory: 10.00 MB

There were 5 risky tests:

1) HogeTest::testStaticFunc
This test does not define a code coverage target but is expected to do so
～略～
```

どうやら、phpunit 設定にて [requireCoverageMetadata](https://docs.phpunit.de/en/11.0/configuration.html#the-requirecoveragemetadata-attribute) を `true` にしている場合、[@covers](https://docs.phpunit.de/en/11.0/annotations.html#covers) アノテーションを各テストに設定しないと risky test と判定されてしまうようでした。

`./vendor/bin/phpunit --generate-configuration` で生成された phpunit　設定ファイルを配置していたのですが、このやり方で生成した設定ファイルのデフォルト値として `requireCoverageMetadata="true"` になっていることを認識できていなかったのが、エラーの根本的な原因ですね。
設定ファイルのデフォルト値やそれぞれの意味は、やはりきちんと理解しないといけませんね…。

### phpunit.xml.dist が便利

前述のトラブルの際に、phpunit.xml と phpunit.xml.dist の違いと使い分けについて学ぶことができました。

- phpunit.xml：
  - phpunit の設定ファイル、デフォルトでは最優先で読み込まれる
  - phpunit.xml.dist ファイルも存在していたとしても無視される
- phpunit.xml.dist：
  - phpunit の設定ファイル、デフォルトでは phpunit.xml が存在しない場合に読み込まれる

参考：https://docs.phpunit.de/en/11.0/textui.html#configuration

テスト設定ファイルは開発環境に応じて自由度をもたせる為にリポジトリで管理すべきではない、
しかしテスト設定ファイルを各自で０から作ってしまうと、設定差異によってはテスト結果の再現性が異なってしまう為、できるだけ避けたい
といった場合にそれぞれ使い分けるようです。
下記が参考になります。
https://gist.github.com/Potherca/15c078ed0c0bf8e5249a166e057ad51a

# まとめ

PHP の開発環境構築と Composer Script の作成を通して、初歩的な開発体験を経験することができました。
今後は作成した PHP アプリの CI/CD を含めたデプロイ周りについても、選択肢や注意点、考慮事項などを学んでゆきたいですね。

本記事をここまでご覧いただきありがとうございました。
今後も有用な記事を発信できるようがんばります。