Composer でパッケージの削除は composer remove が良いと思ったらそうでも無かった

Composer で複数のパッケージをインストールしていて、開発を進めるうちにそれらの一部が不要になること、あると思います。そんなとき、どうやって不要になったパッケージを削除しているでしょうか？

幾つか方法はあると思うので、それらの動作を確認してみます。

まず、実験のために `composer.json` を a b c d の 4 つのディレクトリに作成します。それぞれ下記の内容です。

```json:a/composer.json
{
    "name": "oreore/a",
    "require": {
        "oreore/c": "~1.0"
    }
}
```

```json:b/composer.json
{
    "name": "oreore/b",
    "require": {
        "oreore/c": "~1.0",
        "oreore/d": "~1.0"
    }
}
```

```json:c/composer.json
{
    "name": "oreore/c"
}
```

```json:d/composer.json
{
    "name": "oreore/d"
}
```

それぞれのディレクトリを git リポジトリにして v1.0.0 タグを作成します。

```bash
for x in a b c d; do (
  cd $x
  git init
  git add .
  git commit -m first
  git tag v1.0.0
); done
```

1つ上のディレクトリにも `composer.json` を作成します。`repositories` で a b c d の .git ディレクトリを指定します。

```json:composer.json
{
    "repositories": [
        { "packagist": false },
        { "type": "vcs", "url": "file://./a/.git" },
        { "type": "vcs", "url": "file://./b/.git" },
        { "type": "vcs", "url": "file://./c/.git" },
        { "type": "vcs", "url": "file://./d/.git" }
    ],
    "require": {
        "oreore/a": "~1.0",
        "oreore/b": "~1.0"
    }
}
```

`require` には a と b だけ記述していますが、a は c に依存しており、b は c と d に依存しているため、a b c d のすべてがインストールされます。

```console
$ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev)       
  - Installing oreore/c (v1.0.0)
    Cloning 2d9272d949393688db7dc9438ed5ef5526d6378c

  - Installing oreore/a (v1.0.0)
    Cloning f48fd8b11e1fb71fb6638d1c765ab556da16e136

  - Installing oreore/d (v1.0.0)
    Cloning 2af7c6202d11209cd60e28e97b83d1686e1fd945

  - Installing oreore/b (v1.0.0)
    Cloning 0dc81980cf7e78c0541b5ed6df7cbd40b34efbe3

Writing lock file
Generating autoload files
```

依存関係のグラフは次のようになっています。

![graph.png](https://qiita-image-store.s3.amazonaws.com/0/8734/36c80414-ffca-e2a8-142d-a64020404bcb.png)

この状態から b が不要になったので b を削除します。

## composer update

一番ありがちなのが `composer.json` から不要になったパッケージを手で削除して `composer update` だと思います。

```json:composer.json
{
    "repositories": [
        { "packagist": false },
        { "type": "vcs", "url": "file://./a/.git" },
        { "type": "vcs", "url": "file://./b/.git" },
        { "type": "vcs", "url": "file://./c/.git" },
        { "type": "vcs", "url": "file://./d/.git" }
    ],
    "require": {
        "oreore/a": "~1.0"
    }
}
```

```console
$ composer update
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
Writing lock file
Generating autoload files
```

しかしこれはあまり良くありません、例えば次のようにインストールしていたパッケージの次のバージョン v1.0.1 がリリースされていた場合、

```bash
for x in a b c d; do (
  cd $x
  touch README.md
  git add .
  git commit -m v1.0.1
  git tag v1.0.1
); done
```

`composer update` だと `composer.lock` で固定されていたパッケージのバージョンが更新されてしまいます。

```console
$ composer update
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
  - Updating oreore/c (v1.0.0 => v1.0.1)
    Checking out afe5adc298e999dd4241306cf39f22a4a8bb7001

  - Updating oreore/a (v1.0.0 => v1.0.1)
    Checking out 3c1000a38b7e806f5a022ded8f8811c7b1e4d426

Writing lock file
Generating autoload files
```

## composer update [package]

`composer update` は引数でパッケージ名を指定できます。なので、`composer.json` から不要になったパッケージを手で削除して、

```json:composer.json
{
    :
    "require": {
        "oreore/a": "~1.0"
    }
}
```

`composer update` で不要になったパッケージ名を指定すると、そのパッケージの削除だけが行われます。

```console
$ composer update oreore/b
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
Writing lock file
Generating autoload files
```

がしかし、これだと b から依存していた d が削除されていません。b が不要なら d も不要なので一緒に削除したいです。

## composer update --with-dependencies [package]

`composer update` に `--with-dependencies` オプションを追加すると、指定したパッケージから依存しているパッケージも一緒に更新されます。

まずは `composer.json` を手で修正して、


```json:composer.json
{
    :
    "require": {
        "oreore/a": "~1.0"
    }
}
```

`composer update --with-dependencies` で不要になったパッケージを指定します。

```console
$ composer update --with-dependencies oreore/b
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
  - Updating oreore/c (v1.0.0 => v1.0.1)
    Checking out afe5adc298e999dd4241306cf39f22a4a8bb7001

Writing lock file
Generating autoload files
```

b と d を両方とも削除することができました。がしかし、a と b の両方から依存している c が更新されてしまいました。

# composer remove [package]

最近になって Composer に `remove` というコマンドが追加されていることに気づきました。このコマンドを使うと `composer.json` の修正も行ってくれるので `composer.json` を手で修正する必要がありません。

```console
$ composer remove oreore/b
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
Writing lock file
Generating autoload files
```

がしかし、`composer update` でパッケージ名を指定したときと同じように、b からしか依存されていない d が削除されませんでした。

# composer remove --update-with-dependencies [package]

`composer remove` には `--update-with-dependencies` というオプションを付けることができます。これなら d も一緒に削除されそうです。

```console
$ composer remove --update-with-dependencies oreore/b
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
  - Updating oreore/c (v1.0.0 => v1.0.1)
    Checking out afe5adc298e999dd4241306cf39f22a4a8bb7001

Writing lock file
Generating autoload files
```

b と d の両方が削除されました。がしかし、`composer update --with-dependencies` と同じように a と b の両方から依存されている c が更新されてしまいました。

# composer remove ---no-update [package]

`composer remove` には `--no-update` というオプションを付けることができます。これなら更新は行われなさそうです。

```console
$ composer remove --no-update oreore/b
```

ん？ なにも起こらない？

```console
$ cat composer.json
{
    "repositories": [
        { "packagist": false },
        { "type": "vcs", "url": "file://./a/.git" },
        { "type": "vcs", "url": "file://./b/.git" },
        { "type": "vcs", "url": "file://./c/.git" },
        { "type": "vcs", "url": "file://./d/.git" }
    ],
    "require": {
        "oreore/a": "~1.0"
    }
}
```

どうやら `composer.json` を修正するだけになるようです。

# まとめ

結局、どうやっても b と b からだけ依存されているパッケージをまとめて **削除だけ** を行うことはできませんでした。

どしても b と d の削除だけを行いたければ、次のようにすると良いでしょう。

まずは `composer remove --no-update` で `composer.json` を修正します。

```console
$ composer remove --no-update oreore/b
```

次に `composer update --dry-run` を実行します。

```console
$ composer update --dry-run
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Uninstalling oreore/b (v1.0.0)

  - Uninstalling oreore/d (v1.0.0)

  - Updating oreore/c (v1.0.0) to oreore/c (v1.0.1)

  - Updating oreore/a (v1.0.0) to oreore/a (v1.0.1)
```

すると、実際に `composer update` を実行すると何が起こるかが表示されます。

`Uninstalling` と表示されたパッケージを `composer update` に羅列します。

```console
$ composer update oreore/b oreore/d
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
Writing lock file
Generating autoload files
```

不要になったパッケージの削除だけが行われます。

---

シェル関数にしてみました。

```bash
composer-uninstall () {
  composer remove --no-update "$@"
  composer update --dry-run |
  grep -Eo -e '- Uninstalling\s+\S+' |
  cut -d' ' -f3 |
  xargs composer update
}
```

次のように実行します。

```console
$ composer-uninstall oreore/b
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
Writing lock file
Generating autoload files
```

.

.

.

どうせなら **Bash の超ステキ機能** も使ってみましょうか。

```bash
export composer_uninstall='() {
  composer remove --no-update "$@"
  composer update --dry-run |
  grep -Eo -e "- Uninstalling\s+\S+" |
  cut -d" " -f3 |
  xargs composer update
}'

exec $SHELL -l
```

次のように実行します。

```console
$ composer_uninstall oreore/b
Loading composer repositories with package information
Updating dependencies (including require-dev)         
  - Removing oreore/b (v1.0.0)
  - Removing oreore/d (v1.0.0)
Writing lock file
Generating autoload files
```

.

.

.

さいごのはネタです。