20日目: プラグイン

昨日は、symfony アプリケーションの国際化とローカライゼーションのしかたを学びました。再度繰り返しますが、ICU 標準とたくさんのヘルパーのおかげで、symfony ではこの作業は本当に楽です。

今日は、~プラグイン~は何ができ、プラグインに何をまとめることができるのか、何のために使うことができるのかについて説明します。

プラグイン

symfony のプラグイン

symfony プラグインはプロジェクトファイルのサブセットのパッケージを作成して配布する方法を提供します。プロジェクトのように、プラグインはクラス、ヘルパー、設定、タスク、モジュール、スキーマとアセットさえも格納できます。

~プライベートプラグイン~

プラグインの最初の使い方は複数のアプリケーションもしくは異なるプロジェクトの間でコードを楽に共有できるようにすることです。symfony アプリケーションはモデルのみを共有することを思い思い出してください。プラグインはさらに複数のアプリケーションの間でコンポーネントを共有する方法を提供します。

異なるプロジェクトもしくは同じモジュールの同じスキーマを~再利用する|コードの再利用~場合、これらをプラグインに移動させます。 プラグインは単なるディレクトリなので、SVN リポジトリを作り svn:externals を指定するもしくは1つのプロジェクトから他のプロジェクトにファイルをコピーすることでプラグインを移動させることができます。

私たちはこれらを「プライベートプラグイン」と呼んでいます。これらの用途は1人の開発者もしくは企業に限定されているからです。 これらは公に利用できません。

TIP プライベートプラグインから~パッケージ|パッケージ作成~を作ることもできます。独自の symfony プラグインチャンネルを作成し、plugin:install タスクをとおしてこれらをインストールします。

~公開プラグイン~

公開プラグインは~コミュニティ~でダウンロードとインストールができます。 このチュートリアルでは、ひとくみの公開プラグイン: sfGuardPluginsfFormExtraPlugin を使いました。 このチュートリアルでは、ひとくみの公開プラグイン: sfDoctrineGuardPluginsfFormExtraPlugin を使いました。

これらはプライベートプラグインとまったく同じです。唯一の違いは誰でもプロジェクトにインストールできることです。後で公開プラグインを公開して symfony の公式サイトでホストする方法を見ることになります。

~コードを編成する|コードの編成~異なる方法

プラグインを考え出す方法とそれらの使い方はいろいろあります。再利用と共有を忘れましょう。プラグインはあなたのコードを編成するための異なる方法として利用できます。レイヤーごとにファイルを編成する代わりに: lib/model/ ディレクトリのすべてのモデル、templates/ ディレクトリのテンプレートなど; ファイルは機能ごとにまとめられます: すべての job ファイルのセット (モデル、モジュール、テンプレート)、すべての CMS ファイルのセットなど。

プラグインのファイル構造

プラグインはファイルの性質に応じて、あらかじめ定義された~構造~にわかれるファイルをもつ単なるディレクトリ構造です。今日は、Jobeetに向けて書いてきたコードの大部分を sfJobeetPlugin に移動させます。使用する基本的なレイヤーは次のとおりです:

sfJobeetPlugin/

config/ sfJobeetPluginConfiguration.class.php // プラグインの初期化 schema.yml // データベーススキーマ routing.yml // ルーティング config/ sfJobeetPluginConfiguration.class.php // プラグインの初期化 routing.yml // ルーティング doctrine/ schema.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

それから、config/ProjectConfiguration.class.php ファイルのなかで sfJobeetPlugin を有効にします。

[php]
public function setup()
{
  $this->enablePlugins(array(
    'sfDoctrinePlugin', 
    'sfDoctrineGuardPlugin',
    'sfFormExtraPlugin',
    'sfJobeetPlugin'
  ));
}

NOTE すべてのプラグインの名前は Plugin で終わらなければなりません。義務ではありませんが、~プレフィックス~として sf をつけるのはよい習慣です。

Model

最初に、config/schema.yml ファイルを plugins/sfJobeetPlugin/config/ に移動させます: 最初に、config/doctrine/schema.yml ファイルを plugins/sfJobeetPlugin/config/ に移動させます:

$ mkdir plugins/sfJobeetPlugin/config/

$ mv config/schema.yml plugins/sfJobeetPlugin/config/schema.yml $ mkdir plugins/sfJobeetPlugin/config/doctrine $ mv config/doctrine/schema.yml plugins/sfJobeetPlugin/config/doctrine/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/

$ rm -rf plugins/sfJobeetPlugin/lib/model/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/form/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/filter/sfDoctrineGuardPlugin

$ rm -rf plugins/sfJobeetPlugin/lib/model/doctrine/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/form/doctrine/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/filter/doctrine/sfDoctrineGuardPlugin

Remove the plugins/sfJobeetPlugin/lib/form/BaseForm.class.php file.

$ rm plugins/sfJobeetPlugin/lib/form/BaseForm.class.php

モデル、フォームとフィルタを移動させた後でクラスを抽象クラスにし、リネームしてプレフィックスとして Plugin をつけなければなりません。

TIP ~プレフィックス~の Plugin をつけるのは自動生成クラスのみですべてのクラスではありません。たとえば手書きのクラスにはプレフィックスをつけないでください。プレフィックスが必要なのは自動生成されたクラスのみです。

JobeetAffiliateJobeetAffiliateTable クラスを移動させる例は次の通りです。

$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliate.class.php plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliate.class.php

コードは次のように更新されます:

[php]
abstract class PluginJobeetAffiliate extends BaseJobeetAffiliate
{
  public function save(Doctrine_Connection $conn = null)
  {
    if (!$this->getToken())
    {
      $this->setToken(sha1($object->getEmail().rand(11111, 99999)));
    }

    parent::save($conn);
  }

  // ...
}

JobeetAffiliateTable クラスを移動させましょう:

$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliateTable.class.php plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliateTable.class.php

クラスの定義は次のようになります:

[php]
abstract class PluginJobeetAffiliateTable extends Doctrine_Table
{
  // ...
}

フォームとフィルタクラスにも同じことを行います。Plugin をプレフィックスとしてつけるためにこれらをリネームします。

formfiltermodel ディレクトリに対して plugins/sfJobeetPlugin/lib/*/doctrine/base ディレクトリを必ず削除してください:

$ rm -rf plugins/sfJobeetPlugin/lib/form/doctrine/base
$ rm -rf plugins/sfJobeetPlugin/lib/filter/doctrine/base
$ rm -rf plugins/sfJobeetPlugin/lib/model/doctrine/base

フォーム、フィルタとモデルクラスを移動させ、リネームして一部を削除したのですべてのクラスをリビルドするタスクを実行します:

$ php symfony doctrine:build --all-classes

いくつかの新しく作成されたディレクトリが lib/model/doctrine/sfJobeetPlugin/ に格納されるスキーマから作成されたモデルを保有こしていることがわかります。

このディレクトリはスキーマから生成されたトップレベルのモデルと基底クラスを含みます。たとえば JobeetJob モデルは次のようなクラス構造を持ちます:

  • JobeetJob (PluginJobeetJob を継承) lib/model/doctrine/sfJobeetPlugin/JobeetJob.class.php: すべてのプロジェクトの機能が設定できるトップレベルのクラスです。ここは機能を追加したりプラグインモデルに付属する機能をオーバーライドできる場所です。

  • PluginJobeetJob (BaseJobeetJob を継承) plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetJob.class.php: このクラスはプラグイン固有の機能すべてを格納します。JobeetJob クラスを修正することでこのクラスと基底クラスをオーバーライドできます。

  • BaseJobeetJob (sfDoctrineRecord を継承) lib/model/doctrine/sfJobeetPlugin/base/BaseJobeetJob.class.php: doctrine:build --model を実行するたびにYAMLスキーマファイルから自動生成される基底クラス。

  • JobeetJobTable (PluginJobeetJobTable を継承) lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php: Doctrine_Core::getTable('JobeetJob')を呼び出す際に返される Doctrine_Table のインスタンスであることを除いて JobeetJob クラスと同じ

  • PluginJobeetJobTable (Doctrine_Table を継承) lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php: このクラスはDoctrine_Core::getTable('JobeetJob')を呼び出す際に返されるDoctrine_Tableのインスタンス用のプラグイン固有の機能をすべて含みます

この生成された構造によってトップレベルの JobeetJob クラスを編集することでプラグインのモデルをカスタマイズできます。 setTableDefinition()setUp() メソッドをオーバーライドすることでスキーマとカラムをカスタマイズし、リレーションシップを追加できます。

NOTE フォームクラスを移動させるとき、かならず configure() メソッドを setup() メソッドに変更し parent::setup() を呼び出してください。下記のコードは例です。

[php]
abstract class PluginJobeetAffiliateForm extends BaseJobeetAffiliateForm
{
  public function setup()
  {
    parent::setup();
  }

  // ...
}

propel:build --model タスクを実行する場合、symfony は lib/model/ の下でファイルを生成しますが、これは私たちが望むことではありません。Propel の出力ディレクトリは package オプションを追加することで設定できます。schema.yml を開き次のコンフィギュレーションを追加します:

[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 --and-load を実行する場合、symfony はプラグインの lib/model/ ディレクトリの下でファイルを生成します:

$ php symfony propel:build --all --and-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

すべての Doctrine フォームが基底クラスを持たないことを確認する必要があります。これらのファイルはプロジェクトに対してグローバルであり doctrine:build --formsdoctrine:build --filters で再生成されます。

プラグインからファイルを削除します:

$ rm plugins/sfJobeetPlugin/lib/form/doctrine/BaseFormDoctrine.class.php
$ rm plugins/sfJobeetPlugin/lib/filter/doctrine/BaseFormFilterDoctrine.class.php

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.phpcomponents.class.php ファイルのクラスの名前を変更することも必要です (たとえば、affiliateActions クラスは sfJobeetAffiliateActions にリネームする必要があります)。

次のテンプレートの include_partial()include_component() 呼び出しも変更しなければなりません:

  • sfJobeetAffiliate/templates/_form.php (affiliatesfJobeetAffiliate に変更する)
  • 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

searchdelete アクションを更新します:

[php]
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php
class sfJobeetJobActions extends sfActions
{
  public function executeSearch(sfWebRequest $request)
  {
    $this->forwardUnless($query = $request->getParameter('query'), 'sfJobeetJob', 'index');

$this->jobs = JobeetJobPeer::getForLuceneQuery($query); $this->jobs = Doctrine_Core::getTable('JobeetJob') ➥ ->getForLuceneQuery($query);

    if ($request->isXmlHttpRequest())
    {
      if ('*' == $query || !$this->jobs)
      {
        return $this->renderText('No results.');
      }

      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 ファイルを修正します:

[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 methodforcriteria: doSelectActive options: model: JobeetJob type: object methodforquery: retrieveActiveJob param: { module: sfJobeetJob, action: show } requirements: id: \d+ sfmethod: GET sfculture: (?: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 設定ファイルで有効にする必要があります:

[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 が初期化されるときにプラグインのコンフィギュレーションクラスがある場合、すべてのプラグインも初期化されます:

[php]
// 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 クラスを作ります:

[php]
// 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('jobhistory', array())); $ids = $user->getAttribute('jobhistory', array());

    if (!empty($ids))
    {
      return Doctrine_Core::getTable('JobeetJob')
        ->createQuery('a')
        ->whereIn('a.id', $ids)
        ->execute();
    }

    return array();

}

  static public function resetJobHistory(sfUser $user)
  {
    $user->getAttributeHolder()->remove('job_history');
  }
}

ディスパッチャーが methodNotFound() メソッドを呼び出すとき、sfEvent オブジェクトを渡します。

JobeetUser クラスにメソッドが存在する場合、このメソッドが呼び出され、その後で戻り値は通知元オブジェクトに返されます。そうではない場合、symfony は次に登録されたリスナーを試すもしくは例外を投げます。

getSubject() メソッドはイベントの通知元オブジェクトを返します。この場合現在の myUser オブジェクトです。

デフォルト構造 vs プラグインアーキテクチャ

プラグインのアーキテクチャを利用することで異なる方法でコードを編成できます:

プラグインのアーキテクチャ

プラグインを使う

新しい機能の実装を始めるとき、もしくは Web の古典的な問題を解決しようとする場合、おそらく誰かが同じ問題をすでに解決していて symfony のプラグインとして解決方法をパッケージにしています。symfony の公開プラグインを探すには、symfony 公式サイトのプラグインセクションに移動します。

プラグインはディレクトリに内蔵されるので、インストールする方法は複数あります:

  • plugin:install タスクを使う (プラグインの開発者がプラグインパッケージを作り、symfony の公式サイトにアップロードする場合のみ機能する)
  • パッケージをダウンロードして plugins/ ディレクトリの下で手動で解凍する (開発者がパッケージをアップロードすることも必要)
  • plugins/ でプラグイン用の svn:externals を作る (プラグインの作者が Suversion でプラグインをホストする場合のみ機能する)

後者の2つの方法は簡単ですが柔軟性に欠けます。最初の方法でプロジェクトの symfony のバージョンに合わせて最新バージョンをインストールして、最新の安定版に簡単にアップグレードする、そして簡単にプラグイン間の依存関係を管理できます。

プラグインを寄付する

プラグインのパッケージを作成する

プラグインパッケージを作成するには、必須のファイルをプラグインのディレクトリ構造に追加する必要があります。最初に、プラグインのルートディレクトリで README ファイルを作りプラグインのインストール方法、これが何を提供するもの、しないものを記述します。README ファイルは Markdown フォーマットでフォーマットしなければなりません。このファイルは symfony 公式サイトでドキュメントのメインピースとして使われます。symfony plugin dingus を利用して README ファイルを HTML に変換することができます。

SIDEBAR プラグイン開発のタスク

プライベートかつ/もしくは公開プラグインをよく作ることに気がついたら、sfTaskExtraPlugin のタスクのいくつかを利用することを考えてください。このプラグインは、コアチームによって維持され、プラグインのライフサイクルの効率化を手助けするたくさんのタスクを含みます:

  • generate:plugin
  • plugin:package

LICENSEファイルを作ることも必要です。 ライセンスの選択は簡単な作業ではありませんが、symfony のプラグインセクションは symfony のライセンス (MIT、BSD、LGPL と PHP) と似たライセンスでリリースされたプラグインのみを表示します。LICENSE ファイルの内容はプラグインの公開ページの license タブの下で表示されます。

最後のステップはプラグインディレクトリのルートで package.xml ファイルを作ることです。この package.xml ファイルは PEAR パッケージ構文に従います。

NOTE package.xml の構文を学ぶベストな方法は既存のプラグインで使われるファイルをコピーすることです。

package.xml ファイルはいくつかの部分で構成されます。このテンプレートの例は次のようなものです:

[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> タグはパッケージに設置する必要のあるファイルを含みます:

[xml]
<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 タスクによって使われます。

[xml]
<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.3.0</min>
      <max>1.5.0</max>
      <exclude>1.5.0</exclude>
    </package>
  </required>
</dependencies>

symfony はバージョンによって微妙に異なる API をもつので、ここで行ったように、依存関係を常に宣言すべきです。最大バージョンと最小バージョンを宣言すれば plugin:install は symfony の必須バージョンがわかります。

別のプラグインとの依存関係を宣言することも可能です:

[xml]
<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" タブとプラグインのフィードで見ることができます。

[xml]
<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 のプラグインリポジトリは便利で面白く、しかしおかしなプラグインで満たされています。

ORM