🎻

symfony/skeletonのcomposer.jsonを読んでみる

2023/12/08に公開

Symfony Advent Calendar 2023 の8日目の記事です!🎄✨

Twitter (X) でもちょいちょいSymfonyネタを呟いてます。よろしければ フォロー お願いします🤲

昨日は @77web さんの記事でした✨

はじめに

公式ドキュメント の手順に従ってSymfonyをインストールすると、初めから composer.json に色々な設定が書かれている状態でプロジェクトが作られますよね。

しかし、この composer.json に書かれている内容についてちゃんと把握して使っている人は意外と少ないのではないかと思います。

というわけで、今日はSymfonyをインストールしたときのデフォルトの composer.json の内容を、一つひとつ意味を確認しながら読んでみたいと思います。

本稿執筆時点で、composer create-project symfony/skeleton によって作成されるプロジェクトの composer.json は以下の内容になります。

{
    "type": "project",
    "license": "proprietary",
    "minimum-stability": "stable",
    "prefer-stable": true,
    "require": {
        "php": ">=8.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/console": "7.0.*",
        "symfony/dotenv": "7.0.*",
        "symfony/flex": "^2",
        "symfony/framework-bundle": "7.0.*",
        "symfony/runtime": "7.0.*",
        "symfony/yaml": "7.0.*"
    },
    "require-dev": {
    },
    "config": {
        "allow-plugins": {
            "php-http/discovery": true,
            "symfony/flex": true,
            "symfony/runtime": true
        },
        "sort-packages": true
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "replace": {
        "symfony/polyfill-ctype": "*",
        "symfony/polyfill-iconv": "*",
        "symfony/polyfill-php72": "*",
        "symfony/polyfill-php73": "*",
        "symfony/polyfill-php74": "*",
        "symfony/polyfill-php80": "*",
        "symfony/polyfill-php81": "*",
        "symfony/polyfill-php82": "*"
    },
    "scripts": {
        "auto-scripts": {
            "cache:clear": "symfony-cmd",
            "assets:install %PUBLIC_DIR%": "symfony-cmd"
        },
        "post-install-cmd": [
            "@auto-scripts"
        ],
        "post-update-cmd": [
            "@auto-scripts"
        ]
    },
    "conflict": {
        "symfony/symfony": "*"
    },
    "extra": {
        "symfony": {
            "allow-contrib": false,
            "require": "7.0.*"
        }
    }
}

この composer.json の内容について、上から順に見ていきましょう!

type license minimum-stability prefer-stable

type プロパティはパッケージの種類を示すためのものです。Symfonyアプリケーションは project です。

license プロパティはパッケージのライセンスを示すためのものです。クローズドなプロジェクトにおいてはライセンス名の代わりに proprietary という識別子が使用できます。Symfonyアプリケーションのデフォルトも proprietary となっています。

minimum-stability プロパティは、インストールする依存パッケージの安定性をフィルタリングするためのものです。Symfonyアプリケーションのデフォルトは stable となっており、この場合、例えば require プロパティに "some/library": "^2.0" と書かれていたら、some/library2.0 以上 3.0 未満の stable版 のみが適合します。もし minimum-stabilitydev なら、some/library2.0 以上 3.0 未満の dev版以上 がすべて適合します。(つまり、dev版やbeta版などがインストールされうる)

prefer-stable プロパティは、依存パッケージをインストールする際により安定なバージョンが選択されるようにするためのフラグです。例えば、minimum-stabilitydev で、依存パッケージの 2.0.02.1.x-dev の2つのバージョンが適合したとき、"prefer-stable": true がセットされている場合は(バージョン番号がより低くても)より安定な 2.0.0 がインストールされます。

Symfonyアプリケーションのデフォルトは "prefer-stable": true ですが、minimum-stabilitystable である限りは実質的に意味を成しません(そもそもstable版にしか適合しないので、その中で「より安定」を選択する余地がない)。開発の途中で minimum-stability をstable未満に変更したときに、"prefer-stable": true がセットされていると「必要に応じて不安定なバージョンを許容しつつ、できる限り安定なバージョンを使う」という挙動になるという効果があります。

require require-dev

require プロパティと require-dev プロパティはさすがに説明不要でしょうか。

require にはこのパッケージが本番環境の文脈で必要とする依存パッケージを、require-dev には開発中やテストの実行などにのみ必要な依存パッケージを列挙します。

composer install --no-dev でインストールすることで require-dev の依存パッケージを除いて require の依存パッケージのみをインストールすることができます。本番環境にデプロイする際には --no-dev オプションを付けるのを忘れないようにしましょう。

config.allow-plugins config.sort-packages

config.allow-plugins プロパティは、Composerの実行中にどのComposerプラグインにコードの実行を許可するかを指定するためのものです。セキュリティのためにComposer 2.2.0以降で追加された プロパティです。Symfonyアプリケーションのデフォルトでは

"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true

の3つのComposerプラグインパッケージが「許可」として指定されています。

config.sort-packages プロパティは、composer require コマンドで composer.json に依存パッケージを追記する際に、require および require-dev 配下のパッケージの並びを名前でソートするためのフラグです。Symfonyアプリケーションのデフォルトは "sort-packages": true です。

autoload.psr-4 autoload-dev.psr-4

autoload プロパティと autoload-dev プロパティは、Composerが提供するPHPオートローダーに対しオートローディングのマッピング情報を指示するためのものです。psr-4 psr-0 classmap files プロパティによる指定がサポートされており、Symfonyアプリケーションのデフォルトでは psr-4 が使用されています。

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
},

この記述により、

  • src/ ディレクトリ配下のクラスファイル群を、App\ 名前空間配下にマッピングする
  • その際、src/ ディレクトリ配下のクラスファイル群は PSR-4 に準拠して構成されていることを期待する

という指示をしていることになります。これにより、例えば src/Controller/HomeController.phpApp\Controller\HomeController クラスを定義(PSR-4準拠)しておくと、他のクラスで App\Controller\HomeControlleruse した際に自動で src/Controller/HomeController.php がロードされてくれる、という(普段意識していなくてもお馴染みになっている)挙動が実現されます。

autoload-dev は、開発中やテストの実行などにのみ必要なクラスのオートロードルールを、メインのオートロードルールとは分けて定義するためのものです。

"autoload-dev": {
    "psr-4": {
        "App\\Tests\\": "tests/"
    }
},

この記述で、tests/ ディレクトリ配下を App\Tests\ 名前空間配下にマッピングしています。

replace

replace プロパティはちょっと難解です(というか僕の理解も正しいか自信薄なので有識者からのご指摘お待ちしてます🙏)。

公式ドキュメントの説明をGoogle翻訳で日本語訳すると以下のような内容になっています。

このパッケージによって置き換えられるパッケージのマップ。これにより、パッケージをフォークし、独自のバージョン番号を持つ別の名前で公開できますが、元のパッケージが置き換えられるため、元のパッケージを必要とするパッケージは引き続きフォークで動作します。

これは、サブパッケージを含むパッケージにも役立ちます。たとえば、メインの symfony/symfony パッケージには、個別のパッケージとしても利用できるすべての Symfony コンポーネントが含まれています。メイン パッケージが必要な場合は、個別のコンポーネントのいずれかの要件が置き換えられるため、その要件が自動的に満たされます。

上記で説明したサブパッケージの目的で replace を使用する場合は注意が必要です。通常は、バージョン制約として self.version を使用して置換するだけにして、メイン パッケージがその正確なバージョンのサブパッケージのみを置換し、他のバージョンは置換しないようにする必要があります (これは正しくありません)。

正直これを読んでも「???」って感じですよね。

個人的には以下のページが理解に役立ちました。

以下に要約します。

  • replace プロパティの本来の使い方は以下のようなもの
    • 例えば original/library をフォークして自作した better/librarycomposer.json に、"replace": {"original/libary": "1.0.2"} のように書いておくと、「better/libraryoriginal/library:1.0.2 の代替である」ということをComposerに伝えることができる
    • これにより、あるパッケージXが better/libraryother/package に依存していて、other/packageoriginal/library:1.0.2 に依存している場合 に、パッケージ Xで composer install をした際、original/library:1.0.2 はインストールされず、代わりに better/library が使われる という挙動を実現できる
  • 応用的に、多数のコンポーネントから成るフレームワークの composer.json には、以下の理由から、「各コンポーネントをフレームワーク自身で replace する記述」がある場合がある
    • フレームワークとコンポーネントの両方に依存しているユーザーに対して、コンポーネントを重複してインストールさせたくない
    • なぜなら、フレームワークさえインストールされていればそこにすべてのコンポーネントが含まれるので
  • 例えば symfony/symfonycomposer.json確かにこんな感じになっている

これを理解した上で、symfony/skeletoncomposer.jsonreplace プロパティの内容を見てみましょう。

"replace": {
    "symfony/polyfill-ctype": "*",
    "symfony/polyfill-iconv": "*",
    "symfony/polyfill-php72": "*",
    "symfony/polyfill-php73": "*",
    "symfony/polyfill-php74": "*",
    "symfony/polyfill-php80": "*",
    "symfony/polyfill-php81": "*",
    "symfony/polyfill-php82": "*"
},

「自分自身が、各種polyfill系パッケージの *(任意のバージョン)の代替である」と宣言している、という意味だと分かります。

自分自身がこれらのパッケージの任意のバージョンの代替である、ということは、結果的に 間接的な依存にこれらのパッケージが含まれていても、それらは一切インストールする必要はない(だって自分自身がそれらの代替なのだから)ということをComposerに伝えていることになります。

ここで改めて require プロパティの中身を見てみましょう。

"require": {
    "php": ">=8.2",
    "ext-ctype": "*",
    "ext-iconv": "*",
    // 略
},

PHPのバージョン8.2以上、ctype拡張、iconv拡張を 物理的に要求しています。

ということは、PHPのバージョン8.2以下の機能やctype拡張やiconv拡張の機能を補完してくれるpolyfillパッケージはインストールする必要がない ですよね(だってそれらはpolyfillしてもらうまでもなく確実にミドルウェアとして存在しているはずだから)。

つまり、この replace プロパティの意図は、間接的な依存によって無駄なパッケージがインストールされないようにしているだけ ということです(だと思います)。

scripts

scripts プロパティには任意のスクリプトを定義することができます。Symfonyアプリケーションのデフォルトは以下の内容になっています。

"scripts": {
    "auto-scripts": {
        "cache:clear": "symfony-cmd",
        "assets:install %PUBLIC_DIR%": "symfony-cmd"
    },
    "post-install-cmd": [
        "@auto-scripts"
    ],
    "post-update-cmd": [
        "@auto-scripts"
    ]
},

post-install-cmdpost-update-cmd は特殊なキーで、composer install および composer update コマンドが実行された直後に Composerが発行するイベント をフックしてスクリプトが実行されることを意味しています。

上記では、post-install-cmdpost-update-cmd の両方のタイミングで @auto-scripts を実行させています。@auto-scripts は、auto-scripts という名前で定義済みのスクリプトを実行する」という意味 です。

auto-scripts の定義内容は

{
    "cache:clear": "symfony-cmd",
    "assets:install %PUBLIC_DIR%": "symfony-cmd"
}

となっていて、何やら見慣れない形をしています。

実は、auto-scripts という名前で定義されたスクリプトは、Symfony Flex によって特別に扱われます。Symfony Flexのソースコード を覗いてみると分かりますが、"{bin/consoleのサブコマンド名}": "symfony-cmd" と書くことで bin/console のサブコマンドを実行させられるということのようです。

ちなみに、(Symfony Flexのソースコード から分かるとおり、)auto-scripts に通常のシェルスクリプトを追加したい場合は、以下のように書けばOKです。

{
    "cache:clear": "symfony-cmd",
    "assets:install %PUBLIC_DIR%": "symfony-cmd",
    "echo hello": "script"
}

conflict

conflict プロパティは、このパッケージと競合する(ため、同時にインストールすることができない)パッケージを指定するためのものです。

Symfonyアプリケーションのデフォルトでは "symfony/symfony": "*" が指定されています。つまり、symfony/skeleton で作成したSymfonyアプリケーションにおいては、symfony/symfony をインストールすることはできないようになっているということです。symfony/symfony に直接依存せず、必要なSymfonyコンポーネントを個別にインストールしましょうということでしょう。

extra.symfony.allow-contrib extra.symfony.require

extra プロパティは、scripts プロパティで定義したスクリプト(や、Comoserプラグインによる処理)に使わせるための任意の追加データを記述するためのものです。

Symfonyアプリケーションのデフォルトで定義されている extra.symfony.allow-contribextra.symfony.require はいずれもSymfony Flexによって使われています。

allow-contrib は、Symfony Contrib Recipesリポジトリ のレシピの利用を有効化するかどうかのフラグです。デフォルトでは false ですが、これを true にすることでSymfony Contrib Recipesリポジトリのレシピが利用可能になります。

require は、すべての symfony/* パッケージのバージョンをロックするためのものです。今回はSymfony 7.0をインストールしたため、"require": "7.0.*" となっています。この指定の効果を理解するために、以下のような状況を想像してみましょう。

  • Symfony 7.1がリリースされている
  • require 配下に "symfony/form": "^7.0" のような指定が追加されている

この状態で composer update を実行すると、通常なら symfony/form については7.1系がインストールされるはずです。しかし、extra.symfony.require: "7.0.*" が指定されているため、実際には7.0系の最新がインストールされます。

extra.symfony.require に適合しないバージョンはSymfony Flexにより除外されるため、Composerの動作が高速になり、メモリ消費を抑えられるという効果があるそうです。

おわりに

というわけで、symfony/skeleton パッケージでSymfonyをインストールしたときのデフォルトの composer.json の内容を、一つひとつ意味を確認しながら読んでみました。

Symfony(とComposer)に対する理解が少し深まりましたね!

Symfony Advent Calendar 2023、明日は空きです🥺どなたかぜひご参加ください!

GitHubで編集を提案

Discussion