symfony1.2でDoctrine1.1使ってJobeetチュートリアル20日目

ついに20台まで来たよ。
pluginの説明とjobeetをplugin化しようの回。
subversionでバージョン管理している場合は、s/mv/svn mv/ で移動を明示的にして行方不明になるのを防ぎませう。
行方不明にしちゃった場合は、行方不明になったファイルをまとめて消すを使うと削除が楽です。
移動・削除の嵐で混乱してきたので、かみ砕いて訳してみます。個人的なメモは、オレンジで書いときます。

Previously on Jobeet

昨日は、symfonyのアプリを国際化・地域言語化する方法を学びましたね。ヘルパーとか使ったから簡単だったね。
今日は、pluginについて説明するよ。pluginとは何か?どうやってplugin化するか?どんな利用が可能か?

Plugins

A symfony Plugin

symfonyのpluginは、プロジェクトのファイル群の一部をパッケージ化して分配する方法を提供します。プロジェクト同様pluginは、classes, helpers, configuration, tasks, modules, schemas, そしてwebファイルを有しています。

Private Plugins

pluginの利用形態としては、あなたのアプリ間、もしくは、異なるプロジェクト間でのコード共有です。symfonyのアプリが共有しているのはmodelだけですか?pluginはより多くの部品をアプリ間で共有する方法を提供します。
もし、異なるプロジェクト間でschemaもしくはmoduleを使い回す必要があるとき、それらをpluginに移動させましょう。pluginといっても、ただのディレクトリです。svnリポジトリを作りsvn:externals(外部定義)を編集、もしくは単純にコピーすることで、使い回すことができます。
我々は、これらの利用形態を"Private Plugins"と呼びます。なぜなら、このような利用方法は、単一の開発者や会社に限られるからです。オープンじゃないよ。

Private Pluginsでも、plugin channelに登録すれば、plugin:installタスクが利用できるよ。

Public Plugins

"Public Plugins"は誰もがdownloadしてinstall可能です。今回のチュートリアルでは、sfDoctrineGuardPlugin と sfFormExtraPluginの二つのPublic Pluginsを使います。
後ほど、symfonyのウェブサイトに自分のpluginを掲載する方法を学びます。

A Different Way to Organize Code

pluginの利用形態として、もう一つ考えられます。再利用性、共有について考えないのであれば、pluginは、コード整理として使えます。ファイルをレイヤーでまとめる代わりに、機能でまとめるのです。

Plugin File Structure

pluginは、まとめられたファイルによって構成された、ただのディレクトリで、その配置は自然なものとなっている。今日は、Jobeetとして書いてきた殆どのコードをsfJobeetPluginへと移動させます。ベースのレイアウトはこんな感じです。

sfJobeetPlugin/
  config/
    sfJobeetPluginConfiguration.class.php // Plugin initialization
    schema.yml                            // Database schema
    routing.yml                           // Routing
  lib/
    Jobeet.class.php                      // Classes
    helper/                               // Helpers
    filter/                               // Filter classes
    form/                                 // Form classes
    model/                                // Model classes
    task/                                 // Tasks
  modules/
    job/                                  // Modules
      actions/
      config/
      templates/
  web/                                    // Assets like JS, CSS, and images

The Jobeet Plugin

pluginsディレクトの下にsfJobeetPluginディレクトリを作成しましょう。

$ mkdir plugins/sfJobeetPlugin

plugin名は、pluginで終わるようにしましょう。また、sfで始まる習慣がありますが強制ではありません。
要は、sfHogePluginにしろと

The Model

まず、config/doctrine/schema.yml を plugins/sfJobeetPlugin/config/ に移動させます。

$ mkdir plugins/sfJobeetPlugin/config/
$ mv config/doctrine/schema.yml plugins/sfJobeetPlugin/config/doctrine/schema.yml

全てのコマンドは、Unixライクな環境のものです。Windows使っている場合は、ドラッグ&ドロップで移動させてください。
そして、Subversionやその他のコード管理ツールを使用している場合は、組み込みコマンドを利用して移動させてください。
Subversionなら、svn mvコマンドね

doctrineフォルダ作り忘れてるので、mvで怒られます。1つ1つちまちま作るのもめんどうなので、

$ mkdir -p plugins/sfJobeetPlugin/config/doctrine

pオプションつけて、一気に作ると楽です。

$ svn mkdir

コマンドもあるのですが、pオプションが無く、後でsvn addすればいいのでmkdirで問題ないです。


model, form, and filterのファイルを 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/

移動し終えたら、クラス名の頭にPluginを付けましょう。
例は、JobeetAffiliateクラスをリネームしています。

$ 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 preValidate($event)
  {
    $object = $event->getInvoker();
 
    if (!$object->getToken())
    {
      $object->setToken(sha1($object->getEmail().rand(11111, 99999)));
    }
  }
 
  // ...
}

JobeetAffiliateTableクラスも同様に、処理していきます。

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

クラスの宣言は、この様に

<?php

class PluginJobeetAffiliateTable extends Doctrine_Table
{
  // ...
}

formsとfilterのクラスにも同様の処理をします。クラス名の頭にPluginを付けます。
BaseFormDoctrine.class.php, BaseFormFilterDoctrine.class.phpは、後で削除するのでリネームの必要無いよ。


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

Subversion使っている場合は、svn delを使います。

$ svn del plugins/sfJobeetPlugin/lib/form/doctrine/base
$ svn del plugins/sfJobeetPlugin/lib/filter/doctrine/base
$ svn del plugins/sfJobeetPlugin/lib/model/doctrine/base

mv時に付いて来ちゃったsfJobeetPlugin内のsfDoctrineGuardPluginを削除します。

$ svn del plugins/sfJobeetPlugin/lib/form/doctrine/sfDoctrineGuardPlugin
$ svn del plugins/sfJobeetPlugin/lib/filter/doctrine/sfDoctrineGuardPlugin
$ svn del plugins/sfJobeetPlugin/lib/model/doctrine/sfDoctrineGuardPlugin


移動、リネーム、削除が完了したら、クラスをビルドし直します。

$ php symfony doctrine:build-forms
$ php symfony doctrine:build-filters
$ php symfony doctrine:build-models

最後のは、タイポ

$ php symfony doctrine:build-model

build-modelはsいりません。
modelの中身は、pluginのみになります。

lib/model
`-- doctrine
    |-- sfDoctrineGuardPlugin
    |   |-- base
    |   |   |-- BasesfGuardGroup.class.php
    |   |   |-- BasesfGuardGroupPermission.class.php
    |   |   |-- BasesfGuardPermission.class.php
    |   |   |-- BasesfGuardRememberKey.class.php
    |   |   |-- BasesfGuardUser.class.php
    |   |   |-- BasesfGuardUserGroup.class.php
    |   |   `-- BasesfGuardUserPermission.class.php
    |   |-- sfGuardGroup.class.php
    |   |-- sfGuardGroupPermission.class.php
    |   |-- sfGuardGroupPermissionTable.class.php
    |   |-- sfGuardGroupTable.class.php
    |   |-- sfGuardPermission.class.php
    |   |-- sfGuardPermissionTable.class.php
    |   |-- sfGuardRememberKey.class.php
    |   |-- sfGuardRememberKeyTable.class.php
    |   |-- sfGuardUser.class.php
    |   |-- sfGuardUserGroup.class.php
    |   |-- sfGuardUserGroupTable.class.php
    |   |-- sfGuardUserPermission.class.php
    |   |-- sfGuardUserPermissionTable.class.php
    |   `-- sfGuardUserTable.class.php
    `-- sfJobeetPlugin
        |-- JobeetAffiliate.class.php
        |-- JobeetAffiliateTable.class.php
        |-- JobeetCategory.class.php
        |-- JobeetCategoryAffiliate.class.php
        |-- JobeetCategoryAffiliateTable.class.php
        |-- JobeetCategoryTable.class.php
        |-- JobeetJob.class.php
        |-- JobeetJobTable.class.php
        `-- base
            |-- BaseJobeetAffiliate.class.php
            |-- BaseJobeetCategory.class.php
            |-- BaseJobeetCategoryAffiliate.class.php
            `-- BaseJobeetJob.class.php


いくつかの新しいディレクトりが、sfJobeetPluginからインクルードされたスキーマによって生成されたことに気がつくでしょう
このディレクトりは、schemaにより生成されたトップレベルのmoduleや基底クラスを含んでいます。
例えば、JobeetJobクラスはこのようなクラス構造を持っています。

Class Name Extends Path Description
JobeetJob PluginJobeetJob lib/model/doctrine/sfJobeetPlugin/JobeetJob.class.php PluginJobeetJobクラスを継承しています。現在のプロジェクト用にカスタマイズする場合はこのクラスを変更します。
PluginJobeetJob BaseJobeetJob plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetJob.class.php このクラスに変更を加えると、継承先全てのクラスに影響を及ぼします。バグフィックスとかは、こっちに
BaseJobeetJob sfDoctrineRecord lib/model/doctrine/sfJobeetPlugin/base/BaseJobeetJob.class.php doctrine:build-modelsを走らせる度に、schema.ymlを元に自動生成されます。
JobeetJobTable PluginJobeetJobTable lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php JobeetJobクラスと同様に、Doctrine_Tableのインスタンスであることが期待されます。Doctrine::getTable('JobeetJob')で返されるのがこのクラスです。
PluginJobeetJobTable Doctrine_Table lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php Doctrine_Tableクラスの全ての機能を有しています。

この生成された構造により、JobeetJobクラスを編集すればsfJobeetPluginのモデルを変更することができます。スキーマの編集、カラムの追加、リレーションの追加は、setTableDefinition()とsetUp()関数を上書きすることで可能です。


pluginフォルダ内に、formのbaseクラスが無いことを確認しましょう。これらのファイルは、プロジェクト全体で利用され、doctrine:build-formsと doctrine:build-filtersタスクにより自動生成されるものです。


pluginから、ファイルを削除します。

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

symfony 1.2.0 もしくは 1.2.1を利用している人は、plugins/sfJobeetPlugin/lib/filter/base/ディレクトリにあります。

subversion利用している人は、svn delで削除して下さい。


Jobeet.class.phpも移動させましょう。

$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/

移動させたら、キャッシュをクリアします。

$ php symfony cc

APCの様な、アクセラレーターを利用している方は、Apacheを再起動させてください。

全てのmodelファイルをpluginへ移動させたら、移動後も問題なく動くことを確認するためにテストをしましょう。

$ php symfony test:all

The Controllers and the Views

続いて、moduleもpluginへ移動させます。

$ mv apps/frontend/modules plugins/sfJobeetPlugin/

module名の衝突回避のために、module名の頭にplugin名を付ける習慣があります。

$ 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

mvしたから、frontend下にはmodules無いのでは?
以下、svn利用者向け。パスが長いので、cdします。

$ cd plugins/sfJobeetPlugin/modules
$ svn mv affiliate sfJobeetAffiliate
$ svn mv api sfJobeetApi
$ svn mv category sfJobeetCategory
$ svn mv job sfJobeetJob
$ svn mv language sfJobeetLanguage
$ cd -


module内のactions.class.phpとcomponents.class.phpに記載されているクラス名もリネームします。
include_partial()とinclude_component()のパスも変更します。

  • sfJobeetAffiliate/templates/newSuccess.php
  • sfJobeetAffiliate/templates/_form.php (change affiliate to sfJobeetAffiliate)
  • sfJobeetCategory/templates/showSuccess.atom.php
  • sfJobeetCategory/templates/showSuccess.php
  • sfJobeetJob/templates/editSuccess.php
  • sfJobeetJob/templates/indexSuccess.atom.php
  • sfJobeetJob/templates/indexSuccess.php
  • sfJobeetJob/templates/newSuccess.php
  • sfJobeetJob/templates/searchSuccess.php
  • sfJobeetJob/templates/showSuccess.php
  • apps/frontend/templates/layout.php


(Job moduleの)searchとdeleteアクション中のjobをsfJobeetJobにリネームしましょう。

<?php
// 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 = Doctrine::getTable('JobeetJob')->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:   sfDoctrineRouteCollection
  options:
    model:          JobeetAffiliate
    actions:        [new, create]
    object_actions: { wait: GET }
    prefix_path:    /:sf_culture/affiliate
    module:         sfJobeetAffiliate
 
api_jobs:
  url:     /api/:token/jobs.:sf_format
  class:   sfDoctrineRoute
  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:   sfDoctrineRoute
  param:   { module: sfJobeetCategory, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object, method: doSelectForSlug }
  requirements:
    sf_format: (?:html|atom)
 
job_search:
  url:   /:sf_culture/search.:sf_format
  param: { module: sfJobeetJob, action: search, sf_format: html }
  requirements:
    sf_format: (?:html|js)
 
job:
  class:   sfDoctrineRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: PUT, extend: PUT }
    prefix_path:    /:sf_culture/job
    module:         sfJobeetJob
  requirements:
    token: \w+
 
job_show_user:
  url:     /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object, method_for_query: retrieveActiveJob }
  param:   { module: sfJobeetJob, action: show }
  requirements:
    id:        \d+
    sf_method: GET
 
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のウェブサイトをブラウザで閲覧しようとした場合、moduleが利用できませんという例外が返ってくるでしょう。プロジェクト内の全てのアプリ間で共有されるので、settings.ymlで明示的に利用を宣言しなければなりません。

// apps/frontend/config/settings.yml
all:
  .settings:
    enabled_modules:
      - default
      - sfJobeetAffiliate
      - sfJobeetApi
      - sfJobeetCategory
      - sfJobeetJob
      - sfJobeetLanguage

最後のステップは、functional testsをします。

Plugin Activation

pluginは、ProjectConfigurationクラスで利用可能にしなければなりません。
デフォルトのブラックリスト形式の場合は、変更する必要はありません。

<?php
// config/ProjectConfiguration.class.php

public function setup()
{
  // 基本全部利用可能で、引数で示すpluginのみ利用不可
  // 僕は、sfDoctrinePluginを使っているので、sfPropelPluginが不可になってます
  $this->enableAllPluginsExcept(array('sfDoctrinePlugin', 'sfCompat10Plugin'));
}

古いsymfonyの場合は、変更を加える必要があります。しかし、enablePlugins()関数を用いた、このホワイトリスト形式はよい方法です。

<?php
// config/ProjectConfiguration.class.php

public function setup()
{
  $this->enablePlugins(array('sfDoctrinePlugin', 'sfGuardPlugin', 'sfFormExtraPlugin', 'sfJobeetPlugin'));
}
The Tasks

タスクをpluginに移動させるのは、非常に簡単です。

$ mv lib/task plugins/sfJobeetPlugin/lib/
The i18n Files

pluginはXLIFFファイルも内包しています

$ mv apps/frontend/i18n plugins/sfJobeetPlugin/
The Routing

pluginはroutingファイルも内包しています

$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/
The Assets

多少わかりにくくても、pluginはimages, stylesheetsそしてJavaScriptsファイルを含むことができます。Jobeet pluginを分散させたくないと思うとき、本当に理解できませんが、それはplugins/sfJobeetPlugin/web/ディレクトリを作成することによって、可能です。
plugin:publish-assetsタスクは、webディレクトリから、pluginのwebディレクトリへシンボリックリンクを張ります。これにより、ブラウザからpluginのwebディレクトリへのアクセスが可能となります。
非推奨なのかな?

$ php symfony plugin:publish-assets
The User

jobのhistoryに関する関数を移動させる必要があります。JobeetUserクラスを作ってmyUserクラスに継承させることが可能です。しかし、もっといい方法があります。特に、いくつかのpluginがクラスに新しい関数を追加したい場合に。
symfonyのコアオブジェクトは、イベントを通知します。今回のケースでは、sfUserオブジェクトで未定義のメソッドが呼ばれた場合に起こるuser.method_not_foundイベントを取得する必要があります。
symfonyが初期化させたとき、plugin configurationクラスを持つ全てのpluginも初期化されます。

<?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を関連付けます。

PHP callableは、PHPの変数でcall_user_func()関数を利用できるように変換されます。そして、is_callable()関数に伝搬したときにtrueを返します。文字列は関数に、配列はオブジェクトもしくはクラスのメソッドに相当します。

上記コードにより、myUserオブジェクトはメソッドが見つからないときはいつも、JobeetUserクラスの静的関数である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)
  {
    // $ids = $this->getAttribute('job_history', array()); thisではなくuser
    $ids = $user->getAttribute('job_history', array()); 

    if (!empty($ids))
    {
      return Doctrine::getTable('JobeetJob')
        ->createQuery('a')
        ->whereIn('a.id', $ids)
        ->execute();
    } else {
      return array();
    }
  }
 
  static public function resetJobHistory(sfUser $user)
  {
    $user->getAttributeHolder()->remove('job_history');
  }
}


ディスパッチャがmethodNotFound()メソッドを呼ぶと、sfEventオブジェクトに通知されます。
methodNotFound()メソッドがJobeetUserクラスに存在する場合、呼び出された後に通知を返します。もし、存在しない場合はsymfonyは次のイベントリスナーを呼び出すか、例外を投げます。
getSubject()メソッドは、イベント(この場合のイベントは現在のmyUserオブジェクト)の通知を返します。
新たなクラスを作成した場合は、ブラウジングやテストの前にキャッシュをクリアするのを忘れないでください。

$ php symfony cc
The Default Structure vs. the Plugin Architecture

pluginを構築することで、あなたは違った方法でコードの整理をすることができます。

Using Plugins

あなたが新機能を実行し始めるもしくは、古典的なウェブ問題を解決しようとするなら、だれかが既に同じ問題を解決して、symfony pluginとして解決策をパッケージしている可能性があります。公式pluginを探すなら、plugin sectionを訪れてみてください。
pluginは、インストールするための様々な方法を内包しています。

  • plugin:installタスクを用いる (制作者がpluginをパッケージ化していて、かつsymfonyのウェブサイトにアップロードしてある場合に限り実行可能です。)
  • パッケージをダウンロードして、手動でpluginディレクトリ以下に解凍する(この方法もまた、制作者がパッケージをアップロードしている必要があります。)
  • pluginをsvn:extarnals(外部定義)する (制作者がpluginをsubversionでホストしているだけで、定義可能です。)

後ろ二つの方法は、簡単ですが柔軟性に欠けます。最初の方法は、symfonyのバージョンにあった最新版をインストールすることが可能で、アップグレードも容易です。さらに、pluginの依存関係解消も容易です。

Contributing a Plugin

Packaging a Plugin

pluginをパッケージ化するためにいくつかの必須ファイルを加える必要があります。始めに、READMEファイルをpluginのルートディレクトリに作成します。そして、インストール方法、何を提供するのか等々の説明を書き込みます。READMEファイルはMarkdown formatに従う必要があります。このファイルは、symfonyのウェブサイトで利用可能な多くのドキュメントの一つです。symfony plugin dingusを用いれば、READMEファイルをHTMLに変換してテストすることもできます。

Plugin Development Tasks

private, publicなpluginを作成する場合は、sfTaskExtraPluginのタスクを利用するといいでしょう。このpluginは主要チームにより保守されており、あなたのplugin作成の助けとなるでしょう。

  • generate:plugin
  • plugin:package


LICENSEファイルも作る必要があります。ライセンスを選ぶのは容易ではないでしょうが、symfonyのpluginは、symfonyと酷似したライセンス(MIT, BSD, LGPL, and PHP)のpluginのみをリストアップしています。pluginは、そのライセンスに従って公開されます。
最後のステップは、package.xmlをpluginのルートディレクトリに作成します。このファイルは、PEAR package syntaxに従います。

package.xmlの構文を学ぶには、既存のpluginをまねするといいでしょう。

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>

contentタグは、packageに含まれるファイル情報を含みます。

<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タグは、pluginの依存関係(PHP, symfonyのバージョンや他のpluginが必要であるなど)を含みます。この情報は、plugin:installタスクで利用され、現在の環境に最適なpluginがインストールされます。必要ならば、依存関係を考慮して他のpluginもインストールされます。

<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に依存関係を明示しなければなりません。インストール可能なsymfonyの最低と最高のバージョン情報や依存関係にあるplugin情報を含めることができます。

<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タブやplugin feedで提供が可能です。

<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>
Hosting a Plugin on the symfony Website

もし、pluginを作成してコミュニティで共有したいなら、アカウントを作成しましょう。既にアカウントを所持している場合は、new pluginページでpluginの登録を行います。
pluginの管理者になると、自動的にadminタブでplugin管理に必要な情報を得ることができます。