Composerのcreate-projectは何をしているのか、何が出来るのか

# 記事のゴール

[BEAR.Sundayのチュートリアル](https://bearsunday.github.io/manuals/1.0/ja/tutorial.html)でプロジェクト作成時に行う以下の操作。

```
$ composer create-project bear/skeleton MyVendor.Weekday
Installing bear/skeleton (1.7.4)
  - Installing bear/skeleton (1.7.4): Loading from cache
Created project in MyVendor.Weekday
> BEAR\Skeleton\Composer::install

What is the vendor name ?

(MyVendor):

What is the project name ?

(MyProject):Weekday
```

上記の対話で如何にして名前空間の設定やテストの作成などを行なっているのかを知る。

# create-projectの挙動を知る

> This is the equivalent of doing a git clone/svn checkout followed by a composer install of the vendors.
> \---
> git clone/svn checkoutのあとにcomposer installを実行すること同等です。

```
$ composer create-project foo/bar
```

上記のコマンドは以下の操作と同等のものであるとのこと。

```
$ git clone https://github.com/foo/bar
$ cd bar
$ composer install
```

参考：
https://getcomposer.org/doc/03-cli.md#create-project

# BEAR\Skeleton\Composer::install について知る

`bear/skeleton`インストール直後に呼ばれていた`BEAR\Skeleton\Composer::install`がどこから呼ばれていたか探したところ、composer.jsonのscriptsから以下のように呼ばれていることがわかった。

```json:composer.json
{
    ...
    "scripts" :{
        "pre-update-cmd": "BEAR\\Skeleton\\Composer::install",
```

## composer.jsonのscriptsとは何か

> A script, in Composer's terms, can either be a PHP callback (defined as a static method) or any command-line executable command. 
> \---
> Composerの用語では、スクリプトはPHPコールバック（静的メソッドとして定義）またはコマンドライン実行可能コマンドのいずれかです。

* 静的メソッドとして定義されたコールバック
* `composer xxx` のような形でコマンドラインから実行するコマンド

の2つに分類される。
今回は前者であろう。

参考：
https://getcomposer.org/doc/articles/scripts.md#scripts

## pre-update-cmdとは何か

Composerは処理の実行中にいくつかのイベントを発火させる。その中の1つ。

関係しそうなイベントをいくつか抜粋。

|イベント|タイミング|
|:--|:--|
|pre-install-cmd|composer.lockが存在する状態でinstallコマンドが実行される前|
|post-install-cmd|composer.lockが存在する状態でinstallコマンドが実行された後|
|pre-update-cmd|updateコマンドが実行される前<br>composer.lockが存在しない状態でinstallコマンドが実行される前|
|post-update-cmd|updateコマンドが実行された後<br>composer.lockが存在しない状態でinstallコマンドが実行された後|
|post-root-package-install|create-projectコマンド中にルートパッケージがインストールされた後|
|post-create-project-cmd|create-projectコマンドが実行された後|

イベントは手動で発火させることも可能。

```
$ composer run-script pre-update-cmd
```

参考：
https://getcomposer.org/doc/articles/scripts.md#running-scripts-manually

## pre-update-cmdはどのタイミングで発火するのか

`composer create-project`においてどのタイミングで発火するかテストする。

```
$ composer create-project piotzkhider/composer:dev-master
Installing piotzkhider/composer (dev-master 2d8512df0a34f4c0b9cd65b08a3feb4721c290b1)
  - Installing piotzkhider/composer (dev-master 2d8512d): Cloning 2d8512df0a from cache
Created project in /packages/composer
> Skeleton\Composer::postRootPackageInstall
postRootPackageInstall                                  <- post-root-package-install
> Skeleton\Composer::preUpdate
preUpdate                                               <- pre-update-cmd
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Generating autoload files
> Skeleton\Composer::postUpdate
postUpdate                                              <- post-update-cmd
Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? Y
> Skeleton\Composer::postCreateProject
postCreateProject                                       <- post-create-project-cmd
```

`composer.lock`が存在しないのでinstall系イベントは発火していない。

## どのように対話を行なっているのか

> When an event is fired, your PHP callback receives as first argument a Composer\EventDispatcher\Event object. This object has a getName() method that lets you retrieve the event name.
> \---
> イベントが発生すると、PHPコールバックは最初の引数としてComposer\EventDispatcher\Eventオブジェクトを受け取り ます。このオブジェクトにはgetName()、イベント名を取得できるメソッドがあります。

コールバックはスクリプトの種類に応じた`Composer\EventDispatcher\Event`のサブクラスを引数として受け取ることが出来る。
今回のイベント、`pre-update-cmd`はコマンドイベントにあたるため[Composer\Script\Event](https://getcomposer.org/apidoc/master/Composer/Script/Event.html)が渡される。

関係するいくつかのサブクラスを抜粋。他にも色々。

|イベントの種類|渡されるサブクラス|
|:--|:--|
|コマンドイベント|Composer\Script\Event|
|インストーラーイベント|Composer\Installer\InstallerEvent|
|パッケージイベント|Composer\Installer\PackageEvent|

渡されたEventのサブクラスからIOに関するクラスを呼び出し対話を行なっている。

```php:src/Install.php
    public function __invoke(Event $event) : void
    {
        $io = $event->getIO();
        $vendor = $this->ask($io, 'What is the vendor name ?', 'MyVendor');
        ...

    private function ask(IOInterface $io, string $question, string $default) : string
    {
        $ask = sprintf("\n<question>%s</question>\n\n(<comment>%s</comment>):", $question, $default);

        return $io->ask($ask, $default);
    }
```

参考：
https://getcomposer.org/doc/articles/scripts.md#event-classes
https://getcomposer.org/doc/articles/scripts.md#defining-scripts
https://getcomposer.org/apidoc/master/Composer/Script/Event.html

# 名前空間の設定やテストの作成など

素直にスタブに設定された文言を対話で得たベンダ名、パッケージ名へと置き換えていく。
不要となったInstallファイルの削除やディレクトリの権限設定、`composer.json`の内容の置換なども同様に難しい手段は取っていない。

```php:src/Install.php
    private function rename(string $vendor, string $package) : callable
    {
        $jobRename = function (\SplFileInfo $file) use ($vendor, $package) {
            if (is_dir($file) || ! is_writable($file)) {
                return;
            }
            $contents = file_get_contents($file);
            $contents = str_replace(
                ['BEAR.Skeleton', 'BEAR\Skeleton', 'bear/skeleton'],
                ["{$vendor}.{$package}", "{$vendor}\\{$package}", strtolower("{$vendor}/{$package}")],
                $contents
            );
            file_put_contents($file, $contents);
        };

        return $jobRename;
    }
```