178
150

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

このPHPがテンプレートエンジンのくせに慎重すぎる (前篇)

Last updated at Posted at 2020-01-28

この記事ではPackagistで公開可能な形式のPHPのライブラリ(Composerパッケージ)を公開するための道具立てを紹介します。あと、現代のPHPerはツールを組み合せてさくっと開発しているんだという自慢です。

タイトルは「この TypeScript が Hello, world! のくせに慎重すぎる」と「この勇者が俺TUEEEくせに慎重すぎる」のぱくr… パロディです。

テンプレートエンジンのくせに型安全なんてなまいきな。

この記事の読みかた

せっかくなので手を動かしながら自分でComposerパッケージを作成してみましょう。

今回の題材は「Hello worldを出力する」という革新的機能を提供する、とても画期的な実用ライブラリです。

記事名通り「慎重すぎる」ので、細かく刻んでGitで経過を保存しながら作業を進めていましょう。なんかよくわからない状態になったらgit reset --hardとかで吹っ飛ばしながらやればだいたいなんとかなります。適宜「コミットしましょう」と書いていますが、省略しているところもあるので、次の見出しに進む前にgit statusで差分があれば必ずコミットしておいてください。

Composerがおかしなことになったら、だいたいcomposer install、それでもcomposer requireが通らないことがあればrm composer.lockとかすれば、きっとどうにかなります。 (無責任)

名前を決めよう

Composerのパッケージ名はvendor/name(発行者/名前)という構成になっています。すべて小文字です。

また、PSR-4という仕様ではPHPのトップレベル名前空間にはベンダー名を付けることになっており、\Vendor\SubNamespace\をプレフィクスにする構成です。

私であればComposerのパッケージ名はtadsan/hello、PHPの名前空間\Tadsan\Hello\のような感じです。……が、私はvendor名としてしばしばBag2を使っているので、今回はBag2\Hello\でいきます。以下、記事にこの名前が出てきたら全部自分で決めた名前に置き換えて読んでください。

Composerパッケージの初期化

現代的なのPHPパッケージはComposerを基本にしています。Composerは依存するパッケージなどの依存を管理し、それらをオートロード可能な状態にできます。(オートロードとは、個別のクラスファイルなどをincluderequireで明示的に読み込まなくても使えるということです)

重要な性質として、Composerは依存パッケージを局所的にインストールします。また、パッケージをインストールする対象(私たちがこれから作る新しいライブラリ)もComposerパッケージです。「いままでComposerパッケージなんか作ったことないよ」という皆さんも、実はcomposer requireコマンドでパッケージをインストールしたり、composer.jsonを弄った時点で(一般公開していなかったとしても)既にComposerパッケージを管理したことがあるということになります。 ΩΩΩ<な、なんだってー!?

ということで、Composerパッケージを作りましょう。composergitはコマンドとしてインストール済みである前提です。

そうそう、とりあえず今回はサンプルプロジェクトなので、helloという名前で作成していきます。

$ cd ~/your/code/dir
$ mkdir hello
$ cd hello
$ git init
$ composer init
$ git add -A
$ git commit -m "Init"

~/your/code/dirはあなたのコンピュータのお好きなソースコード置き場を指定してください。(私は~/repo/php/$projectのようなディレクトリに置くのが気に入ってます)

ひとつひとつのコマンドの意味について解説することはしませんが、一連の流れを動画にしました。
(動画はmkdir, cd, git init の後から始まっています)

composer-init.gif

忘れないうちにLICENSEファイルも作っておきましょう。

動画ではApache-2.0(Apache License, Version 2)を指定しています。なぜApache LicenseなのかはApacheライセンスのソースコードをGitHubにあげるまでに書きました。

Apache Licenseを採用する場合は以下のように作業を進めてください。

$ curl https://www.apache.org/licenses/LICENSE-2.0.txt > LICENSE

または

$ php -r 'copy("https://www.apache.org/licenses/LICENSE-2.0.txt", "LICENSE");'

好きな方を実行してください。起こる結果は同じです。この結果もコミットしておきましょう。

$ git add LICENSE
$ git commit -m "Add LICENSE"

ここで実行した結果は、もうGitHubにpushしておきましょう。

ということで、このリポジトリは https://github.com/bag2php/hello に置いてあります。

composer.lockは抜く

ここまでうっかり忘れていましたが、composer.lockはコミットしたままでした。

composer.lockをGit管理に含めるかどうかは各自判断する必要があります。

  • composer.lockを含める利点
    • 固定したバージョンのパッケージを確実にインストールできる
    • composer install時に依存性の解決を省けるので高速
  • composer.lockを含める欠点
    • 依存パッケージのバージョンが固定されるので、環境によってインストールできない
    • 定期的にアップデートしないと依存バージョンがどんどん古びていく

インストールする対象のPHP環境が均一である場合はcomposer.lockを含めるのが有利です。

デプロイ対象の環境の均質度が高い業務アプリケーションやDockerなどのコンテナで動作させる前提のアプリケーションはcomposer.lockを含めておくべきでしょう。一方でライブラリは、あえてcomposer.lockを管理対象から除くことで、マイナーバージョンごとにcomposer updateする手間を省けます。

ということで、今回は幅広い環境にインストールするパッケージを作りたいのでcomposer.lockはGitの管理対象から消します。

$ git rm --cached composer.lock
$ echo "/composer.lock" >> .gitignore
$ git add -u
$ git commit -m "Remove composer.lock"

間違って--cachedオプションを付け忘れてgit rm composer.lockを実行してしまっても心配しないでください。composer installすれば戻ってきます。

テスティングフレームワークのインストール

PHPのテスティングフレームワークの定番中の定番であるPHPUnitを入れていきましょう。

$ composer require --dev phpunit/phpunit

今回もまたバージョンは指定しません。この時点でまたコミットしておきましょう。

$ git add -u
$ git commit -m "composer require --dev phpunit/phpunit"

Composer経由でクラスをロードするためにcomposer.jsonを編集して設定します。

diff --git a/composer.json b/composer.json
index 943af4f..d559d78 100644
--- a/composer.json
+++ b/composer.json
@@ -14,5 +14,15 @@
     ],
     "require-dev": {
         "phpunit/phpunit": "^8.5"
+    },
+    "autoload": {
+        "psr-4": {
+            "Bag2\\Hello\\": "src"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Bag2\\Hello\\": "tests"
+        }
     }
 }

ファイルの置き場に決まったルールはありませんが、ソースコードはsrcに、ユニットテストはtestsに置くのが定番の構成です。ほかにもlibだったり名前空間と同じ名前だったり、ルートファイルに直接置く場合もあるので、好きな位置に置いて構いません。

composer.jsonに書く時は、名前空間の\\\のように二重にすることを忘れないでください(これはJSONの制約です)。また、名前空間の最後にも\\を付けることをを忘れないでください。実装だけでなくautoload-devも漏れなく設定するのは、後述するPHPStanで静的解析するためです。

autoload-devに登録するユニットテストの名前空間を実装コードを同じにするか、Bag2\Hello\Test\のように空間を別にするかは個人の好みによります。これも決め手はありませんが、私は同じにしておいた方がuseを減らせるので好きです。逆に、全ての実装クラスを明示的にuseで列挙せざるを得ない構成にすることをメリットと捉えることもできます。

この設定の意味について詳しく知りたい人はincludeって書きたくない僕たちのためのオートローディングとComposer - Qiitaを読んでください。

せっかくなので、もうひとつ。依存関係が増える前にテキストエディタでcomposer.jsonを弄っておきましょう。

diff --git a/composer.json b/composer.json
index d559d78..56ca999 100644
--- a/composer.json
+++ b/composer.json
@@ -24,5 +24,8 @@
         "psr-4": {
             "spec\\Bag2\\Hello\\": "spec"
         }
+    },
+    "config": {
+        "sort-packages": true
     }
 }

この設定をすると、composer requireで依存関係が増えたときにパッケージ名を勝手にソートしてくれるようになります。

手作業でcomposer.jsonを編集した後は、必ずcomposer update php && composer validateで検証してください。また、オートロード関連の設定をした後はcomposer dump-autoloadを実行してください。

PHPUnitを設定する

PHPUnitの設定の初期化は3. XML 設定ファイル — PHPUnit latest Manualを読んで自分で設定しろ… ということになるのですが当然だるいので、これに関しては秘伝のphpunit.xml.distがあるので、これをそのまま使ってもいいです。

phpunit.xml.dist
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
    backupGlobals="false"
    backupStaticAttributes="false"
    bootstrap="tests/bootstrap.php"
    colors="true"
    verbose="false">
  <testsuites>
    <testsuite name="Hello">
      <directory>tests/</directory>
    </testsuite>
  </testsuites>
  <filter>
    <whitelist processUncoveredFilesFromWhitelist="true">
      <directory suffix=".php">src/</directory>
    </whitelist>
  </filter>

  <logging>
    <log type="coverage-clover" target="build/logs/clover.xml"/>
  </logging>
</phpunit>

この設定では tests/bootstrap.php というスクリプトを読み込むようにしています。

ここではComposerのオートロードスクリプトを読み込むだけにしておきます。

<?php

require_once __DIR__ . '/../vendor/autoload.php';

最初の方で「includerequireで明示的に読み込まなくても使える」と言いましたが、このファイルだけは例外です。PHPスクリプトを起動する起点となるファイルでこのファイルを読み込むだけです。何度読み込んでもエラーにはなりませんが、一度だけ読み込めば十分です。

さて、./vendor/bin/phpunitを実行してみましょう。

スクリーンショット 2020-01-29 1.52.56.png

とりあえずNo tests executed!と表示されていれば成功です

抽象テストケースを用意しておこう

はじめに、このプロジェクトでの共通のテストケースクラスを定義しておきます。

tests/TestCase.php
<?php

namespace Bag2\Hello;

abstract class TestCase extends \PHPUnit\Framework\TestCase
{
}

このクラスは現在特に何の役割も持っていません。ただ、将来的にPHPUnitに後方互換性のない変更があった際の差分を吸収する層としての役割を期待することもできます。 私は過去何度かPHPUnitの非互換変更に苦しめられてきましたが、この層の存在が薬に立っています。

さておき、外部のライブラリを使用する際はアプリケーションコードから直接利用するのではなく、委譲や継承としてラッパークラスを用意するというのは有用な戦術なので覚えておいてください。PHPUnitの制約としてテスト対象のクラスはPHPUnit\Framework\TestCaseを継承しなければいけないので、PHPUnitに対しては委譲ではなく継承を用います。

実装とテストケースを追加しよう

空のテストケースと実装を用意するので、ここに足していきましょう。

tests/HelloTest.php
<?php

namespace Bag2\Hello;

use Bag2\Hello\TestCase;

final class HelloTest extends TestCase
{
}
src/Hello.php
<?php

namespace Bag2\Hello;

final class Hello
{
}

外側のクラスだけです。これでPHPUnitを実行すると以下のようになるはずです。

スクリーンショット 2020-01-29 2.29.11.png

WARNINGですが、今はこれで問題ありません。Gitでコミットして次に進みましょう。

テストを先に書く

tests/HelloTest.phpにテストと追加してみましょう。

さて、ユニットテストとは何のために書くのでしょう。動作を保証したいという目論見があるのはもちろんそうなのですが、「Clean Code that works (動作するきれいなコード)」という言葉があります。

つまり、そのクラスを使うためのシンプルなサンプルコードであって、それが実際に動くものだというのです。

ということで、難しいことは考えずに書いてみます。

diff --git a/tests/HelloTest.php b/tests/HelloTest.php
index 873d123..3c56906 100644
--- a/tests/HelloTest.php
+++ b/tests/HelloTest.php
@@ -6,4 +6,10 @@ use Bag2\Hello\TestCase;

 final class HelloTest extends TestCase
 {
+    public function test()
+    {
+        $subject = new Hello();
+
+        $this->assertSame('Hello, World!', $subject->to('World'));
+    }
 }

Helloクラスが->to()というメソッドを持ち、それは'Hello, World!'という文字列を返すものである、という仕様は私がいま即興で考えました。どのような呼び出しをすれば使いやすい実装になるかということを想像しながら「動作するサンプルコード」として、このテストケースを書きました。->to()というメソッド名がぴんとこなければ、各自好きな名前を付けましょう。所詮は私の思い付きなので。

$this->assertSame($expected, $actual)は期待する値と実際の値が同じであることを検証するメソッドです。順番を間違えないように気をつけてください。

この状態でPHPUnitを実行してみましょう。

スクリーンショット 2020-01-29 2.43.18.png

当然ですね。まだメソッドを実装してないからです。ただ、ここでわかる事実が一つあります。new Helloには成功しているのです。つまりいままでの作業で、オートロードの設定は成功しているのです。

もしどこかでクラス名の定義や呼び出しなどが間違っていると、以下のようなエラーが出ているはずです。(この例ではHelloを間違ってHalloと書いている箇所があります)

また、ファイル名が間違っていてHelloTast.phpのようなファイル名(ファイル名の末尾がTest.phpになっていない)であれば、そもそもテストが実行できていないのです。

ここで私が言いたいのは「わざとテストを失敗させることでも得られる情報がある」ということです。失敗もしないテストというのはそもそもテストが動いていないので、私はどんな簡単なテストでもまず最初にテストを失敗させます。

スクリーンショット 2020-01-29 2.52.23.png

Error: Call to undefined methodが出ることが確認できたら、次に進みましょう。

実装→テスト→実装

src/Hello.phpにテストが通るための最低限のコードを実装してみましょう。

diff --git a/src/Hello.php b/src/Hello.php
index 74e44fd..d03dbed 100644
--- a/src/Hello.php
+++ b/src/Hello.php
@@ -4,4 +4,8 @@ namespace Bag2\Hello;

 final class Hello
 {
+    public function to($name)
+    {
+        return 'Hello, World!';
+    }
 }

引数は受け取るのですが、ここではまずreturn 'Hello, World!'と書くだけでテストは通せます。

実行してみましょう。

スクリーンショット 2020-01-29 3.01.17.png

おめでとう! コードが動きました。ここまで長かったですね。ではテストもどんどん増やしていきましょう。 (ここでまたgit commitしておく)

次にテストケースをまた増やします。

diff --git a/tests/HelloTest.php b/tests/HelloTest.php
index 3c56906..a1e489f 100644
--- a/tests/HelloTest.php
+++ b/tests/HelloTest.php
@@ -11,5 +11,6 @@ final class HelloTest extends TestCase
         $subject = new Hello();

         $this->assertSame('Hello, World!', $subject->to('World'));
+        $this->assertSame('Hello, Miku!', $subject->to('Miku'));
     }
 }

引数に"Miku"を渡すとHello, Miku!が帰ってくる、というのです。

まあまあ、とりあえずテストケースの追加だけをしてまた実行してみましょう。

スクリーンショット 2020-01-29 3.12.10.png

この結果は「Hello, Miku!が返ってくると思ったけどHello, World!だった」と読めます。

これを実装するのは簡単です。

diff --git a/src/Hello.php b/src/Hello.php
index d03dbed..4b825af 100644
--- a/src/Hello.php
+++ b/src/Hello.php
@@ -6,6 +6,6 @@ final class Hello
 {
     public function to($name)
     {
-        return 'Hello, World!';
+        return "Hello, {$name}!";
     }
 }

これで./vendor/bin/phpunitを実行するとテストが通ると思います。

スクリーンショット 2020-01-29 3.18.02.png

成功したらgit commitしたら次に進みましょう。

静的解析しよう

静的解析という言葉を聞いたことはありますでしょうか。静的ということは、「コードを動かさず(実行せず)に」ソースコードを解析するということです。静的解析の反対語は実際にプログラムを動かして検証するということで、つまりPHPUnitのようにコードを実際に動作させて検証するということです。

静的解析にはいくつものツールがあるので、これからインストールしていきましょう。この記事は慎重さを旨としているので、複数導入していきます。

ツールを導入するのにbamarni/composer-bin-plugin: No conflicts for your bin dependenciesというものを使います。これはプロジェクトのメインのcomposer.jsonとは依存関係を分けながらツールを導入するのに使われるものです。

以下のようにcomposer require --devで導入できます。composer-bin-pluginで設定したパッケージはvendor-bin/にインストールされるので、これも.gitignoreに追加していきます。

$ composer require --dev bamarni/composer-bin-plugin

composer-bin-pluginは標準設定ではvendor-binというディレクトリにインストールしてこようとします。vendorと似ているし微妙に使いにくいので、ここではtoolsというディレクトリにインストールしたいと思います。composer.jsonに設定を追加するとインストール先を変更できます。

diff --git a/composer.json b/composer.json
index 00a6f3f..eba9abe 100644
--- a/composer.json
+++ b/composer.json
@@ -28,5 +28,10 @@
     },
     "config": {
         "sort-packages": true
+    },
+    "extra": {
+        "bamarni-bin": {
+            "target-directory": "tools"
+        }
     }
 }

手作業で編集した後はcomposer update php && composer validateでエラーがないかどうかをチェックしてくださいね。

tools/.gitignorevendorcomposer.lockを追加して、Gitの管理下に含まれないようにします。composer.lockを管理対象にするかどうかは各自の判断で決めてください。私は業務ではcomposer.lockをあえて管理対象にしています。

tools/.gitignore
/*/composer.lock
/*/vendor/

PHPStanをインストールしよう

次に、静的解析ツールの一つPHPStanをインストールしておきます。

$ composer bin phpstan require phpstan/phpstan phpstan/extension-installer

これを実行すると、上記設定が済んでいればtools/phpstanにPHPStanがインストールされます。セットでphpstan/extension-installerをインストールしておくと良いでしょう。これを設定しておくとPHPStan拡張のための設定を減らすことができます。

作業が済んだら早速実行してみましょう。 ./vendor/bin/phpstan analyse srcで実行してみます。

スクリーンショット 2020-01-29 3.58.22.png

エラーがないと言われました。やりましたね。しかし、Tip of the Dayとしてまめちしきを教えてくれています。PHPStanは--level指定ができるのです。

./vendor/bin/phpstan analyse src --level=maxで再実行してみましょう。

スクリーンショット 2020-01-29 3.58.01.png

なんか怒られましたね。気がかりですが、ここは一旦無視して次に進みましょう。

Psalmをインストールしよう

PsalmはPHPStanとはまた別の静的解析ツールです。

$ composer bin psalm require vimeo/psalm

すると tools/psalm/composer.json というファイルができているはずなので、これもgit addしてコミットしておきます。

Psalmは./vendor/bin/psalmで実行できます。ですが、初回実行時は以下のような出力が出るはずです。

Could not locate a config XML file in path /Users/megurine/repo/php/hello/. Have you run 'psalm --init' ?

メッセージ通りに./vendor/bin/psalm --initを実行すると設定ファイルが生成されるので、もう一度実行するとPsalmでコードを解析できます。

スクリーンショット 2020-01-29 4.07.44.png

またもや型がないことを叱られました。ところで、PSalmの特徴はPsalterというツールが同梱されていて、安全な範囲でコードを自動修正してくれることが特徴です。

ということで、実行してみましょう。

スクリーンショット 2020-01-29 4.12.49.png

なんか src/Hello.php が変更されたっぽい出力が出てますね。

前篇のまとめ

以下のツールについて取り上げました

  • Composer
  • PHPUnit
  • composer-bin-plugin
  • PHPStan
  • Psalm

まとめ

PHPを解析したりテストしたり構成管理したりするべんりっぽいあれそれはQAツールとか呼ばれており、PHP Quality AssuranceとかEdgedesignCZ/phpqa: Analyze PHP code with one commandなんかに並んでますが、玉石混淆なので全部を検証するのは結構大変です。

いつもTwitterとかSlackで「便利だよ!」って言ってもあまり使ってるひとが居ないんじゃないかと疑心暗鬼だったので、今日は具体的な導入方法について書きました。

というか私もsasezakiさんがTwitterでぼそっとつぶやいてるのを見て知ったツールが多いのですが、そのsasezakiさんが昨年12月のPHPカンファレンス2019で発表した「このPHP QAツールがすごい!2019」がPHPの背景事情やこの記事ともオーバーラップするツールの詳細な説明などが、ことこまかに解説されています。

感想

とても眠い。

えっ、こういうツールを業務でばりばりつかいこなしてるひとたちと直接コミュニケーションがとれるPHPeKaigiってイベントが来月開催されて、まだチケットが発売中なんですか? (宣伝) → 「あなたが今年PHPeKaigi2020に参加しなければいけない理由

後篇で取り上げる予定のツール

  • Phan
  • PHP-CS-Fixer
  • Infection
  • GitHub Actions
  • slevomat/coding-standard
  • thecodingmachine/safe (これはツールじゃないけど慎重要素)
  • (あとなんか忘れてそうなので思い出したら)
178
150
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
178
150

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?