Caution: You are browsing the legacy Logo of the legacy symfony 1 version 1.x part of this website.
This version of symfony is not maintained anymore. If some of your projects still use this version, consider upgrading.

Master Symfony fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
training.sensiolabs.com

Discover SensioLabs' Professional Business Solutions

Peruse our complete Symfony & PHP solutions catalog for your web development needs.
sensiolabs.com
Jobeet

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

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

プラグイン

symfony のプラグイン

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

プライベートプラグイン

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

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

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

tip

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

公開プラグイン

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

これらはプライベートプラグインとまったく同じです。唯一の違いは誰でもプロジェクトにインストールできることです。後で公開プラグインを公開して 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

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

public function setup()
{
  $this->enablePlugins(array(
    'sfPropelPlugin', 
    'sfPropelGuardPlugin',
    'sfFormExtraPlugin',
    '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/

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

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

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

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 --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

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 アクションを更新します:

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

# 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 オブジェクトです。

デフォルト構造 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 ファイルはいくつかの部分で構成されます。このテンプレートの例は次のようなものです:

<!-- 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.3.0</min>
      <max>1.5.0</max>
      <exclude>1.5.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 のプラグインリポジトリは便利で面白く、しかしおかしなプラグインで満たされています。