昨日は、symfonyアプリケーションの国際化とローカライゼーションのしかたを学びました。 再度繰り返しますが、ICU標準とたくさんのヘルパーのおかげで、symfonyではこの作業は本当に楽です。
今日は、プラグインは何ができ、プラグインに何をまとめることができるのか、何のために使うことができるのかについて説明します。
プラグイン
symfonyのプラグイン
symfonyプラグインはプロジェクトファイルのサブセットのパッケージを作成して配布する方法を提供します。 プロジェクトのように、プラグインはクラス、ヘルパー、設定、タスク、モジュール、スキーマとアセットさえも格納できます。
プライベートプラグイン
プラグインの最初の使い方は複数のアプリケーションもしくは異なるプロジェクトの間でコードを楽に共有できるようにすることです。 symfonyアプリケーションはモデルのみを共有することを思い思い出してください。 プラグインはさらに複数のアプリケーションの間でコンポーネントを共有する方法を提供します。
異なるプロジェクトもしくは同じモジュールの同じスキーマを再利用する場合、これらをプラグインに移動させます。
プラグインは単なるディレクトリなので、SVNリポジトリを作りsvn:externals
を指定するもしくは1つのプロジェクトから他のプロジェクトにファイルをコピーすることでプラグインを移動させることができます。
私たちはこれらを"プライベート(非公開の)プラグイン"と呼んでいます。 これらの用途は1人の開発者もしくは企業に限定されているからです。 これらは公に利用できません。
tip
プライベートプラグインからパッケージを作ることもできます。
独自のsymfonyプラグインチャンネルを作成し、plugin:install
タスクをとおしてこれらをインストールします。
公開プラグイン
公開プラグインはコミュニティでダウンロードとインストールができます。
このチュートリアルでは、ひとくみの公開プラグイン: sfGuardPlugin
とsfFormExtraPlugin
を使いました。
これらはプライベートプラグインとまったく同じです。 唯一の違いは誰でもプロジェクトにインストールできることです。 後で公開プラグインを公開してsymfonyの公式サイトでホストする方法を見ることになります。
コードを編成する異なる方法
プラグインを考え出す方法とそれらの使い方はいろいろあります。
再利用と共有を忘れましょう。プラグインはあなたのコードを編成するための異なる方法として利用できます。
レイヤーごとにファイルを編成する代わりに: lib/model/
ディレクトリのすべてのモデル、templates/
ディレクトリのテンプレートなど; ファイルは機能ごとにまとめられます: すべてのjobファイルのセット(モデル、モジュール、テンプレート)、すべてのCMSファイルのセットなど。
プラグインのファイル構造
プラグインはファイルの性質に応じて、あらかじめ定義された構造にわかれるファイルを持つ単なるディレクトリ構造です。
今日は、Jobeet用に書いてきた大部分のコードをsfJobeetPlugin
に移動させます。
使用する基本的なレイヤーは次の通りです:
sfJobeetPlugin/ config/ sfJobeetPluginConfiguration.class.php // プラグインの初期化 schema.yml // データベーススキーマ routing.yml // ルーティング lib/ Jobeet.class.php // クラス helper/ // ヘルパー filter/ // フィルタークラス form/ // フォームクラス model/ // モデルクラス task/ // タスク modules/ job/ // モジュール actions/ config/ templates/ web/ // JS、CSSと画像のようなアセット
Jobeetプラグイン
プラグインのブートストラップはシンプルでplugins/
の下で新しいディレクトリを作ります。
Jobeetに関して、sfJobeetPlugin
ディレクトリを作りましょう:
$ mkdir plugins/sfJobeetPlugin
note
すべてのプラグインの名前はPlugin
で終わらなければなりません。
義務ではありませんが、プレフィックスとしてsf
をつけるのはよい習慣です。
Model
最初に、config/schema.yml
ファイルをplugins/sfJobeetPlugin/config/
に移動させます:
$ mkdir plugins/sfJobeetPlugin/config/ $ mv config/schema.yml plugins/sfJobeetPlugin/config/schema.yml
note
すべてのコマンドはUnix系環境のものです。
Windowsを利用しているのであれば、Explorerでファイルをドラッグ&ドロップできます。
コードを管理するのにSubversion、もしくは他のツールを使う場合、
これらが提供する組み込みのツールを使います(ファイルを移動させるsvn mv
など)。
モデル、フォーム、フィルターファイルをplugins/sfJobeetPlugin/lib/
に移動させます:
$ mkdir plugins/sfJobeetPlugin/lib/ $ mv lib/model/ plugins/sfJobeetPlugin/lib/ $ mv lib/form/ plugins/sfJobeetPlugin/lib/ $ mv lib/filter/ plugins/sfJobeetPlugin/lib/
propel:build-model
タスクを実行する場合、symfonyはlib/model/
の下でファイルを生成しますが、これは私たちが望むことではありません。
Propelの出力ディレクトリはpackage
オプションを追加することで設定できます。
schema.yml
を開き次のコンフィギュレーションを追加します:
# plugins/sfJobeetPlugin/config/schema.yml propel: _attributes: { package: plugins.sfJobeetPlugin.lib.model }
これでsymfonyはplugins/sfJobeetPlugin/lib/model/
ディレクトリの下でファイルを生成します。
フォームとフィルタービルダーはファイルを生成する際にこの設定も考慮します。
propel:build-sql
タスクはテーブルを作成するSQLファイルを生成します。
ファイルはパッケージの名前からつけられるので、現在のファイルを削除します:
$ rm data/sql/lib.model.schema.sql
propel:build-all-load
を実行する場合、symfonyはプラグインのlib/model/
ディレクトリの下でファイルを生成します:
$ php symfony propel:build-all-load --no-confirmation
タスクを実行した後で、lib/model/
ディレクトリが作成されなかったことを確認します。
タスクはlib/form/
とlib/filter/
ディレクトリを作成しました。
これらは両方ともプロジェクトのPropelフォーム用の基底クラスを格納します。
これらのファイルはプロジェクトに対してグローバルなので、プラグインからこれらのファイルを削除します:
$ rm plugins/sfJobeetPlugin/lib/form/BaseFormPropel.class.php $ rm plugins/sfJobeetPlugin/lib/filter/BaseFormFilterPropel.class.php
note
symfony 1.2.0もしくは1.2.1を利用する場合、フィルターの基底クラスはplugins/sfJobeetPlugin/lib/filter/base/
ディレクトリにあります。
Jobeet.class.php
ファイルをプラグインに移動させることもできます:
$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/
ファイルを移動させたので、キャッシュをクリアします:
$ php symfony cc
tip
APCのようなPHPアクセラレーターを利用している場合はこの時点で動作がおかしくなるので、Apacheを再起動させます。
すべてのモデルファイルはプラグインに移動させたので、すべてがきちんと動作するのか確認するためにテストを実行します:
$ php symfony test:all
ControllerとView
次のロジックのステップはモジュールをプラグインに移動させることです。 モジュールの名前の衝突を避けるために、モジュールの名前にプレフィックスとしてプラグインの名前をつけるのは常によい習慣です:
$ mkdir plugins/sfJobeetPlugin/modules/ $ mv apps/frontend/modules/affiliate plugins/sfJobeetPlugin/modules/sfJobeetAffiliate $ mv apps/frontend/modules/api plugins/sfJobeetPlugin/modules/sfJobeetApi $ mv apps/frontend/modules/category plugins/sfJobeetPlugin/modules/sfJobeetCategory $ mv apps/frontend/modules/job plugins/sfJobeetPlugin/modules/sfJobeetJob $ mv apps/frontend/modules/language plugins/sfJobeetPlugin/modules/sfJobeetLanguage
それぞれのモジュールに対して、すべてのactions.class.php
とcomponents.class.php
ファイルのクラスの名前を変更することも必要です(たとえば、affiliateActions
クラスはsfJobeetAffiliateActions
にリネームする必要があります)。
次のテンプレートのinclude_partial()
とinclude_component()
呼び出しも変更しなければなりません:
sfJobeetAffiliate/templates/_form.php
(affiliate
をsfJobeetAffiliate
に変更する)sfJobeetCategory/templates/showSuccess.atom.php
sfJobeetCategory/templates/showSuccess.php
sfJobeetJob/templates/indexSuccess.atom.php
sfJobeetJob/templates/indexSuccess.php
sfJobeetJob/templates/searchSuccess.php
sfJobeetJob/templates/showSuccess.php
apps/frontend/templates/layout.php
search
とdelete
アクションを更新します:
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php class sfJobeetJobActions extends sfActions { public function executeSearch(sfWebRequest $request) { if (!$query = $request->getParameter('query')) { return $this->forward('sfJobeetJob', 'index'); } $this->jobs = JobeetJobPeer::getForLuceneQuery($query); if ($request->isXmlHttpRequest()) { if ('*' == $query || !$this->jobs) { return $this->renderText('No results.'); } else { return $this->renderPartial('sfJobeetJob/list', array('jobs' => $this->jobs)); } } } public function executeDelete(sfWebRequest $request) { $request->checkCSRFProtection(); $jobeet_job = $this->getRoute()->getObject(); $jobeet_job->delete(); $this->redirect('sfJobeetJob/index'); } // ... }
最後に、これらの修正が考慮されるようにrouting.yml
ファイルを修正します:
# apps/frontend/config/routing.yml affiliate: class: sfPropelRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: GET } prefix_path: /:sf_culture/affiliate module: sfJobeetAffiliate requirements: sf_culture: (?:fr|en) api_jobs: url: /api/:token/jobs.:sf_format class: sfPropelRoute param: { module: sfJobeetApi, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml) category: url: /:sf_culture/category/:slug.:sf_format class: sfPropelRoute param: { module: sfJobeetCategory, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom) sf_culture: (?:fr|en) job_search: url: /:sf_culture/search param: { module: sfJobeetJob, action: search } requirements: sf_culture: (?:fr|en) job: class: sfPropelRouteCollection options: model: JobeetJob column: token object_actions: { publish: PUT, extend: PUT } prefix_path: /:sf_culture/job module: sfJobeetJob requirements: token: \w+ sf_culture: (?:fr|en) job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: model: JobeetJob type: object method_for_criteria: doSelectActive param: { module: sfJobeetJob, action: show } requirements: id: \d+ sf_method: GET sf_culture: (?:fr|en) change_language: url: /change_language param: { module: sfJobeetLanguage, action: changeLanguage } localized_homepage: url: /:sf_culture/ param: { module: sfJobeetJob, action: index } requirements: sf_culture: (?:fr|en) homepage: url: / param: { module: sfJobeetJob, action: index }
JobeetのWebサイトを見ようとすると、モジュールが有効にされていないことを知らせる例外が表示されます。
プラグインはプロジェクトのすべてのアプリケーションの間で共有されるので、アプリケーションに必要なモジュールをsettings.yml
設定ファイルで有効にする必要があります:
# apps/frontend/config/settings.yml all: .settings: enabled_modules: - default - sfJobeetAffiliate - sfJobeetApi - sfJobeetCategory - sfJobeetJob - sfJobeetLanguage
マイグレーションの最後のステップはモジュールの名前に対してテストする機能テストを修正することです。
タスク
タスクはプラグインに簡単に移動させることができます:
$ mv lib/task plugins/sfJobeetPlugin/lib/
国際化ファイル
プラグインはXLIFFファイルも格納できます:
$ mv apps/frontend/i18n plugins/sfJobeetPlugin/
ルーティング
プラグインはルーティングルールも格納できます:
$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/
アセット
少し直感に反していますが、プラグインは、画像、スタイルシートとJavaScriptのようなWebアセットも格納できます。
Jobeetプラグインを配布したくないので、本当に意味がありませんが、plugins/sfJobeetPlugin/web/
ディレクトリを作ることで実現できます。
プラグインのアセットはブラウザーから閲覧可能なプロジェクトのweb/
ディレクトリからアクセスできなければなりません。
plugin:publish-assets
はUnixシステムではシンボリックリンクの作成、Windowsプラットフォームではファイルをコピーすることでこれに対処します:
$ php symfony plugin:publish-assets
ユーザー
求人履歴を扱うmyUser
クラスメソッドを移動させることは少し手間がかかります。
JobeetUser
クラスを作成しmyUser
を継承させることもできます。
しかし、とりわけいくつかのプラグインが新しいメソッドをクラスに追加したい場合、ベターな方法があります。
リスニングできるライフサイクルの間にsymfonyのコアオブジェクトはイベントを通知します。
我々の事例では、未定義のメソッドがsfUser
オブジェクトに呼び出されるときに起きるuser.method_not_found
イベントをリスニングする必要があります。
symfonyが初期化されるときにプラグインのコンフィギュレーションクラスがある場合、すべてのプラグインも初期化されます:
// plugins/sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php class sfJobeetPluginConfiguration extends sfPluginConfiguration { public function initialize() { $this->dispatcher->connect('user.method_not_found', array('JobeetUser', 'methodNotFound')); } }
イベントの通知はイベントディスパッチャーオブジェクトであるsfEventDispatcher
によって管理されます。
connect()
メソッドを呼び出せばリスナーが登録されます。
connect()
メソッドはイベントの名前をPHP callableに結びつけます。
note
PHP callableはPHP変数でcall_user_func()
関数によって使われis_callable()
関数に渡されるときにtrue
を返します。
文字列は関数を表し、配列はオブジェクトメソッドもしくはクラスメソッドを表します。
上記のコードによって、myUser
オブジェクトはメソッドを見つけられないときはJobeetUser
クラスのmethodNotFound()
スタティックメソッドを呼び出します。
見つからないメソッドを処理するもしくは処理しないのはmethodNotFound()
メソッド次第です。
myUser
クラスからすべてのメソッドを削除しJobeetUser
クラスを作成します:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { } // plugins/sfJobeetPlugin/lib/JobeetUser.class.php class JobeetUser { static public function methodNotFound(sfEvent $event) { if (method_exists('JobeetUser', $event['method'])) { $event->setReturnValue(call_user_func_array( array('JobeetUser', $event['method']), array_merge(array($event->getSubject()), $event['arguments']) )); return true; } } static public function isFirstRequest(sfUser $user, $boolean = null) { if (is_null($boolean)) { return $user->getAttribute('first_request', true); } else { $user->setAttribute('first_request', $boolean); } } static public function addJobToHistory(sfUser $user, JobeetJob $job) { $ids = $user->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $user->setAttribute('job_history', array_slice($ids, 0, 3)); } } static public function getJobHistory(sfUser $user) { return JobeetJobPeer::retrieveByPks($user->getAttribute('job_history', array())); } static public function resetJobHistory(sfUser $user) { $user->getAttributeHolder()->remove('job_history'); } }
ディスパッチャがmethodNotFound()
メソッドを呼び出すとき、sfEvent
オブジェクトを渡します。
JobeetUser
クラスにメソッドが存在する場合、このメソッドが呼び出され、その後で戻り値は通知元オブジェクトに返されます。
そうではない場合、symfonyは次に登録されたリスナーを試すもしくは例外を投げます。
getSubject()
メソッドはイベントの通知元オブジェクトを返します。
この場合現在のmyUser
オブジェクトです。
いつものように新しいクラスを作るとき、ブラウザーで見るもしくはテストを実行する前にキャッシュをクリアすることをお忘れなく:
$ php symfony cc
デフォルト構造 vs プラグインアーキテクチャ
プラグインのアーキテクチャを利用することで異なる方法でコードを編成できます:
プラグインを使う
新しい機能の実装を始めるとき、もしくはWebの古典的な問題を解決しようとする場合、おそらく誰かが同じ問題をすでに解決していてsymfonyのプラグインとして解決方法をパッケージにしています。 symfonyの公開プラグインを探すには、symfony公式サイトのプラグインセクションに移動します。
プラグインはディレクトリに内蔵されるので、インストールする方法は複数あります:
plugin:install
タスクを使う(プラグインの開発者がプラグインパッケージを作りsymfonyの公式サイトにアップロードする場合のみ機能する)- パッケージをダウンロードして
plugins/
ディレクトリの下で手動で解凍する(開発者がパッケージをアップロードすることも必要) plugins/
でプラグイン用のsvn:externals
を作る(プラグインの作者がSuversionでプラグインをホストする場合のみ機能する)
後者の2つの方法は簡単ですが柔軟性に欠けます。 最初の方法でプロジェクトのsymfonyのバージョンに合わせて最新バージョンをインストールして、 最新の安定版に簡単にアップグレードする、そして簡単にプラグイン間の依存関係を管理できます。
プラグインを寄付する
プラグインのパッケージを作成する
プラグインパッケージを作成するには、必須のファイルをプラグインのディレクトリ構造に追加する必要があります。
最初に、プラグインのルートディレクトリでREADME
ファイルを作りプラグインのインストール方法、これが何を提供するもの、しないものを記述します。
README
ファイルはMarkdownフォーマットでフォーマットしなければなりません。
このファイルはsymfony公式サイトでドキュメントのメインピースとして使われます。
symfony plugin dingusを利用してREADMEファイルをHTMLに変換することができます。
LICENSE
ファイルを作ることも必要です。
ライセンスの選択は簡単な作業ではありませんが、symfonyのプラグインセクションはsymfonyのライセンス(MIT、BSD、LGPL、とPHP)と似たライセンスでリリースされたプラグインのみを表示します。
LICENSE
ファイルの内容はプラグインの公開ページのlicenseタブの下で表示されます。
最後のステップはプラグインディレクトリのルートでpackage.xml
ファイルを作ることです。
このpackage.xml
ファイルはPEARパッケージ構文に従います。
note
package.xml
の構文を学ぶベストな方法は既存のプラグインで使われるファイルをコピーすることです。
package.xml
ファイルはいくつかの部分で構成されます。
このテンプレートの例は次のようなものです:
<!-- plugins/sfJobeetPlugin/package.xml --> <?xml version="1.0" encoding="UTF-8"?> <package packagerversion="1.4.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd" > <name>sfJobeetPlugin</name> <channel>plugins.symfony-project.org</channel> <summary>A job board plugin.</summary> <description>A job board plugin.</description> <lead> <name>Fabien POTENCIER</name> <user>fabpot</user> <email>fabien.potencier@symfony-project.com</email> <active>yes</active> </lead> <date>2008-12-20</date> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <notes /> <contents> <!-- CONTENT --> </contents> <dependencies> <!-- DEPENDENCIES --> </dependencies> <phprelease> </phprelease> <changelog> <!-- CHANGELOG --> </changelog> </package>
<contents>
タグはパッケージに設置する必要のあるファイルを含みます:
<contents> <dir name="/"> <file role="data" name="README" /> <file role="data" name="LICENSE" /> <dir name="config"> <file role="data" name="config.php" /> <file role="data" name="schema.yml" /> </dir> <!-- ... --> </dir> </contents>
<dependencies>
タグはプラグインが持つすべての依存関係の参照: PHP、symfony、と他のプラグインをつけます。
この情報はプロジェクト環境に対してベストなプラグインのバージョンをインストールし存在するのであれば必要なプラグインの依存関係をインストールするためにplugin:install
タスクによって使われます。
<dependencies> <required> <php> <min>5.0.0</min> </php> <pearinstaller> <min>1.4.1</min> </pearinstaller> <package> <name>symfony</name> <channel>pear.symfony-project.com</channel> <min>1.2.0</min> <max>1.3.0</max> <exclude>1.3.0</exclude> </package> </required> </dependencies>
symfonyはバージョンによって微妙に異なるAPIを持つので、ここで行ったように、依存関係を常に宣言すべきです。
最大バージョンと最小バージョンを宣言することでplugin:install
はsymfonyの必須バージョンがわかります。
別のプラグインとの依存関係を宣言することも可能です:
<package> <name>sfFooPlugin</name> <channel>plugins.symfony-project.org</channel> <min>1.0.0</min> <max>1.2.0</max> <exclude>1.2.0</exclude> </package>
<changelog>
タグはオプションですがリリースの間の変更に関する有益な情報が得られます。
この情報は"Changelog"タブとプラグインのフィードで見ることができます。
<changelog> <release> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <date>2008-12-20</date> <license>MIT</license> <notes> * fabien: First release of the plugin </notes> </release> </changelog>
symfony公式サイトでプラグインを公開する
便利なプラグインを開発してsymfonyのコミュニティと共有したい場合、まだなければsymfony公式サイトのアカウントを作成し、新しいプラグインを作成します。
あなたは自動的にプラグインの管理者になりインターフェイスの"admin"タブを見ることになります。 このタブにおいて、プラグインを管理してパッケージをアップロードするために必要なすべての機能が見つかります。
note
プラグインのFAQページにはプラグイン開発者のための有益な情報がたくさんあります。
また明日
プラグインの作成とコミュニティとの共有はsymfony公式サイトへのベストな貢献方法の1つです。 これはとても簡単なので、symfonyのプラグインリポジトリは便利で面白く、しかしおかしなプラグインで満たされています。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.