Caution: You are browsing the legacy symfony 1.x part of this website.

20日目: プラグイン

1.2 / Propel
Symfony version
1.4
Language ORM

昨日は、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

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.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)
  {
    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

マイグレーションの最後のステップはモジュールの名前に対してテストする機能テストを修正することです。

sidebar

プラグインの有効化

プロジェクトで利用できるプラグインは、ProjectConfigurationクラスで有効にしなければなりません。

このステップはデフォルトのコンフィギュレーションでは必要ありません。 プラグインの一部を除いてすべてのプラグインを有効にする"ブラックリスト"の方法がsymfonyにあるからです:

// config/ProjectConfiguration.class.php
public function setup()
{
  $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin'));
}

この方法は古いsymfonyのバージョンとの後方互換性を維持するために必要ですが、"ホワイトリスト"方式のほうがすぐれているので代わりにenablePlugins()メソッドを使います:

// config/ProjectConfiguration.class.php
public function setup()
{
  $this->enablePlugins(array('sfPropelPlugin', 'sfGuardPlugin', 'sfFormExtraPlugin', 'sfJobeetPlugin'));
}

タスク

タスクはプラグインに簡単に移動させることができます:

$ 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に変換することができます。

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