s平面の左側

左側なので安定してます(制御工学の話は出てきません)

Composer において Packagist 以外(GitHub リポジトリ、ローカルのファイル)からパッケージを取得する方法

先日、PHP カンファレンス沖縄 2021 にて「自分たちのコードを Composer パッケージに分割して開発する」というテーマで発表しました。

phpcon.okinawa.jp

プロポーザル

fortee.jp

スライド

www.slideshare.net

発表資料作成の過程で、Composer において Packagist 以外(GitHub リポジトリ、ローカルのファイル)からパッケージを取得する方法もまとめていました。 しかし発表で伝えたかった主題はそこではないため、最終版ではその説明を大幅に簡略化する判断を下したのでした。

そこで、お蔵入りとなった部分を改めてブログ記事として公開しようと思います。

Composer とは

f:id:okashoi:20210608054811p:plain

getcomposer.org

Composer とは、PHP における依存関係管理ツールのデファクトスタンダードであり、例えば Web アプリケーションフレームワークでも

  • Laravel
  • CakePHP
  • Symfony
  • CodeIgniter
  • Laminas
  • Yii

といった多くのフレームワークで使われています。

そのプロジェクトが依存しているライブラリ(パッケージ)とそのバージョンを明示し、管理のためのインターフェース(コマンド)を提供しています。

{
    "require": {
        "monolog/monolog": "2.0.*"
    }
}

普段特に意識せずに Composer を使っている場合、依存するパッケージは Packagist からインストールされます。

f:id:okashoi:20210608060814p:plain
packagist.org の Web サイトにてパッケージを探すこともできる

実はこれはデフォルトの挙動であって、設定によって Packagist 以外の場所からのインストールを指定することができます。

Composer パッケージの作り方

まず前提知識として、Composer パッケージを作る方法を説明しておきましょう。

とは言ってもその方法はとても単純です。 公式ドキュメントに

As soon as you have a composer.json in a directory, that directory is a package.

と記されている通り、composer.json ファイルが存在している時点でそのディレクトリは Composer パッケージなのです。

getcomposer.org

これは composer create-project コマンドを実行した場合でも例外ではなく、Laravel を使って開発した Web アプリケーションなどもまた Composer パッケージなのです。

とにかく「composer.json ファイルが存在していれば Composer パッケージ」という点を覚えておいてください。

リポジトリとは

続いて、Composer における「リポジトリ*1」という概念について解説します。

Composer におけるリポジトリについて、公式ドキュメントには次のように説明されています。

getcomposer.org

A repository is a package source. It's a list of packages/versions. Composer will look in all your repositories to find the packages your project requires.

composer installcomposer require といったコマンドが実行されたときに、Composer がパッケージを探したり、取得元とするのがリポジトリです。

リポジトリは複数指定することができ、パッケージを探す際は順番に探していき最初に発見したものが使われます。 Packagist はデフォルトのリポジトリとして、何も設定しなくても最後に探索されます*2

getcomposer.org

Composer uses this information to search for the right set of files in package "repositories" that you register using the repositories key, or in Packagist, the default package repository.

リポジトリを追加するにはcomposer.jsonrepositories キーに配列*3の形で記述していきます。

以下は公式ドキュメントの記述例です。 今は詳細まで読み解く必要はありません、ざっくり雰囲気だけ掴んでおいてください。

getcomposer.org

{
    "repositories": [
        {
            "type": "composer",
            "url": "http://packages.example.com"
        },
        {
            "type": "composer",
            "url": "https://packages.example.com",
            "options": {
                "ssl": {
                    "verify_peer": "true"
                }
            }
        },
        {
            "type": "vcs",
            "url": "https://github.com/Seldaek/monolog"
        },
        {
            "type": "package",
            "package": {
                "name": "smarty/smarty",
                "version": "3.1.7",
                "dist": {
                    "url": "https://www.smarty.net/files/Smarty-3.1.7.zip",
                    "type": "zip"
                },
                "source": {
                    "url": "https://smarty-php.googlecode.com/svn/",
                    "type": "svn",
                    "reference": "tags/Smarty_3_1_7/distribution/"
                }
            }
        }
    ]
}

repositories キーに設定された配列内の各要素がリポジトリです。

それぞれ type という属性を持っており、それに応じて他の属性も適宜設定されています。

GitHub リポジトリを直接 Composer パッケージとして扱う

Packagist 以外からパッケージを取得できる例として、まず、GitHub 上に Composer パッケージを用意しました。

中身はただ "Hello World." と出力する関数が定義されているだけです。

github.com

こちらのソースコードは GitHub 上に公開していますが、Packagist には登録していません。

そのため、普通に composer require を実行してインストールしようとしてもパッケージを発見できずにエラーになってしまいます。

$ composer require okashoi/hello-world:dev-main
                                                                                                                                                                              
  [InvalidArgumentException]                                                                                                                                                  
  Could not find a matching version of package okashoi/hello-world. Check the package spelling, your version constraint and that the package is available in a stability whi  
  ch matches your minimum-stability (stable).

パッケージをインストールしたいプロジェクトの composer.json に以下を追記します。

{
  ...
  "repositories": [
    {
      "type": "github",
      "url": "https://github.com/okashoi/composer-hello-world"
    }
  ]
}

この状態でcomposer require を実行すると GitHub からパッケージを取得し、インストールに成功します。

$ composer require okashoi/hello-world:dev-main
./composer.json has been updated
Running composer update okashoi/hello-world
Loading composer repositories with package information
Updating dependencies                                 
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking okashoi/hello-world (dev-main 3f65e95)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Syncing okashoi/hello-world (dev-main 3f65e95) into cache
  - Installing okashoi/hello-world (dev-main 3f65e95): Cloning 3f65e95130 from cache
Generating autoload files

この挙動を利用することで例えば、利用しているパッケージにバグを見つけた際に「本家にバグ修正の PR を出しつつ、それがマージされるまでは fork した GitHub リポジトリのパッケージを利用する」ということができます。

ローカルのディレクトリを Composer パッケージとして扱う

先ほどの例は独立した、GitHub 上に公開された Git リポジトリを Composer パッケージとして扱ったものでした。 一方で、ローカルに存在するディレクトリを Composer パッケージとして扱う方法も存在しています。 リポジトリの type 属性を "path" に設定すると、ローカルに存在するディレクトリをリポジトリとして指定できます。

getcomposer.org

例として、今度は以下のようなディレクトリ構造を考えます。

apps/my-app および packages/my-package の 2 箇所に Composer パッケージが存在しています(composer.json の存在するディレクトリが Composer パッケージ、ということを思い出してください)。

...
├── apps
│   └── my-app
│       └── composer.json
├── packages
│   └── my-package
│       └── composer.json
...

my-app の依存関係に my-package を追加する(my-app パッケージが my-package パッケージを利用する形)には、まずapps/my-app/composer.json を次のようにします。

{
    ...
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package"
        }
    ],
    ...
}

type 属性には前述のとおり "path" を設定し、url 属性には依存関係に追加したいパッケージ(ここでは my-package)のファイルパスを記述します。

この状態で次のコマンドを実行します。

composer require "my/package:*@dev"

この例では packages/my-package/composer.jsonname 属性に my/package が設定してあることを想定しています。

また、バージョン(この例では *)の後ろに指定している @dev は、 minmum-stability の設定を下回る stability のパッケージをインストールするための記述です(stability-flag)。

getcomposer.org

コマンドを実行した結果、 apps/my-app/composer.json は次のように更新され、my-app パッケージから my-package パッケージを利用できるようになります。

{
    ...
    "require": {
        ...
        "my/package": "*@dev",
        ...
    },
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package"
        }
    ],
    ...
}

apps/my-app/vendor 下には packages/my-package へのシンボリックリンクが作成されています。

その先の話

ここまでで「Packagist 以外からパッケージを取得する方法」の説明は終わりです。

発表ではこの先「何を、どういった基準でパッケージに分割していくのが良いのか」という話に発展していきます。 気になる方は資料を参照してください。

あるいはそれも、語りきれなかったというか、説明(と、サンプルコード)が不足している感が否めないので、気が向いたら補強してブログに書くかもしれません。

*1:ソースコードのバージョン管理のための「リポジトリ」とは区別が必要です。以降特別な言及なしに「リポジトリ」と言った場合は Composer におけるリポジトリを指すものとします。一方、ソースコードのバージョン管理のためのリポジトリについては、便宜上 Git を使う想定として「Git リポジトリ」とします。

*2:これを明示的に無効可することも、一応できます。

*3:スキーマ定義上は key-value の組(オブジェクト)もサポートされていますが、順番が保証されないのであえて使うメリットは無いと思います。