15日目: Web サービス

Jobeet のフィード追加に関して、求職者はリアルタイムで新しい求人情報が知らされます。

一方では、求人を投稿するとき、できる限りもっとも注目されたいと願います。たくさんの小さな Web サイトで求人情報が配信される場合、適任者を見つける機会が増えます。これが~ロングテール~の力です。今日開発する ~Web サービス~のおかげでアフィリエイトは自身の Web サイトに投稿された最新の求人情報を公開できます。

~アフィリエイト~

2日目の要件です:

「ストーリー F7: アフィリエイトは現在アクティブな求人リストを検索する」

フィクスチャ

アフィリエイトのために新しい~フィクスチャ~ファイルを作りましょう:

# data/fixtures/030_affiliates.yml # data/fixtures/affiliates.yml JobeetAffiliate: sensio_labs: url: http://www.sensio-labs.com/ email: [email protected] is_active: true token: sensio_labs jobeet_category_affiliates: [programming] JobeetCategories: [programming]

  symfony:
    url:       http://www.symfony-project.org/
    email:     [email protected]
    is_active: false
    token:     symfony

jobeet_category_affiliates: [design, programming] JobeetCategories: [design, programming]

多対多のリレーションシップのまんなかのテーブル用のレコード作成はまんなかのテーブルのキーに s を追加した配列を定義することで簡単に実現できます。 多対多のリレーションシップ用のレコード作成はリレーションシップの名前であるキーをもつ配列を定義することで簡単にできます。 配列の内容はフィクスチャファイルで定義されたオブジェクトの名前です。異なるファイルからオブジェクトをリンクできますが、まず名前を定義しなければなりません。

フィクスチャファイルにおいて、テスト作業を簡略化するために~トークン~は決め打ちされていますが、実際のユーザーがアカウントを申し込むとき、トークンの生成が必要になります:

[php] // lib/model/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function save(PropelPDO $con = null) { if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); }

    return parent::save($con);
  }

  // ...
}

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

    return parent::save($conn);
  }

  // ...
}

データをリロードできます:

$ php symfony propel:data-load

求人情報の Web サービス

新しいリソースを作成するとき、最初に ~URL~ を定義するのはよい習慣です:

#

このルートに関して、特殊変数の ~sf_format~ は URL で終わり、有効な値は xmljson もしくは yaml です。

アクションがルートに関連するオブジェクトのコレクションを読み取るときに、getForToken() メソッドが呼び出されます。アフィリエイトがアクティベートされることを確認する必要があるとき、ルートのデフォルトのふるまいをオーバーライドする必要があります:

[php] // lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getForToken(array $parameters) { $affiliate = JobeetAffiliatePeer::getByToken($parameters['token']); if (!$affiliate || !$affiliate->getIsActive()) { throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token'])); }

    return $affiliate->getActiveJobs();
  }

  // ...
}

[php] // lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function getForToken(array $parameters) { $affiliate = Doctrine_Core::getTable('JobeetAffiliate') ➥ ->findOneByToken($parameters['token']); if (!$affiliate || !$affiliate->getIsActive()) { throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token'])); }

    return $affiliate->getActiveJobs();
  }

  // ...
}

トークンがデータベースに存在しない場合、sfError404Exception の例外が投げられます。この例外クラスは自動的に~404|404エラー~レスポンスに変換されます。これがモデルクラスから404ページを生成するためのもっともシンプルな方法です。

getForToken() メソッドは今新しく作る2つのメソッドを使います。

最初に、トークンを渡されたアフィリエイトを取得するために getByToken() メソッドを作らなければなりません:

// lib/model/JobeetAffiliatePeer.php
class JobeetAffiliatePeer extends BaseJobeetAffiliatePeer
{
  static public function getByToken($token)
  {
    $criteria = new Criteria();
    $criteria->add(self::TOKEN, $token);
 
    return self::doSelectOne($criteria);
  }
}
 

それから、getActiveJobs() メソッドはアフィリエイトによって選択されたカテゴリ用の現在アクティブな求人リストを返します:

getForToken() メソッドは getActiveJobs() という名前の新しいメソッドを使い現在アクティブな求人リストを返します:

[php] // lib/model/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function getActiveJobs() { $cas = $this->getJobeetCategoryAffiliates(); $categories = array(); foreach ($cas as $ca) { $categories[] = $ca->getCategoryId(); }

    $criteria = new Criteria();
    $criteria->add(JobeetJobPeer::CATEGORY_ID, $categories, Criteria::IN);
    JobeetJobPeer::addActiveJobsCriteria($criteria);

    return JobeetJobPeer::doSelect($criteria);
  }

  // ...
}

[php] // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function getActiveJobs() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->leftJoin('c.JobeetAffiliates a') ->where('a.id = ?', $this->getId());

    $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q);

    return $q->execute();
  }

  // ...
}

最後のステップは api アクションとテンプレートを作ることです。generate:module タスクでモジュールをブートストラップします:

$ php symfony generate:module frontend api

デフォルトの index アクションを使わないので、アクションクラスからこれを除外し、関連する indexSucess.php テンプレートを除外します。

アクション

すべてのフォーマットは同じ list アクションを共有します:

// apps/frontend/modules/api/actions/actions.class.php
public function executeList(sfWebRequest $request)
{
  $this->jobs = array();
  foreach ($this->getRoute()->getObjects() as $job)
  {
    $this->jobs[$this->generateUrl('job_show_user', $job, true)] =
     ➥ $job->asArray($request->getHost());
  }
}
 

テンプレートに JobeetJob オブジェクトの配列を渡す代わりに、文字列の配列を渡します。同じアクションに対して3つの異なるテンプレートがあるので、値を処理するロジックは JobeetJob::asArray() メソッドで取り除かれました:

 
 

// lib/model/JobeetJob.php // lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function asArray($host) { return array( 'category' => $this->getJobeetCategory()->getName(), 'type' => $this->getType(), 'company' => $this->getCompany(), 'logo' => $this->getLogo() ? 'http://'.$host.'/uploads/jobs/'.$this->getLogo() : null, 'url' => $this->getUrl(), 'position' => $this->getPosition(), 'location' => $this->getLocation(), 'description' => $this->getDescription(), 'how_to_apply' => $this->getHowToApply(), 'expires_at' => $this->getExpiresAt('c'), 'expires_at' => $this->getExpiresAt(), ); }

  // ...
}

xml フォーマット

テンプレートを作れば xml フォーマットを簡単にサポートできます:

<!-- apps/frontend/modules/api/templates/listSuccess.xml.php -->
<?xml version="1.0" encoding="utf-8"?>
<jobs>
<?php foreach ($jobs as $url => $job): ?>
  <job url="<?php echo $url ?>">
<?php foreach ($job as $key => $value): ?>
    <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>>
<?php endforeach ?>
  </job>
<?php endforeach ?>
</jobs>
 

json フォーマット

JSON フォーマットのサポートも同じです:

<!-- apps/frontend/modules/api/templates/listSuccess.json.php -->
[
<?php $nb = count($jobs); $i = 0; foreach ($jobs as $url => $job): ++$i ?>
{
  "url": "<?php echo $url ?>",
<?php $nb1 = count($job); $j = 0; foreach ($job as $key => $value): ++$j ?>
  "<?php echo $key ?>": <?php echo json_encode($value).($nb1 == $j ? '' : ',') ?>
 
<?php endforeach ?>
}<?php echo $nb == $i ? '' : ',' ?>
 
<?php endforeach ?>
]
 

~yaml フォーマット|フォーマット (作成)~

組み込みのフォーマットに関して、Content-Type を変更する、レイアウトを無効にするなど、 symfony はバックグランドで同じコンフィギュレーションを変更 します。

YAML フォーマットは組み込みのリクエストフォーマットのリストにないので、レスポンスの Content-Type は変更可能でレイアウトはアクションでは無効です:

class apiActions extends sfActions
{
  public function executeList(sfWebRequest $request)
  {
    $this->jobs = array();
    foreach ($this->getRoute()->getObjects() as $job)
    {
      $this->jobs[$this->generateUrl('job_show_user', $job, true)] =
       ➥ $job->asArray($request->getHost());
    }
 
    switch ($request->getRequestFormat())
    {
      case 'yaml':
        $this->setLayout(false);
        $this->getResponse()->setContentType('text/yaml');
        break;
    }
  }
}
 

アクションにおいて、setLayout() メソッドはデフォルトの~レイアウト|レイアウト (無効)~を変更もしくは false にセットされているときにそれを無効にします。

YAML 用のテンプレートは次の内容を読み込みます:

<!-- apps/frontend/modules/api/templates/listSuccess.yaml.php -->
<?php foreach ($jobs as $url => $job): ?>
-
  url: <?php echo $url ?>
 
<?php foreach ($job as $key => $value): ?>
  <?php echo $key ?>: <?php echo sfYaml::dump($value) ?>
 
<?php endforeach ?>
<?php endforeach ?>
 

有効ではないトークンで Web サービスを呼び出すとき、XML フォーマットの404ページ、JSON フォーマットの404ページを用意します。しかし YAML フォーマットに関して、symfony は何をレンダリングすればよいのかわかりません。

フォーマットを作成するとき、~カスタムエラーテンプレート~を作らなければなりません。テンプレートは404ページと他のすべての例外に使われます。

~例外|例外処理~は運用環境と開発環境で異なるので、2つのファイルが必要です (デバッグに config/error/exception.yaml.php、運用環境に config/error/error.yaml.php):

// config/error/exception.yaml.php
<?php echo sfYaml::dump(array(
  'error'       => array(
    'code'      => $code,
    'message'   => $message,
    'debug'     => array(
      'name'    => $name,
      'message' => $message,
      'traces'  => $traces,
    ),
)), 4) ?>
 
// config/error/error.yaml.php
<?php echo sfYaml::dump(array(
  'error'       => array(
    'code'      => $code,
    'message'   => $message,
))) ?>
 

試す前に、YAML フォーマット用のレイアウトを作らなければなりません:

// apps/frontend/templates/layout.yaml.php
<?php echo $sf_content ?>
 

404エラー

組み込みのテンプレートの404エラーと~例外|例外処理~をオーバーライドするのは簡単で、config/error/ ディレクトリにファイルを作ります。

~Web サービスのテスト|テスト (Web サービス)~

Web サービスをテストするには、アフィリエイトフィクスチャを data/fixtures/ から test/fixtures/ ディレクトリにコピーして自動生成された apiActionsTest.php ファイルの内容を次のものに置き換えます:

// test/functional/frontend/apiActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
 
$browser = new JobeetTestFunctional(new sfBrowser());
$browser->loadData();
 
$browser->
  info('1 - Web service security')->
 
  info('  1.1 - A token is needed to access the service')->
  get('/api/foo/jobs.xml')->
  with('response')->isStatusCode(404)->
 
  info('  1.2 - An inactive account cannot access the web service')->
  get('/api/symfony/jobs.xml')->
  with('response')->isStatusCode(404)->
 
  info('2 - The jobs returned are limited to the categories configured for the affiliate')->
  get('/api/sensio_labs/jobs.xml')->
  with('request')->isFormat('xml')->
  with('response')->begin()->
    isValid()->
    checkElement('job', 32)->
  end()->
 
  info('3 - The web service supports the JSON format')->
  get('/api/sensio_labs/jobs.json')->
  with('request')->isFormat('json')->
  with('response')->matches('/"category"\: "Programming"/')->
 
  info('4 - The web service supports the YAML format')->
  get('/api/sensio_labs/jobs.yaml')->
  with('response')->begin()->
    isHeader('content-type', 'text/yaml; charset=utf-8')->
    matches('/category\: Programming/')->
  end()
;
 

このテストにおいて、2つの新しいメソッドに注目します:

  • isValid(): XML レスポンスが妥当であるかどうかチェックします
  • isFormat(): これはリクエストのフォーマットをテストします
  • matches() : HTML ではないフォーマットに対して、レスポンスが引数として渡される正規表現を検証します

アフィリエイトアプリケーションのフォーム

Web サービスを提供する準備ができたので、アフィリエイト用のアカウント作成フォームを作りましょう。再度新しい機能をアプリケーションに追加することで古典的なプロセスを説明します。

ルーティング

ご想像のとおり、最初に作るのは~ルート~です:

#

これは新しい設定オプション: actions をもつ古典的な ##ORM## コレクションルートです。ルートによって定義されるデフォルトの7つのアクションすべてが不要なので、actions オプションは newcreate アクションのみにマッチするようにルートに指示します。追加の wait ルートはまもなくアフィリエイトになる人にアカウントに関するフィードバックをします。

ブートストラップする

2番目のステップはモジュールの生成です:

$ php symfony propel:generate-module frontend affiliate JobeetAffiliate --non-verbose-templates

テンプレート

propel:generate-module タスクは古典的な7つのアクションとそれらに対応する~テンプレート~を生成します。templates/ ディレクトリにおいて、_form.phpnewSuccess.php 以外のすべてのファイルを削除します。維持するファイルに関して、これらの内容を次のものに置き換えます:

<!-- apps/frontend/modules/affiliate/templates/newSuccess.php -->
<?php use_stylesheet('job.css') ?>
 
<h1>Become an Affiliate</h1>
 
<?php include_partial('form', array('form' => $form)) ?>
 
<!-- apps/frontend/modules/affiliate/templates/_form.php -->
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
 
<?php echo form_tag_for($form, 'affiliate') ?>
  <table id="job_form">
    <tfoot>
      <tr>
        <td colspan="2">
          <input type="submit" value="Submit" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
</form>
 

waitSuccess.php テンプレートを作ります:

<!-- apps/frontend/modules/affiliate/templates/waitSuccess.php -->
<h1>Your affiliate account has been created</h1>
 
<div style="padding: 20px">
  Thank you!
  You will receive an email with your affiliate token
  as soon as your account will be activated.
</div>
 

最後に、affiliate モジュールを指し示すようにフッターのリンクを変更します:

// apps/frontend/templates/layout.php
<li class="last">
  <a href="<?php echo url_for('affiliate_new') ?>">Become an affiliate</a>
</li>
 

アクション

ここで繰り返しますが、作成フォームのみを使うので、actions.class.php ファイルを開き executeNew()executeCreate()processForm() 以外のすべてのメソッドを削除します。

processForm() アクションに関して、リダイレクト URL を wait アクションに変更します:

// apps/frontend/modules/affiliate/actions/actions.class.php
$this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));
 

wait アクションはシンプルなのでテンプレートに何も渡さずに済みます:

// apps/frontend/modules/affiliate/actions/actions.class.php
public function executeWait(sfWebRequest $request)
{
}
 

アフィリエイトはトークンを選択したり、アカウントを即座にアクティベートできません。~フォーム~をカスタマイズするために JobeetAffiliateForm ファイルを開きます:

 
 

// lib/form/JobeetAffiliateForm.class.php // lib/form/doctrine/JobeetAffiliateForm.class.php class JobeetAffiliateForm extends BaseJobeetAffiliateForm { public function configure() { $this->useFields(array( 'url', 'email', 'jobeet_categories_list' )); $this->widgetSchema['jobeet_category_affiliate_list']->setOption('expanded', true); $this->widgetSchema['jobeet_category_affiliate_list']->setLabel('Categories');

    $this->validatorSchema['jobeet_category_affiliate_list']->setOption('required', true);

$this->widgetSchema['jobeet_categories_list']->setOption('expanded', true); $this->widgetSchema['jobeet_categories_list']->setLabel('Categories');

    $this->validatorSchema['jobeet_categories_list']->setOption('required', true);

    $this->widgetSchema['url']->setLabel('Your website URL');
    $this->widgetSchema['url']->setAttribute('size', 50);

    $this->widgetSchema['email']->setAttribute('size', 50);

    $this->validatorSchema['email'] = new sfValidatorEmail(array('required' => true));
  }
}

新しい sfForm::useFields() メソッドによって、保ちたいフィールドのホワイトリストを指定することができます。言及されていないフィールドはすべてフォームから削除されます。

フォームフレームワークは他のカラムのように~多対多のリレーションシップ|多対多のリレーションシップ (フォーム)~をサポートします。 デフォルトでは、sfWidgetFormChoice ウィジェットのおかげでこのようなリレーションシップはドロップダウンボックスのようなレンダラです。10日目で見たように、expanded オプションを指定してレンダリングされたタグを変更しました。

メールと URL は input タグのデフォルトサイズよりも長くなりがちなので、デフォルトの HTML 属性は setAttribute() メソッドを使用して設定できます。

アフィリエイトのフォーム

テスト

最後のステップは新しい機能に対して~機能テスト|フォーム (テスト)~を書くことです。

affiliate モジュール用の生成テストを次のコードで置き換えます:

// test/functional/frontend/affiliateActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
 
$browser = new JobeetTestFunctional(new sfBrowser());
$browser->loadData();
 
$browser->
  info('1 - An affiliate can create an account')->
 
  get('/affiliate/new')->
  click('Submit', array('jobeet_affiliate' => array(
    'url'                            => 'http://www.example.com/',
    'email'                          => '[email protected]',
 

'jobeet_category_affiliate_list' => array($browser->getProgrammingCategory()->getId()), 'jobeet_categories_list' => array(Doctrine_Core::getTable('JobeetCategory')->findOneBySlug('programming')->getId()), )))-> with('response')->isRedirected()-> followRedirect()-> with('response')->checkElement('#content h1', 'Your affiliate account has been created')->

  info('2 - An affiliate must at least select one category')->

  get('/affiliate/new')->
  click('Submit', array('jobeet_affiliate' => array(
    'url'   => 'http://www.example.com/',
    'email' => '[email protected]',
  )))->

with('form')->isError('jobeet_category_affiliate_list') with('form')->isError('jobeet_categories_list') ;

チェックボックスの選択をシミュレートするには、チェックする識別子の配列を渡します。タスクを簡略化するために、JobeetTestFunctional クラスのなかで新しい getProgrammingCategory() メソッドを作りました:

// lib/test/JobeetTestFunctional.class.php
class JobeetTestFunctional extends sfTestFunctional
{
  public function getProgrammingCategory()
  {
    $criteria = new Criteria();
    $criteria->add(JobeetCategoryPeer::SLUG, 'programming');
 
    return JobeetCategoryPeer::doSelectOne($criteria);
  }
 
  // ...
}
 

しかし getMostRecentProgrammingJob() メソッドのなかにこのコードがあるので、コードを~リファクタリング~して JobeetCategoryPeergetForSlug() メソッドを作りましょう:

// lib/model/JobeetCategoryPeer.php
static public function getForSlug($slug)
{
  $criteria = new Criteria();
  $criteria->add(self::SLUG, $slug);
 
  return self::doSelectOne($criteria);
}
 

JobeetTestFunctional のこのコードの2つの存在を置き換えます。

アフィリエイトのバックエンド

~バックエンド~に関して、管理者によってアフィリエイトをアクティベートするために affiliate モジュールを作成しなければなりません:

$ php symfony propel:generate-admin backend JobeetAffiliate --module=affiliate

新しく作成されたモジュールにアクセスするには、アクティベートされるアフィリエイトの人数を伴うメインメニューにリンクを追加します:

<!-- apps/backend/templates/layout.php -->
<li>
 

Affiliates - Affiliates - countToBeActivated() ?>

// lib/model/JobeetAffiliatePeer.php class JobeetAffiliatePeer extends BaseJobeetAffiliatePeer { static public function countToBeActivated() { $criteria = new Criteria(); $criteria->add(self::IS_ACTIVE, 0);

    return self::doCount($criteria);
  }

// lib/model/doctrine/JobeetAffiliateTable.class.php class JobeetAffiliateTable extends Doctrine_Table { public function countToBeActivated() { $q = $this->createQuery('a') ->where('a.is_active = ?', 0);

    return $q->count();
  }

  // ...

}

バックエンドで必要なアクションのみがアカウントをアクティベートするもしくはアクティベートを解除するので、インターフェイスを少し簡略化するために config セクションにおいて、デフォルトのジェネレータを変更し list ビューからアカウントを直接アクティベートするリンクを追加します:

#

管理者をより生産的にするために、アクティベートされるアフィリエイトのみを表示するようにデフォルトのフィルタを変更します:

// apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class.php
class affiliateGeneratorConfiguration extends BaseAffiliateGeneratorConfiguration
{
  public function getFilterDefaults()
  {
    return array('is_active' => '0');
  }
}
 

書く必要があるのは activatedeactivate アクションに対応するコードだけです:

// apps/backend/modules/affiliate/actions/actions.class.php
class affiliateActions extends autoAffiliateActions
{
  public function executeListActivate()
  {
    $this->getRoute()->getObject()->activate();
 
    $this->redirect('jobeet_affiliate');
  }
 
  public function executeListDeactivate()
  {
    $this->getRoute()->getObject()->deactivate();
 
    $this->redirect('jobeet_affiliate');
  }
 
  public function executeBatchActivate(sfWebRequest $request)
  {
 

$affiliates = JobeetAffiliatePeer::retrieveByPks($request->getParameter('ids')); $q = Doctrine_Query::create() ->from('JobeetAffiliate a') ->whereIn('a.id', $request->getParameter('ids'));

    $affiliates = $q->execute();

    foreach ($affiliates as $affiliate)
    {
      $affiliate->activate();
    }

    $this->redirect('jobeet_affiliate');
  }

  public function executeBatchDeactivate(sfWebRequest $request)
  {

$affiliates = JobeetAffiliatePeer::retrieveByPks($request->getParameter('ids')); $q = Doctrine_Query::create() ->from('JobeetAffiliate a') ->whereIn('a.id', $request->getParameter('ids'));

    $affiliates = $q->execute();

    foreach ($affiliates as $affiliate)
    {
      $affiliate->deactivate();
    }

    $this->redirect('jobeet_affiliate');
  }
}

// lib/model/JobeetAffiliate.php // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function activate() { $this->setIsActive(true);

    return $this->save();
  }

  public function deactivate()
  {
    $this->setIsActive(false);

    return $this->save();
  }

  // ...
}

アフィリエイトのバックエンド

また明日

symfony の ~REST~ アーキテクチャのおかげで、プロジェクト用にWebサービスを実装するのはとても簡単です。しかしながら、今日は読み込みだけの Web サービス用のコードを書いたので、Web サービスの読み書き機能を実装するための symfony の知識は十分にあります。

プロジェクトに新しい機能を追加するプロセスに慣れ親しんでいるので、フロントエンドと対応するバックエンドでアフィリエイトアカウント作成フォームの実装は本当に簡単でした。

2日目の要件を覚えているなら次のとおりです:

"アフィリエイトは返される求人の件数を制限することおよび、カテゴリを指定することでクエリを絞りこむこともできる。"

この機能の実装は簡単なので今夜やってみましょう。

アフィリエイトのアカウントが管理者によって有効にされるときは、アフィリエイトの購読を確認しトークンを渡すためにメールがアフィリエイトに送信されます。メールの送信は明日お話しするトピックです。

ORM

インデックス

Document Index

関連ページリスト

Related Pages

日本語ドキュメント

Japanese Documents

リリース情報
Release Information

Symfony2 に関する情報(公式) Books on symfony