このエントリーは Symfony Advent Calendar 2015 24日目の記事です。 昨日は @ochi51 さんの 「CybozuHttpBundleについて」 でした。(2015/12/24 9:00 現在未投稿 :sweat_smile:

SpBowerBundleはbowerでインストールしたパッケージをSymfonyのアセットにハンドリングしてくれる便利なバンドルです。 このバンドルのキャッシュエラーで非常にハマった時の事例を紹介します。

この記事を書いた環境

  • Symfony 2.7
  • SpBowerBundle 0.11

SpBowerBundleの簡単なおさらい

インストール

SpBowerBundleのコマンドを使用するにはnpmパッケージbowerがインストールされている必要があります。

  • https://www.npmjs.com/package/bower

SpBowerBundleのインストール方法はここでは省略します。詳しくは公式ドキュメントを参照してください。

  • v0.11 Installation https://github.com/Spea/SpBowerBundle/blob/v0.11/Resources/doc/index.md

使い方

bower.json

バンドル配下にbower.jsonを作成し依存パッケージを記述します。

// [バンドル]/Resources/config/bower/bower.json
{
    "name": "acme-demo-bundle",
    "dependencies": {
        "jquery": "~1.8.2"
    }
}

依存パッケージのインストール

$ php app/console sp:bower:install

コマンドを実行すると、依存パッケージのインストール・キャッシュ作成が行われます。

/Resources
	/public
		/components
			/jquery
			/cache

twigから参照

twigで依存パッケージのファイルを読み込みます。

{% block javascripts %}
    {% javascripts '@AcmeDemoBundle/Resources/public/components/jquery/dist/jquery.js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
{% endblock %}

アセットの作成

$ php app/console assetic:dump

これで/web配下に依存パッケージのアセットが作成され、ブラウザで開くことができるようになります。

Symfonyのページにアクセスした時のキャッシュエラー

ある日いつも通りブラウザにアクセスしたところ、このようなエラーが表示されました。

Dependency cache keys not yet generated, run "app/console sp:bower:install" to initiate the cache: Cached dependencies for "/path/to/project/AcmeDemoBundle/Resources/config/bower" not found, create it with the method createDependencyMappingCache() in . (which is being imported from "/path/to/project/app/cache/dev/assetic/routing.yml").

ユニットテストの実行でも同じメッセージが表示されます。 キャッシュエラーは時々目にしていてキャッシュクリアやインストールコマンド再実行で解決していたのですが、この日ばかりはエラーが全く解決できず、非常に悩みました。

sp:bower:installコマンドの動作を追ってみる

あまりに解決できなかったのでSpBowerBundleのソースコードを追いかけて原因を探る事にしました。

1. バンドルのロード

https://github.com/Spea/SpBowerBundle/blob/v0.11/DependencyInjection/SpBowerExtension.php#L46

SpBowerBundleがロードされると、まずconfig.ymlで設定された内容が読み込まれます。

2. sp:bower:installコマンドの実行

https://github.com/Spea/SpBowerBundle/blob/v0.11/Command/InstallCommand.php#L39

config.ymlで設定されたバンドルごとに、順番にインストール処理が行われます。

作業フォルダ

インストール処理の作業フォルダとして使用されるのは/cache/[env]/sp_bower/です。 この設定はconfig.ymlで上書きする事ができます。

2-1. bower.jsonのコピー

https://github.com/Spea/SpBowerBundle/blob/v0.11/EventListener/ExecListener.php#L52

バンドルごとに作成したbower.jsonが作業フォルダにコピーされます。

2-2. .bowerrcの出力

https://github.com/Spea/SpBowerBundle/blob/v0.11/Bower/Bower.php#L189 https://github.com/Spea/SpBowerBundle/blob/v0.11/Bower/Configuration.php#L136

config.ymlの設定内容を.bowerrcとして作業フォルダに出力します。 この時、依存パッケージのインストール先が[各バンドル]/Resources/public/componentsに設定されます。

2-3. プロセス作成

https://github.com/Spea/SpBowerBundle/blob/v0.11/Bower/Bower.php#L229-L232

プロセスが実行するbowerのバイナリのパスはデフォルトでは/usr/bin/bowerですがconfig.ymlで設定している場合はそちらで上書きされます。

7. インストールの実行

https://github.com/Spea/SpBowerBundle/blob/v0.11/Bower/Bower.php#L245

いよいよプロセスが実行されます。 この時作業フォルダは以下のような状態になっています。

/cache/[env]/sp_bower
	.bowerrc
	bower.json

インストールの動作は以下のコマンドを繰り返し呼び出すのと同じです。

$ cd /path/to/project/app/cache/[env]/sp_bower
$ bower install

8. キャッシュの作成

https://github.com/Spea/SpBowerBundle/blob/v0.11/EventListener/CacheCreateListener.php#L41 https://github.com/Spea/SpBowerBundle/blob/v0.11/Bower/Bower.php#L124

インストールが終わると結果がキャッシュに保存されます。

  • 使用するオブジェクト:DoctrineCache (filesystem)
  • 保存先[バンドル]/Resources/public/components/cache
  • キー[バンドル]/Resources/config/bowerをsha1でハッシュしたもの
  • 内容bower list --jsonで取得したインストール結果

キャッシュキーはバンドル直下のクラスAcmeDemoBundle.phpReflectionClass::getFileNameした絶対パスから生成されます。

9. アセットの作成

$ php app/console assetic:dumpコマンド実行時にSpBowerBundleはキャッシュから依存パッケージを読み込もうとします。

https://github.com/Spea/SpBowerBundle/blob/v0.11/Resources/config/assetic.xml#L21-L22 https://github.com/Spea/SpBowerBundle/blob/v0.11/Assetic/BowerResource.php#L96-L98 https://github.com/Spea/SpBowerBundle/blob/v0.11/Bower/Bower.php#L148

  • config.ymlで登録されたバンドルを読み込み
  • キャッシュキーを再作成
  • キャッシュを読み込み

この時にキャッシュが存在しないと Dependency cache keys not yet generated… のエラーが発生する、という仕組みになっています。

今回発生していたエラーの解決

キャッシュキーがどのように作成されているかを調べたところで、ファイルシステムの返す絶対パスがおかしくなっている事に気づきました。ファイルシステムの不具合の原因までは調査していませんが、SpBowerBundleのエラーに関してはPC再起動で解決する事ができました。

キャッシュエラーの解決策を紹介

今回調査したSpBowerBundleの仕組みを元に有効と思われる解決策を記載します。

注意

フォルダやパッケージ削除を含みますので実行には十分ご注意ください

インストールコマンド再実行

新しくバンドルが追加された場合など、キャッシュ未作成時にエラーが出るのは正しい状態です。大抵のキャッシュエラーはインストールコマンド実行で解決します。

$ php app/console sp:bower:install

特定の環境だけで起きる場合

dev(ブラウザ)では問題なくページが開くのにtest(ユニットテスト)はキャッシュエラーが発生する、という状況もあります。そのような場合は環境を指定してインストールコマンドを実行します。

$ php app/console sp:bower:install --env=[dev/test]

bowerパッケージ全削除

ひとつ前のキャッシュがエラーを起こしている場合はキャッシュフォルダの削除が有効です。 またインストール済みパッケージが残っているとコンフリクトを起こす事もあるのでcomponents配下の全削除をおすすめします。

# 全バンドルのcomponentsを削除して再インストール
$ rm -rf $(find src -regex ".*\/Resources\/public\/components$")
$ php app/console sp:bower:install

引用元
http://qiita.com/ttskch/items/8fb6c6c3d67a958e4179

作業ディレクトリ削除

キャッシュエラーは[バンドル]/Resources/public/components/cacheに由来するものと、/app/cache/[env]/sp_bowerに由来するものと2通りがあります。

// エラーをよく見るとパスがapp/cacheになっている
Dependency cache keys not yet generated, run "app/console sp:bower:install" to initiate the cache: Cached dependencies for "/path/to/project/app/cache/dev/sp_bower" not found, create it with the method createDependencyMappingCache().

このような場合はSymfonyのキャッシュクリアが有効です。

$ php app/console cache:clear
$ php app/console sp:bower:install

bower再インストール

今回のキャッシュエラーとは関連しませんが./node_modules/bin/bowerの破損が原因でエラーになる事もありました。そのような場合はnpmのキャッシュクリアと再インストールを試します。

$ rm -rf ~/.npm
$ rm -rf ./node_modules
$ npm install
$ php app/console sp:bower:install

フォルダリネーム

何らかの原因でキャッシュキーの生成に原因がある可能性も考えられます。私が今回遭遇したトラブルの原因はプロジェクトフォルダの大文字・小文字の違いによるものでした。実際のフォルダ名ProjectとPHPのReflectionClassが返すフォルダ名projectの先頭1文字の違いにより、ハッシュ化したキー名が異なっていました。このような状況ではフォルダのリネームやPC再起動も有効な解決策となるかもしれません。

まとめ

以前からたびたびこのバンドルのキャッシュエラーに遭遇して悩まされてきました。今回はじっくり原因を調査した事で「今まで試したどの対応が有効だったのか」を理解する事ができ、またコードリーディングのいい機会となりました。同じトラブルに遭って悩んでいる方の参考になればと思います。

明日の担当は @iteman さんです!