誰かの作ったSDKをComposerでパッケージングする

Composerやクラスのオートローディングの概念がなかった時代に作られたPHPライブラリを、いい感じに管理を試みます。

この記事の前提として、PHPにおける「SDK」とは、概ね外部Webサービスをプログラミング言語から透過的に連携するためのHTTPリクエスト処理と、そのレスポンスをマッピングしたクラスから構成されるものを指すことが多い気がします。

あとこの記事は個人の見解であり、この内容は架空のものです。いいですね？


## 背景1: PHP

現代的なPHPライブラリは[Composer](https://getcomposer.org/)を前提に設計され、ひとつひとつのファイルで明示的に`include`や`require`を書いて読み込む必要も、読み込まれる必要もありません。

そのあたりの話は先月北海道で、Composerの説明と絡めて話してきました。

<a href="https://niconare.nicovideo.jp/watch/kn2785"><img width="641" alt="スクリーンショット 2017-12-02 午前0.26.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/4121/007c0db1-a94e-988d-0363-25d71585b5bb.png"></a>

今回の記事に関連のある点を要約すると、こんな感じです。

 * Composerやオートロード以前の時代は、ライブラリが提供するPHPファイルは[`include_path`](http://php.net/manual/ja/function.set-include-path.php)に依存したファイル読み込みが主流だった
 * Composerのオートロード機能を利用すれば基本的には明示的にクラスを`require`する必要も、`include_path`を設定する必要もない

## 背景2: SDK

とある事情により、ある組織の提供するSDKをプライベートなプロダクトに取り込むことになりました。

そのSDKは以下のような特徴を持ちます。

 * ファイル一式は基本的にzipで提供される
 * PHP 5.x(謎)と7系で互換性があり、エラーが発生しないように記述されてる
 * SDKの機能を呼び出す前に、インストールディレクトリを`include_path`に追加する必要がある
 * このSDKを構成する各クラスは、基本的に**全ファイルの冒頭に**`include_path`に依存した`include`文がある
 * このSDKの設定ファイルは`include_path`の**決められた位置**に配置する必要がある

各ファイル冒頭の文は以下のような感じです。

```php
require_once ('com/example/sdk/output/Foo.php');
```

この書きかたをされてしまっては、下手な手を打つとめんどくさいことになります。

## 対応策の検討

 1. `require_once`行をスクリプトで除去してクラスマップを作成する
 2. `include_path`を設定して、本来の設定意図通りに動作するようにする

今回は元の素材を活かす形で**2**の方を採用するようにしました。まあコードに手を入れてしまったらアップデートの追従がめんどくさくなりますしね。

ちなみに、こちらのソースコードは既に何年も前に`include_path`に依存した記述は一切削除済みですので、`include_path`がひとつふたつ増えようが、パフォーマンスへの影響は全然ありません。

方針は決まりました。これだけならComposerの基本機能だけで実現できます。**つまりComposerでインストール可能なパッケージにしてやれば良いことです**。

なんとなく厄介なのが設定ファイルです。これを配置するスクリプトも書きましょう。

## Gitリポジトリを用意しよう

Composerパッケージとは何でしょうか？ その解は簡単で、`composer.json`が含まれるディレクトリやアーカイブファイル、またはVCSのリポジトリです。zipやGitのみならず、[多様な形式から取得することができます](https://github.com/composer/composer/tree/master/src/Composer/Downloader)。

開発の上ではGitやMercurialなどのVSCのリポジトリにするのがお手軽です。

ライセンス上再配布できないものなので、プライベートなリポジトリで管理します。treeでディレクトリ構成を表すと、こんな感じになります。

```
├── README.md
├── bin
│   └── example-sdk-setup (← これは自前で書く)
├── composer.json
├── composer.lock
├── example-sdk
│   ├── (ここにzipの内容をぶちまける)
```

ソースコードは加工せずディレクトリに展開するだけなので、アップデートもらくちんですね。

### Composerを設定しよう

こうします。

```composer.json
{
    "name": "zonuexe/example-sdk",
    "license": "proprietary",
    "description": "Example Service PHP SDK",
    "include-path": ["example-sdk/src/"],
    "bin": ["bin/example-sdk-setup"],
    "require-dev": {
        "sstalle/php7cc": "^1.2"
    }
}
```

この設定ファイルを置いた状態で`composer install`とか`composer dump-autoload`とか実行すると、`vendor/autoload.php`などが生成されるはずです。

`bin/example-sdk-setup` は後述する設定例ファイル生成スクリプトです。実行ファイルにしておくと、Composerをインストールした側から利用しやすくなります。`"bin"`を設定しないと`vendor/bin/`ディレクトリにスクリプトが展開されないので、実行ファイルは登録しておいた方がいいです。

Composerの機能で大事なのは`include-path`です。この設定項目については、公式ドキュメント[composer.json Schema #include-path](https://getcomposer.org/doc/04-schema.md#include-path)にこんなことが書いてあります。

> **DEPRECATED**: This is only present to support legacy projects, and all new code should preferably use autoloading. As such it is a deprecated practice, but the feature itself will not likely disappear from Composer.

要は「レガシープロジェクト向けの機能だからおすすめはしないけど、機能はたぶんずっと残すよ」ってことですね。煮えきらないですが、ここは安心して使っておきます。

これほんとにPHP7と互換性があるのかな、と石橋を叩いて渡りたかったので`require-dev`に[sstalle/php7cc: PHP 7 Compatibility Checker](https://github.com/sstalle/php7cc)を持ってきました。`./vendor/bin/php7cc example-sdk` とかやるとチェックできます。

### 設定ファイル

設定ファイルは`ini`形式です。ファイルは二種類あり、「APIの接続先定義(本番環境と開発用環境)」と、「ログの出力先のファイル設定」です。

こちらでは、以下のような感じでセットアップ用スクリプトを起動すればパスの通った場所に設定ファイルを自動で配置するようにします。

```sh
# 開発時
./vendor/bin/example-sdk-setup development /path/to/log/example-sdk/error.log

# 本番時
./vendor/bin/example-sdk-setup production /path/to/log/example-sdk/error.log
```

*固定ファイルに書き出されるのは苦しいので、せめて[PSR-3](http://www.php-fig.org/psr/psr-3/)形式のロガーを登録するとか、エラーハンドリング用の関数をコールバックで設定させてくれるとか、PHPの関数からファイルリソースを直接登録させてくれるとかだと嬉しかったです。*

読者のみなさまもSDKを提供なさるときは、情報の伝達方法には心を砕いていただきたいところです。

……話がそれましたね。

### スクリプトを実装しよう

長くなったのでGistに置きました https://gist.github.com/zonuexe/9abc2ac3a1425637edb8d9bc9a69ee09

石橋を叩いて渡りたかったので、冗長なチェックが入ってます。みなさまは、[`file_get_contents()`](http://php.net/file_get_contents)の第2引数を指定したことはありますか？ 私は生まれて初めて指定しました。その意味は「ファイルを`include_path`から検索する」です。

### `.gitattributes`を設定しよう

実はSDKのリポジトリにはサンプルコードとかドキュメントも入ってたのですが、これは開発時に役に立つことはあっても本番運用時には無用の長物です。こういったものは`.gitattributes`で`export-ignore`に登録することで、`composer install --prefer-dist`したときにはファイルが展開されなくなります。


```.gitattributes
example_sdk/doc/ export-ignore
sample/ export-ignore
```

これは今回のケースに限らず知っておくとお得な知識ですが、うっかり必要なファイルが除外されないように、テストはきちんとしてくださいね。

### SDKをインストールしよう

プライベートな[Satis](https://github.com/composer/satis)とか運用してる場合は単に登録すればいけますね。

そんなことをしてない場合はどうすればいいか。前述の通り、ComposerはGitやMercurialなどのVCSをサポートするので、リポジトリをそのままインストールできます。

* [Repositories - Composer](https://getcomposer.org/doc/05-repositories.md#vcs)

公式ドキュメントのこのあたりを見て、いい感じに操作するとインストールできるかと。

### セットアップスクリプトはいつ実行するか

 * 開発者が開発環境でセットアップ時
 * CI環境でのテスト実行前のセットアップ時
 * 本番環境にデプロイ前のセットアップ時

それぞれ全体のセットアップスクリプト内で実行されます。

### SDKが更新されたらどうする

(大規模に構成が変更されない限りは)zipファイルをもらって、展開して、`git push`するだけです。それほどの手間ではない。

## まとめ

Composerが想定されてないSDKでも勝手にパッケージングしてやれば、なんかいい感じになることがわかりましたね。