<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Symfony中文教程 &#187; jobeet</title>
	<atom:link href="http://www.newlifeclan.com/symfony/archives/tag/jobeet/feed" rel="self" type="application/rss+xml" />
	<link>http://www.newlifeclan.com/symfony</link>
	<description>站在巨人肩膀上的phpweb框架</description>
	<lastBuildDate>Fri, 12 Dec 2025 00:58:27 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.0.38</generator>
	<item>
		<title>jobeet第十九天:国际化和本地化</title>
		<link>http://www.newlifeclan.com/symfony/archives/392</link>
		<comments>http://www.newlifeclan.com/symfony/archives/392#comments</comments>
		<pubDate>Fri, 27 Mar 2015 09:28:00 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=392</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/392">jobeet第十九天:国际化和本地化</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>昨天我们为搜索引擎添加了AJAX功能，这下搜索引擎变得有趣多了。而在今天的内容中，我们来了解一下Jobeet的国际化（i18n）和本地化（l10n）。</p>
<p><span id="more-392"></span></p>
<blockquote><p>来自<a href="http://zh.wikipedia.org/wiki/%E5%9B%BD%E9%99%85%E5%8C%96">维基百科</a>： <strong>国际化</strong>是指在设计软件，将软件与特定语言及地区脱钩的过程。当软件被移植到不同的语言及地区时，软件本身不用做内部工程上的改变或修正。 <strong>本地化</strong>是指当移植软件时，加上与特定区域设置有关的信息和翻译文件的过程。</p></blockquote>
<h2><a id="user-content-用户" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-19/chapter-19.md#%E7%94%A8%E6%88%B7"></a>用户</h2>
<p>软件或者网站没有国际化就意味着有可能会造成用户的流失。如果你的网站能够支持多种语言或者是能够为世界不同地区的人们服务的话，那么就有必要为用户提供语言选择功能以最大程度地满足用户的需求。</p>
<p>Symfony的i18n和l10n功能是以用户文化（user culture）为基础的。文化是由语言和其所在国家的用户所构成的。比如，那些会说法语的人被称为&#8217;fr&#8217;，而那些来自法国的人（同时也会法语）则被成为&#8217;fr_FR&#8217;。</p>
<p>Symfony中的翻译工作是由<a href="http://symfony.com/doc/current/glossary.html#term-service">Translator</a>来处理的，它会根据用户的本地信息来查找并返回文本的翻译。在使用它之前，我们需要在配置中启用Translator：</p><pre class="crayon-plain-tag"># app/config/config.yml
# ...

framework:
    #esi:             ~
    translator:      { fallback: en }
    # ...
    default_locale:  "en"

# ...</pre><p>&nbsp;</p>
<h2>Culture in the URL</h2>
<p>Jobeet网站需要能够支持英语和法语。因为一个URL就是代表了一个单一的资源，所以我们必须在URL中加入语言参数供用户切换不同语言的资源。为了达到这个目的，我们打开<em>routing.yml</em>文件，除了<em>api</em>路由外，我们需要为所有的路由添加一个特殊的<em>locale</em>变量。为了使路由简单，我们在url的前面加上<em>/{_locale}</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Resources/config/routing.yml
login:
    pattern: /login
    defaults: { _controller: IbwJobeetBundle:Default:login }

login_check:
    pattern: /login_check

logout:
  pattern: /logout

IbwJobeetBundle_category:
    pattern:  /{_locale}/category/{slug}/{page}
    defaults: { _controller: IbwJobeetBundle:Category:show, page: 1 }   
    requirements:
        _locale: en|fr

IbwJobeetBundle_job:
    resource: "@IbwJobeetBundle/Resources/config/routing/job.yml"
    prefix:   /{_locale}/job
    requirements: 
        _locale: en|fr

ibw_jobeet_homepage:
    pattern:  /
    defaults: { _controller: IbwJobeetBundle:Job:index } 

IbwJobeetBundle_api:
    pattern: /api/{token}/jobs.{_format}
    defaults: {_controller: "IbwJobeetBundle:Api:list"}
    requirements:
        _format: xml|json|yaml

IbwJobeetBundle_ibw_affiliate:
    resource: "@IbwJobeetBundle/Resources/config/routing/affiliate.yml"
    prefix:   /{_locale}/affiliate       
    requirements: 
        _locale: en|fr</pre><p>我们的首页应该要尽可能多地支持不同语言（/en/，/fr/,&#8230;）。系统根据用户在url中指定的<em>_locale</em>值转向到特定语言版本的首页（/）。但如果用户还没有指定<em>_locale</em>的值（用户可能是第一次访问Jobeet），那么Jobeet会先为用户选择一种默认语言（我们上面定义的是en）。现在我们来添加一个新路由，然后再修改首页的路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing.yml
# ...
ibw_jobeet_homepage:
    pattern:  /{_locale}/
    defaults: { _controller: IbwJobeetBundle:Job:index }
    requirements: 
        _locale: en|fr
# ...

IbwJobeetBundle_nonlocalized:
    pattern:  /
    defaults: { _controller: "IbwJobeetBundle:Job:index" }</pre><p>然后我们来添加和修改这两个路由对应的控制器的行为：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $request = $this-&gt;getRequest();

        if($request-&gt;get('_route') == 'IbwJobeetBundle_nonlocalized') {
            return $this-&gt;redirect($this-&gt;generateUrl('ibw_jobeet_homepage'));
        }

        $em = $this-&gt;getDoctrine()-&gt;getManager();

        // ...
    }

// ...</pre><p>如果用户访问Jobeet时没有指定优先选择使用哪种语言的话（http://jobeet.local/app\_dev.php），那么用户将会被重定向到首页（/），并且系统会为用户选择默认的语言（http://jobeet.local/app\_dev.php/en/）。</p>
<h2>Culture测试</h2>
<p>现在是时候测试我们实现的功能了。但在编写测试之前，我们需要先来修改之前编写的测试。因为我们在上面修改过了路由，所有的URL被都改变了，所以我们需要修改之前的功能测试，在测试中的所有URL前面加上/en。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
namespace Ibw\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\ArrayInput;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
use Symfony\Component\DomCrawler\Crawler;

class JobControllerTest extends WebTestCase
{  
    // ...

    public function testIndex()
    {
        // get the custom parameters from app config.yml
        $kernel = static::createKernel();
        $kernel-&gt;boot();
        $max_jobs_on_homepage = $kernel-&gt;getContainer()-&gt;getParameter('max_jobs_on_homepage');

        $client = static::createClient();

        $crawler = $client-&gt;request('GET', '/fr/');
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::indexAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        // If the selected culture is italian, the page requested will not be found
        $crawler = $client-&gt;request('GET', '/it/');
        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());

        $crawler = $client-&gt;request('GET', '/en/');
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::indexAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        // expired jobs are not listed
        $this-&gt;assertTrue($crawler-&gt;filter('.jobs td.position:contains("Expired")')-&gt;count() == 0);

        // only $max_jobs_on_homepage jobs are listed for a category
        $this-&gt;assertTrue($crawler-&gt;filter('.category_programming tr')-&gt;count()&lt;= $max_jobs_on_homepage); 
        $this-&gt;assertTrue($crawler-&gt;filter('.category_design .more_jobs')-&gt;count() == 0);
        $this-&gt;assertTrue($crawler-&gt;filter('.category_programming .more_jobs')-&gt;count() == 1);

        // jobs are sorted by date
        $this-&gt;assertTrue($crawler-&gt;filter('.category_programming tr')-&gt;first()-&gt;filter(sprintf('a[href*="/%d/"]', $this-&gt;getMostRecentProgrammingJob()-&gt;getId()))-&gt;count() == 1);

        // each job on the homepage is clickable and give detailed information
        $job = $this-&gt;getMostRecentProgrammingJob();
        $link = $crawler-&gt;selectLink('Web Developer')-&gt;first()-&gt;link();
        $crawler = $client-&gt;click($link);
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::showAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
        $this-&gt;assertEquals($job-&gt;getCompanySlug(), $client-&gt;getRequest()-&gt;attributes-&gt;get('company'));
        $this-&gt;assertEquals($job-&gt;getLocationSlug(), $client-&gt;getRequest()-&gt;attributes-&gt;get('location'));
        $this-&gt;assertEquals($job-&gt;getPositionSlug(), $client-&gt;getRequest()-&gt;attributes-&gt;get('position'));
        $this-&gt;assertEquals($job-&gt;getId(), $client-&gt;getRequest()-&gt;attributes-&gt;get('id'));

        // a non-existent job forwards the user to a 404
        $crawler = $client-&gt;request('GET', '/en/job/foo-inc/milano-italy/0/painter');
        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());

        // an expired job page forwards the user to a 404
        $crawler = $client-&gt;request('GET', sprintf('/en/job/sensio-labs/paris-france/%d/web-developer', $this-&gt;getExpiredJob()-&gt;getId()));
        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());
    }

    public function testJobForm()
    {
        $client = static::createClient();
        $crawler = $client-&gt;request('GET', '/en/job/new');

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::newAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array(
            'job[company]'      =&gt; 'Sensio Labs',
            'job[url]'          =&gt; 'http://www.sensio.com',
            'job[file]'         =&gt; __DIR__.'/../../../../../web/bundles/ibwjobeet/images/sensio-labs.gif',
            'job[how_to_apply]' =&gt; 'Send me an email',
            'job[description]'  =&gt; 'You will work with symfony to develop websites for our customers',
            'job[location]'     =&gt; 'Atlanta, USA',
            'job[email]'        =&gt; 'for.a.job@example.com',
            'job[position]'     =&gt; 'Developer',
            'job[is_public]'    =&gt; false,
        ));

        $client-&gt;submit($form);
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::createAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $client-&gt;followRedirect();
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::previewAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $kernel = static::createKernel();
        $kernel-&gt;boot();
        $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

        $query = $em-&gt;createQuery('SELECT count(j.id) from IbwJobeetBundle:Job j WHERE j.location = :location AND j.is_activated IS NULL AND j.is_public = 0');
        $query-&gt;setParameter('location', 'Atlanta, USA');
        $this-&gt;assertTrue(0 &lt; $query-&gt;getSingleScalarResult());

        $crawler = $client-&gt;request('GET', '/en/job/new');
        $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array(
            'job[company]'      =&gt; 'Sensio Labs',
            'job[position]'     =&gt; 'Developer',
            'job[location]'     =&gt; 'Atlanta, USA',
            'job[email]'        =&gt; 'not.an.email',
        ));
        $crawler = $client-&gt;submit($form);

        // check if we have 3 errors
        $this-&gt;assertTrue($crawler-&gt;filter('.error_list')-&gt;count() == 3);
        // check if we have error on job_description field
        $this-&gt;assertTrue($crawler-&gt;filter('#job_description')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);
        // check if we have error on job_how_to_apply field
        $this-&gt;assertTrue($crawler-&gt;filter('#job_how_to_apply')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);
        // check if we have error on job_email field
        $this-&gt;assertTrue($crawler-&gt;filter('#job_email')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);
    }

    public function createJob($values = array(), $publish = false)
    {
        $client = static::createClient();
        $crawler = $client-&gt;request('GET', '/en/job/new');
        $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array_merge(array(
            'job[company]'      =&gt; 'Sensio Labs',
            'job[url]'          =&gt; 'http://www.sensio.com/',
            'job[position]'     =&gt; 'Developer',
            'job[location]'     =&gt; 'Atlanta, USA',
            'job[description]'  =&gt; 'You will work with symfony to develop websites for our customers.',
            'job[how_to_apply]' =&gt; 'Send me an email',
            'job[email]'        =&gt; 'for.a.job@example.com',
            'job[is_public]'    =&gt; false,
        ), $values));

        $client-&gt;submit($form);
        $client-&gt;followRedirect();

        if($publish) {
            $crawler = $client-&gt;getCrawler();
            $form = $crawler-&gt;selectButton('Publish')-&gt;form();
            $client-&gt;submit($form);
            $client-&gt;followRedirect();
        }

        return $client;
    }

    // ...

    public function testEditJob()
    {
        $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO3'), true);
        $crawler = $client-&gt;getCrawler();
        $crawler = $client-&gt;request('GET', sprintf('/en/job/%s/edit', $this-&gt;getJobByPosition('FOO3')-&gt;getToken()));
        $this-&gt;assertTrue( 404 === $client-&gt;getResponse()-&gt;getStatusCode());
    }

    public function testExtendJob()
    {
        // A job validity cannot be extended before the job expires soon
        $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO4'), true);
        $crawler = $client-&gt;getCrawler();
        $this-&gt;assertTrue($crawler-&gt;filter('input[type=submit]:contains("Extend")')-&gt;count() == 0);

        // A job validity can be extended hen the job expires soon
        // Create a new FOO5 job
        $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO5'), true);
        // Get the job and change the expire date to today
        $kernel = static::createKernel();
        $kernel-&gt;boot();
        $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');
        $job = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByPosition('FOO5');
        $job-&gt;setExpiresAt(new \DateTime());
        $em-&gt;flush();

        // Go to preview page and extend the job
        $crawler = $client-&gt;request('GET', sprintf('/en/job/%s/%s/%s/%s', $job-&gt;getCompanySlug(), $job-&gt;getLocationSlug(), $job-&gt;getToken(), $job-&gt;getPositionSlug()));
        $crawler = $client-&gt;getCrawler();

        $form = $crawler-&gt;selectButton('Extend')-&gt;form();
        $client-&gt;submit($form);
        $client-&gt;followRedirect();
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::previewAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        // Reload the job from database
        $job = $this-&gt;getJobByPosition('FOO5');

        // Check the expiration date
        $this-&gt;assertTrue($job-&gt;getExpiresAt()-&gt;format('y/m/d') == date('y/m/d', time() + 86400 * 30));
    }

    public function testSearch()
    {
        $client = static::createClient();

        $crawler = $client-&gt;request('GET', '/en/job/search');
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::searchAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $crawler = $client-&gt;request('GET', '/en/job/search?query=sens*', array(), array(), array(
            'X-Requested-With' =&gt; 'XMLHttpRequest',
        ));
        $this-&gt;assertTrue($crawler-&gt;filter('tr')-&gt;count()== 2);
    }
}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/AffiliateControllerTest.php
// ...     

public function testAffiliateForm()
{
    $client = static::createClient();
    $crawler = $client-&gt;request('GET', '/en/affiliate/new');

    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::newAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

    $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(
        'affiliate[url]'   =&gt; 'http://sensio-labs.com/',
        'affiliate[email]' =&gt; 'fabien.potencier@example.com'
    ));

    $client-&gt;submit($form);
    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::createAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

    $kernel = static::createKernel();
    $kernel-&gt;boot();
    $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

    $crawler = $client-&gt;request('GET', '/en/affiliate/new');
    $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(
        'affiliate[email]'        =&gt; 'not.an.email',
    ));
    $crawler = $client-&gt;submit($form);

    // check if we have 1 errors
    $this-&gt;assertTrue($crawler-&gt;filter('.error_list')-&gt;count() == 1);
    // check if we have error on affiliate_email field
    $this-&gt;assertTrue($crawler-&gt;filter('#affiliate_email')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);
}

public function testCreate()
{
    $client = static::createClient();
    $crawler = $client-&gt;request('GET', '/en/affiliate/new');
    $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(
        'affiliate[url]'   =&gt; 'http://sensio-labs.com/',
        'affiliate[email]' =&gt; 'address@example.com'
    ));

    $client-&gt;submit($form);
    $client-&gt;followRedirect();

    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::waitAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

    return $client;
}

public function testWait()
{
    $client = static::createClient();
    $crawler = $client-&gt;request('GET', '/en/affiliate/wait');

    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::waitAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
}

// ...</pre><p>&nbsp;</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/CategoryControllerTest.php
// ...

public function testShow()
{
    $kernel = static::createKernel();
    $kernel-&gt;boot();

    // get the custom parameters from app/config.yml
    $max_jobs_on_category = $kernel-&gt;getContainer()-&gt;getParameter('max_jobs_on_category');
    $max_jobs_on_homepage = $kernel-&gt;getContainer()-&gt;getParameter('max_jobs_on_homepage');

    $client = static::createClient();

    $categories = $this-&gt;em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();

    // categories on homepage are clickable
    foreach($categories as $category) {
        $crawler = $client-&gt;request('GET', '/en/');

        $link = $crawler-&gt;selectLink($category-&gt;getName())-&gt;link();
        $crawler = $client-&gt;click($link);

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\CategoryController::showAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
        $this-&gt;assertEquals($category-&gt;getSlug(), $client-&gt;getRequest()-&gt;attributes-&gt;get('slug'));

        $jobs_no = $this-&gt;em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;countActiveJobs($category-&gt;getId()); 

        // categories with more than $max_jobs_on_homepage jobs also have a "more" link                 
        if($jobs_no &gt; $max_jobs_on_homepage) {
            $crawler = $client-&gt;request('GET', '/en/');
            $link = $crawler-&gt;filter(".category_" . $category-&gt;getSlug() . " .more_jobs a")-&gt;link();
            $crawler = $client-&gt;click($link);

            $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\CategoryController::showAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
            $this-&gt;assertEquals($category-&gt;getSlug(), $client-&gt;getRequest()-&gt;attributes-&gt;get('slug'));
        }

        $pages = ceil($jobs_no/$max_jobs_on_category);

        // only $max_jobs_on_category jobs are listed 
        $this-&gt;assertTrue($crawler-&gt;filter('.jobs tr')-&gt;count() &lt;= $max_jobs_on_category);
        $this-&gt;assertRegExp("/" . $jobs_no . " jobs/", $crawler-&gt;filter('.pagination_desc')-&gt;text());

        if($pages &gt; 1) {
            $this-&gt;assertRegExp("/page 1\/" . $pages . "/", $crawler-&gt;filter('.pagination_desc')-&gt;text());

            for ($i = 2; $i &lt;= $pages; $i++) {
                $link = $crawler-&gt;selectLink($i)-&gt;link();
                $crawler = $client-&gt;click($link);

                $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\CategoryController::showAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
                $this-&gt;assertEquals($i, $client-&gt;getRequest()-&gt;attributes-&gt;get('page'));
                $this-&gt;assertTrue($crawler-&gt;filter('.jobs tr')-&gt;count() &lt;= $max_jobs_on_category);
                if($jobs_no &gt; 1) {
                    $this-&gt;assertRegExp("/" . $jobs_no . " jobs/", $crawler-&gt;filter('.pagination_desc')-&gt;text());
                }
                $this-&gt;assertRegExp("/page " . $i . "\/" . $pages . "/", $crawler-&gt;filter('.pagination_desc')-&gt;text());
            }
        }     
    }
}

// ...</pre><p>&nbsp;</p>
<h2>语言切换</h2>
<p>为了让用户能够切换语言，我们需要在layout中添加一个表单。现在我们来修改它：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;
&lt;div id="footer"&gt;
    &lt;div class="content"&gt;
        &lt;!-- ... --&gt;
        &lt;form action="{{ path('IbwJobeetBundle_changeLanguage') }}" method="get"&gt;
            &lt;label&gt;Language&lt;/label&gt;
                &lt;select name="language"&gt;
                    &lt;option value="en" {% if app.request.get('_locale') == 'en' %}selected="selected"{% endif %}&gt;English&lt;/option&gt;
                    &lt;option value="fr" {% if app.request.get('_locale') == 'fr' %}selected="selected"{% endif %}&gt;French&lt;/option&gt;
                &lt;/select&gt;
            &lt;input type="submit" value="Ok"&gt; 
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;!-- ... --&gt;</pre><p>现在来添加路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing.yml
# ...

IbwJobeetBundle_changeLanguage:
    pattern: /change_language
    defaults: { _controller: "IbwJobeetBundle:Default:changeLanguage" }</pre><p>然后添加Action：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/DefaultController.php
namespace Ibw\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class DefaultController extends Controller
{
    // ...

    public function changeLanguageAction()
    {
        $language = $this-&gt;getRequest()-&gt;get('language');
        return $this-&gt;redirect($this-&gt;generateUrl('ibw_jobeet_homepage', array('_locale' =&gt; $language)));
    }
}</pre><p>添加完成之后不要忘了清除cache。</p>
<p>&nbsp;</p>
<h2>模板</h2>
<p>一个国际化的网站就意味着它支持多种不同国家语言版本的用户界面和用户接口。对于Jobeet来说，默认支持的是英语，然后是法语。为了翻译模板，我们会使用Twig的<em>{% trans %}</em>标签。Symfony在渲染模板时，每当遇到一个<em>{% trans %}</em>标签，Symfony就会去查找用户当前的本地信息来得到对应的翻译。如果找到了对应的翻译字符串，那么就返回它。如果没有找到，那么需要被翻译的字符串就会被当做备用（fallback）值返回。</p>
<p>所有的翻译文件都保存在<em>src/Ibw/JobeetBundle/Resources/translations/</em>目录下，我们将会使用XLIFF格式来保存翻译文本，它是一个标准，而且灵活性好。</p>
<p>现在我们在模板中添加<em>{% trans %}</em>标签：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;
            {% block title %}
                {% trans %}Jobeet - Your best job board{% endtrans %}
            {% endblock %}
        &lt;/title&gt;
        &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&gt;
        {% block stylesheets %}
            &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/main.css') }}" type="text/css" media="all" /&gt;
            &lt;link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="{{ url('ibw_job', {'_format': 'atom'}) }}" /&gt;
        {% endblock %}
        {% block javascripts %}
            &lt;script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/jquery-2.0.3.min.js') }}"&gt;&lt;/script&gt;
            &lt;script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/search.js') }}"&gt;&lt;/script&gt;
        {% endblock %}
        &lt;link rel="shortcut icon" href="{{ asset('bundles/ibwjobeet/images/favicon.ico') }}" /&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;div id="container"&gt;
            &lt;div id="header"&gt;
                &lt;div class="content"&gt;
                    &lt;h1&gt;&lt;a href="{{ path('ibw_jobeet_homepage') }}"&gt;
                        &lt;img alt="Jobeet Job Board" src="{{ asset('bundles/ibwjobeet/images/logo.jpg') }}" /&gt;
                    &lt;/a&gt;&lt;/h1&gt;

                    &lt;div id="sub_header"&gt;
                        &lt;div class="post"&gt;
                            &lt;h2&gt;{% trans %}Ask for people{% endtrans %}&lt;/h2&gt;
                            &lt;div&gt;
                                &lt;a href="{{ path('ibw_job_new') }}"&gt;{% trans %}Post a Job{% endtrans %}&lt;/a&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;

                        &lt;div class="search"&gt;
                            &lt;h2&gt;{% trans %}Ask for a job{% endtrans %}&lt;/h2&gt;
                            &lt;form action="{{ path('ibw_job_search') }}" method="get"&gt;
                                &lt;input type="text" name="query" value="{{ app.request.get('query') }}" id="search_keywords" /&gt;
                                &lt;input type="submit" value="search" /&gt;
                                &lt;img id="loader" src="{{ asset('bundles/ibwjobeet/images/loader.gif') }}" style="vertical-align: middle; display: none" /&gt;
                                &lt;div class="help"&gt;
                                    {% trans %}Enter some keywords (city, country, position, ...){% endtrans %}
                                &lt;/div&gt;
                            &lt;/form&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
           &lt;div id="job_history"&gt;
                {% trans %}Recent viewed jobs:{% endtrans %}
                &lt;ul&gt;
                    {% for job in app.session.get('job_history') %}
                        &lt;li&gt;
                            &lt;a href="{{ path('ibw_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}"&gt;{{ job.position }} - {{ job.company }}&lt;/a&gt;
                        &lt;/li&gt;
                    {% endfor %}
                &lt;/ul&gt;
            &lt;/div&gt;
           &lt;div id="content"&gt;
               {% for flashMessage in app.session.flashbag.get('notice') %}
                   &lt;div class="flash_notice"&gt;
                       {{ flashMessage }}
                   &lt;/div&gt;
               {% endfor %}

               {% for flashMessage in app.session.flashbag.get('error') %}
                   &lt;div class="flash_error"&gt;
                       {{ flashMessage }}
                   &lt;/div&gt;
               {% endfor %}

               &lt;div class="content"&gt;
                   {% block content %}
                   {% endblock %}
               &lt;/div&gt;
           &lt;/div&gt;

           &lt;div id="footer"&gt;
               &lt;div class="content"&gt;
                   &lt;span class="symfony"&gt;
                       &lt;img src="{{ asset('bundles/ibwjobeet/images/jobeet-mini.png') }}" /&gt;
                           powered by &lt;a href="http://www.symfony.com/"&gt;
                           &lt;img src="{{ asset('bundles/ibwjobeet/images/symfony.gif') }}" alt="symfony framework" /&gt;
                       &lt;/a&gt;
                   &lt;/span&gt;
                   &lt;ul&gt;
                       &lt;li&gt;&lt;a href=""&gt;{% trans %}About Jobeet{% endtrans %}&lt;/a&gt;&lt;/li&gt;
                       &lt;li class="feed"&gt;&lt;a href="{{ path('ibw_job', {'_format': 'atom'}) }}"&gt;{% trans %}Full feed{% endtrans %}&lt;/a&gt;&lt;/li&gt;
                       &lt;li&gt;&lt;a href=""&gt;{% trans %}Jobeet API{% endtrans %}&lt;/a&gt;&lt;/li&gt;
                       &lt;li class="last"&gt;&lt;a href="{{ path('ibw_affiliate_new') }}"&gt;{% trans %}Become an affiliate{% endtrans %}&lt;/a&gt;&lt;/li&gt;
                   &lt;/ul&gt;
                   &lt;form action="{{ path('IbwJobeetBundle_changeLanguage') }}" method="get"&gt;
                       &lt;label&gt;{% trans %}Language{% endtrans %}&lt;/label&gt;
                       &lt;select name="language"&gt;
                           &lt;option value="en" {% if app.request.get('_locale') == 'en' %}selected="selected"{% endif %}&gt;English&lt;/option&gt;
                                &lt;option value="fr" {% if app.request.get('_locale') == 'fr' %}selected="selected"{% endif %}&gt;French&lt;/option&gt;
                       &lt;/select&gt;
                       &lt;input type="submit" value="Ok"&gt; 
                   &lt;/form&gt;
               &lt;/div&gt;
           &lt;/div&gt;
       &lt;/div&gt;
   &lt;/body&gt;
&lt;/html&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/show.html.twig --&gt;
{% extends 'IbwJobeetBundle::layout.html.twig' %}

{% block title %}
    {% trans with {'%company%': entity.company, '%position%': entity.position} %}%company% is looking for a %position%{% endtrans %}
{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/job.css') }}" type="text/css" media="all" /&gt;
{% endblock %}

{% block content %}
    {% if app.request.get('token') %}
        {% include 'IbwJobeetBundle:Job:admin.html.twig' with {'job': entity} %}
    {% endif %}
    &lt;div id="job"&gt;
        &lt;h1&gt;{{ entity.company }}&lt;/h1&gt;
        &lt;h2&gt;{{ entity.location }}&lt;/h2&gt;
        &lt;h3&gt;
            {{ entity.position }}
            &lt;small&gt; - {{ entity.type }}&lt;/small&gt;
        &lt;/h3&gt;

        {% if entity.logo %}
            &lt;div class="logo"&gt;
                &lt;a href="{{ entity.url }}"&gt;
                    &lt;img src="/uploads/jobs/{{ entity.logo }}"
                        alt="{{ entity.company }} logo" /&gt;
                &lt;/a&gt;
            &lt;/div&gt;
        {% endif %}

        &lt;div class="description"&gt;
            {{ entity.description|nl2br }}
        &lt;/div&gt;

        &lt;h4&gt;{% trans %}How to apply?{% endtrans %}&lt;/h4&gt;

        &lt;p class="how_to_apply"&gt;{{ entity.howtoapply }}&lt;/p&gt;

        &lt;div class="meta"&gt;
            &lt;small&gt;{% trans with {'%date%': entity.createdat|date('m/d/Y')} %}posted on %date%{% endtrans %}&lt;/small&gt;
        &lt;/div&gt;
    &lt;/div&gt;
{% endblock %}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/new.html.twig --&gt;
&lt;!-- ... --&gt;
{% block content %}
    &lt;h1&gt;{% trans %}Job creation{% endtrans %}&lt;/h1&gt;
    &lt;!-- ... --&gt;
        &lt;br /&gt; {% trans %}Whether the job can also be published on affiliate websites or not.{% endtrans %}
    &lt;!-- ... --&gt;
&lt;!-- ... --&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/index.html.twig --&gt;
&lt;!-- ... --&gt;
    {% if category.morejobs %}
        &lt;div class="more_jobs"&gt;
            {% trans with {'%count%': '&lt;a href="' ~ path('IbwJobeetBundle_category', { 'slug': category.slug }) ~ '"&gt;' ~  category.morejobs ~ '&lt;/a&gt;'} %}and %count% more...{% endtrans %}
        &lt;/div&gt;
    {% endif %}
&lt;!-- ... --&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/edit.html.twig --&gt;
&lt;!-- ... --&gt;
{% block content %}
    &lt;h1&gt;{% trans %}Job edit{% endtrans %}&lt;/h1&gt;
    &lt;!-- ... --&gt;
        &lt;br /&gt; {% trans %}Whether the job can also be published on affiliate websites or not.{% endtrans %}
    &lt;!-- ... --&gt;
&lt;!-- ... --&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/admin.html.twig --&gt;
&lt;div id="job_actions"&gt;
    &lt;h3&gt;Admin&lt;/h3&gt;
    &lt;ul&gt;
        {% if not job.isActivated %}
            &lt;ul&gt;
                &lt;li&gt;&lt;a href="{{ path('ibw_job_edit', { 'token': job.token }) }}"&gt;{% trans %}Edit{% endtrans %}&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;
                    &lt;form action="{{ path('ibw_job_publish', { 'token': job.token }) }}" method="post"&gt;
                        {{ form_widget(publish_form) }}
                            &lt;button type="submit"&gt;{% trans %}Publish{% endtrans %}&lt;/button&gt;
                    &lt;/form&gt;
                &lt;/li&gt;
            &lt;/ul&gt;
        {% endif %}
        &lt;li&gt;
            &lt;form action="{{ path('ibw_job_delete', { 'token': job.token }) }}" method="post"&gt;
                {{ form_widget(delete_form) }}
                    &lt;button type="submit" onclick="if(!confirm('{% trans %}Are you sure?{% endtrans %}')) { return false; }"&gt;{% trans %}Delete{% endtrans %}&lt;/button&gt;
            &lt;/form&gt;
        &lt;/li&gt;
        {% if job.isActivated %}
            &lt;li {% if job.expiresSoon %} class="expires_soon" {% endif %}&gt;
                {% if job.isExpired %}
                    {% trans %}Expired{% endtrans %}
                {% else %}
                    {% trans with {'%count%':'&lt;strong&gt;' ~ job.getDaysBeforeExpires ~ '&lt;/strong&gt;' } %}Expires in %count% days{% endtrans %}
                {% endif %}

                {% if job.expiresSoon %}
                    &lt;form action="{{ path('ibw_job_extend', { 'token': job.token }) }}" method="post"&gt;
                        {{ form_widget(extend_form) }}
                            &lt;button type="submit" value="Extend"&gt;{% trans %}Extend{% endtrans %}&lt;/button&gt; {% trans %}for another 30 days{% endtrans %}
                    &lt;/form&gt;
                {% endif %}
            &lt;/li&gt;
        {% else %}
            &lt;li&gt;
                [{% trans with {'%url%': '&lt;a href="' ~ url('ibw_job_preview', { 'token': job.token, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) ~ '"&gt;URL&lt;/a&gt;'} %}Bookmark this %url% to manage this job in the future{% endtrans %}.]
            &lt;/li&gt;
        {% endif %}
    &lt;/ul&gt;
&lt;/div&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Category/show.html.twig --&gt;
{% extends 'IbwJobeetBundle::layout.html.twig' %}

{% block title %}
    {% trans with {'%category%': category.name} %}Jobs in the %category% category{% endtrans %}
{% endblock %}

{% block stylesheets %}
    {{ parent() }}
    &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/jobs.css') }}" type="text/css" media="all" /&gt;
{% endblock %}

{% block content %}
    &lt;div class="category"&gt;
        &lt;div class="feed"&gt;
            &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}"&gt;Feed&lt;/a&gt;
        &lt;/div&gt;   
        &lt;h1&gt;{{ category.name }}&lt;/h1&gt;
    &lt;/div&gt;

    {% include 'IbwJobeetBundle:Job:list.html.twig' with {'jobs': category.activejobs} %}

    {% if last_page &gt; 1 %}
        &lt;div class="pagination"&gt;
            &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, 'page': 1 }) }}"&gt;
                &lt;img src="{{ asset('bundles/ibwjobeet/images/first.png') }}" alt="First page" title="First page" /&gt;
            &lt;/a&gt;

            &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, 'page': previous_page }) }}"&gt;
                &lt;img src="{{ asset('bundles/ibwjobeet/images/previous.png') }}" alt="Previous page" title="Previous page" /&gt;
            &lt;/a&gt;

            {% for page in 1..last_page %}
                {% if page == current_page %}
                    {{ page }}
                {% else %}
                    &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, 'page': page }) }}"&gt;{{ page }}&lt;/a&gt;
                {% endif %}
            {% endfor %}

            &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, 'page': next_page }) }}"&gt;
                &lt;img src="{{ asset('bundles/ibwjobeet/images/next.png') }}" alt="Next page" title="Next page" /&gt;
            &lt;/a&gt;

            &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, 'page': last_page }) }}"&gt;
                &lt;img src="{{ asset('bundles/ibwjobeet/images/last.png') }}" alt="Last page" title="Last page" /&gt;
            &lt;/a&gt;
        &lt;/div&gt;
    {% endif %}

    &lt;div class="pagination_desc"&gt;
        {% transchoice total_jobs with {'%count%': '&lt;strong&gt;' ~ total_jobs ~ '&lt;/strong&gt;'} %}
            {0} No job in this category|{1} One job in this category|]1,Inf] %count% jobs in this category
        {% endtranschoice %}
        {% if last_page &gt; 1 %}
            - page &lt;strong&gt;{{ current_page }}/{{ last_page }}&lt;/strong&gt;
        {% endif %}
    &lt;/div&gt;        
{% endblock %}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Affiliate/wait.html.twig --&gt;
{% extends "IbwJobeetBundle::layout.html.twig" %}

{% block content %}
    &lt;div class="content"&gt;
        &lt;h1&gt;{% trans %}Your affiliate account has been created{% endtrans %}&lt;/h1&gt;
        &lt;div style="padding: 20px"&gt;
            {% trans %}Thank you!
            You will receive an email with your affiliate token
            as soon as your account will be activated.{% endtrans %}
        &lt;/div&gt;
    &lt;/div&gt;
{% endblock %}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Affiliate/affiliate_new.html.twig --&gt;
&lt;!-- ... --&gt;
    &lt;h1&gt;{% trans %}Become an affiliate{% endtrans %}&lt;/h1&gt;
&lt;!-- ... --&gt;</pre><p>每个翻译都是通过trans-unit标签来管理的，trans-unit有一个唯一的id属性。现在我们可以在文件中添加或者修改法语翻译了：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/translations/messages.fr.xlf --&gt;
&lt;?xml version="1.0"?&gt;
&lt;xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"&gt;
    &lt;file source-language="en" datatype="plaintext" original="file.ext"&gt;
        &lt;body&gt;
            &lt;trans-unit id="1"&gt;
                &lt;source&gt;Jobeet - Your best job board&lt;/source&gt;
                &lt;target&gt;Jobeet - Les meilleurs offres d'emplois&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="2"&gt;
                &lt;source&gt;Enter some keywords (city, country, position, ...)&lt;/source&gt;
                &lt;target&gt;Entre des mots cle (ville, pays, position, ...)&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="3"&gt;
                &lt;source&gt;Recent viewed jobs:&lt;/source&gt;
                &lt;target&gt;Dernier emplois vus:&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="4"&gt;
                &lt;source&gt;About Jobeet&lt;/source&gt;
                &lt;target&gt;Apropos de Jobeet&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="5"&gt;
                &lt;source&gt;Become an affiliate&lt;/source&gt;
                &lt;target&gt;Devenir un affilie&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="6"&gt;
                &lt;source&gt;and %count% more...&lt;/source&gt;
                &lt;target&gt;et %count%
                        autres...&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="7"&gt;
                &lt;source&gt;Language&lt;/source&gt;
                &lt;target&gt;Langue&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="8"&gt;
                &lt;source&gt;Publish&lt;/source&gt;
                &lt;target&gt;Publier&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="9"&gt;
                &lt;source&gt;Edit&lt;/source&gt;
                &lt;target&gt;Editer&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="10"&gt;
                &lt;source&gt;Are you sure?&lt;/source&gt;
                &lt;target&gt;Etes-vous sur?&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="11"&gt;
                &lt;source&gt;Delete&lt;/source&gt;
                &lt;target&gt;Supprimer&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="12"&gt;
                &lt;source&gt;Extend&lt;/source&gt;
                &lt;target&gt;Prolonger&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="13"&gt;
                &lt;source&gt;for another 30 days&lt;/source&gt;
                &lt;target&gt;pour 30 jours supplementaires&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="14"&gt;
                &lt;source&gt;Bookmark this %url% to manage this job in the future&lt;/source&gt;
                &lt;target&gt;Marquer cette %url% pour gerer ce travail a l'avenir&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="15"&gt;
                &lt;source&gt;Whether the job can also be published on affiliate websites or not.&lt;/source&gt;
                &lt;target&gt;Si le travail peut egalement etre publie sur les sites affilies ou non.&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="16"&gt;
                &lt;source&gt;%company% is looking for a %position%&lt;/source&gt;
                &lt;target&gt;%company% est a la recherche d'un %position%&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="17"&gt;
                &lt;source&gt;How to apply?&lt;/source&gt;
                &lt;target&gt;comment appliquer?&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="18"&gt;
                &lt;source&gt;posted on %date%&lt;/source&gt;
                &lt;target&gt;poste en %date%&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="19"&gt;
                &lt;source&gt;{0} No job in this category|{1} One job in this category|]1,Inf] %count% jobs in this category&lt;/source&gt;
                &lt;target&gt;{0}Aucune annonce dans cette categorie|{1}Une annonce dans cette categorie|]1,+Inf] %count% annonces dans cette categorie&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="20"&gt;
                &lt;source&gt;Jobs in the %category% category&lt;/source&gt;
                &lt;target&gt;Travails dans le %category% categorie&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="21"&gt;
                &lt;source&gt;Your affiliate account has been created&lt;/source&gt;
                &lt;target&gt;Votre compte d'affiliation a ete cree&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="22"&gt;
                &lt;source&gt;Thank you!
            You will receive an email with your affiliate token
            as soon as your account will be activated.&lt;/source&gt;
                &lt;target&gt;On te remercie! Vous recevrez un email avec votre jeton d'affiliation des que votre compte sera active.&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="23"&gt;
                &lt;source&gt;Expires in %count% days&lt;/source&gt;
                &lt;target&gt;Expire en %count% jours&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="24"&gt;
                &lt;source&gt;Ask for people&lt;/source&gt;
                &lt;target&gt;Recherche des gens&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="25"&gt;
                &lt;source&gt;Ask for a job&lt;/source&gt;
                &lt;target&gt;Recherche d'un emploi&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="26"&gt;
                &lt;source&gt;Jobeet API&lt;/source&gt;
                &lt;target&gt;API Jobeet&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="27"&gt;
                &lt;source&gt;Job creation&lt;/source&gt;
                &lt;target&gt;Creation d'emploi&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="28"&gt;
                &lt;source&gt;Job edit&lt;/source&gt;
                &lt;target&gt;Edit l'emploi&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="29"&gt;
                &lt;source&gt;Expired&lt;/source&gt;
                &lt;target&gt;Expiré&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="30"&gt;
                &lt;source&gt;Full feed&lt;/source&gt;
                &lt;target&gt;Fil RSS&lt;/target&gt;
            &lt;/trans-unit&gt;
            &lt;trans-unit id="31"&gt;
                &lt;source&gt;Post a Job&lt;/source&gt;
                &lt;target&gt;Poste un emploi &lt;/target&gt;
            &lt;/trans-unit&gt;
        &lt;/body&gt;
    &lt;/file&gt;
&lt;/xliff&gt;</pre><p>每次当你添加了新的翻译时，你都需要清除cache。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/09/09/symfony2-jobeet-day-19-internationalization-and-localization/">http://www.intelligentbee.com/blog/2013/09/09/symfony2-jobeet-day-19-internationalization-and-localization/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/392">jobeet第十九天:国际化和本地化</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/392/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第十八天:ajax</title>
		<link>http://www.newlifeclan.com/symfony/archives/390</link>
		<comments>http://www.newlifeclan.com/symfony/archives/390#comments</comments>
		<pubDate>Fri, 27 Mar 2015 09:13:06 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=390</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/390">jobeet第十八天:ajax</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>在昨天的教程中，我们使用了Zend Lucene库为Jobeet实现了一个功能强大的搜索引擎。而在今天的教程中，我们来提高搜索引擎的响应性，并且发挥Ajax的优点，把搜索变成实时响应的。现在我们需要给表单添加Javascript功能，但又不能往表单标签元素中嵌入Javascript，那么我们将会使用<a href="http://en.wikipedia.org/wiki/Unobtrusive_JavaScript">unobtrusive JavaScript</a>来实现这个功能。在客户端代码中，如HTML，CSS和Javascript，使用unobtrusive JavaScript能够更好的遵循代码分离的概念。</p>
<p><span id="more-390"></span></p>
<p>&nbsp;</p>
<h2>安装jQuery</h2>
<p>到<a href="http://jquery.com/">jQuery</a>官方网站下载最新版本的jQuery，然后把<em>.js</em>文件放在<em>src/Ibw/JobeetBundle/Resources/public/js/</em>目录下。</p>
<p>把<em>.js</em>文件放在<em>js</em>目录下之后，运行下面的命令安装到Symfony资源中：</p><pre class="crayon-plain-tag">php app/console assets:install web --symlink</pre><p>&nbsp;</p>
<h2>包含jQuery</h2>
<p>我们需要在所有的页面中使用到jQuery，因此我们需要修改layout文件：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;
    {% block javascripts %}
        &lt;script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/jquery-2.0.3.min.js') }}"&gt;&lt;/script&gt;
    {% endblock %}
&lt;!-- ... --&gt;</pre><p>&nbsp;</p>
<h2>添加行为</h2>
<p>一个实时的搜索功能就意味着每当用户在搜索框中输入字符时，系统就会触发一个到服务器的请求。服务器会返回请求的数据，并用这些数据更新页面中的一部分区域而不用重新刷新整个页面来加载这些请求数据。jQuery背后的原则是：等待页面加载完成之后才给DOM添加行为，而不是直接在HTML标签上添加on*()行为。如果你禁用了浏览器的Javascript支持，那么任何行为都不会被注册，尽管如此，表单还是能够和之前一样正常地工作。</p>
<p>我们的第一步是得到用户在输入框中输入的关键字：</p><pre class="crayon-plain-tag">$('#search_keywords').keyup(function(key)
{
    if (this.value.length &gt;= 3 || this.value == '')
    {
        // do something
    }
});</pre><p></p>
<blockquote><p>现在请不要把代码修改到文件中，因为我们还要对它进行大幅度的修改。在下面的教程中，我们会把最终的Javascript代码会在添加到layout文件里。</p></blockquote>
<p>每当用户输入关键字时，jQuery就会执行上面代码定义的异步函数（anonymous function），但只有用户输入的字符超过三个或者不输入任何字符时才会执行里面的逻辑。</p>
<p>一种简单实现Ajax请求的方式就是使用DOM元素上的load()方法：</p><pre class="crayon-plain-tag">$('#search_keywords').keyup(function(key)
{
    if (this.value.length &gt;= 3 || this.value == '')
    {
        $('#jobs').load(
            $(this).parents('form').attr('action'), { query: this.value + '*' }
        );
    }
});</pre><p>为了处理AJAX请求，我们将会在下面的部分中修改action。</p>
<p>最后但同样重要的是，如果Javascript可以使用，我们想要把search按钮给移除掉：</p><pre class="crayon-plain-tag">$('.search input[type="submit"]').hide();</pre><p>&nbsp;</p>
<h2>用户反馈</h2>
<p>在执行AJAX调用时，页面有时候并不会马上进行更新，浏览器会等待服务器把数据返还回来后才进行页面的更新。与此同时，我们需要做一些反馈给用户，告诉他们数据正在获取中。</p>
<p>在AJAX执行期间，给用户显示一个loading图标的用法已经成为了一种惯例。我们在layout中添加loader图片，并默认把它设成隐藏的：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;
    &lt;div class="search"&gt;
        &lt;h2&gt;Ask for a job&lt;/h2&gt;
        &lt;form action="{{ path('ibw_job_search') }}" method="get"&gt;
            &lt;input type="text" name="query" value="{{ app.request.get('query') }}" id="search_keywords" /&gt;
            &lt;input type="submit" value="search" /&gt;
            &lt;img id="loader" src="{{ asset('bundles/ibwjobeet/images/loader.gif') }}" style="vertical-align: middle; display: none" /&gt;
            &lt;div class="help"&gt;
                Enter some keywords (city, country, position, ...)
            &lt;/div&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;!-- ... --&gt;</pre><p>我们完成功能所需的HTML代码都已经有了，现在我们创建<em>search.js</em>文件，然后把之前我们解释过的代码添加到里面去：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Resources/public/js/search.js
$(document).ready(function()
{
    $('.search input[type="submit"]').hide();

    $('#search_keywords').keyup(function(key)
    {
        if(this.value.length &gt;= 3 || this.value == '') {
            $('#loader').show();
            $('#jobs').load(
                $(this).parent('form').attr('action'),
                { query: this.value ? this.value + '*' : this.value },
                function() {
                    $('#loader').hide();
                }
            );
        }
    });
});</pre><p>运行下面的代码安装资源：</p><pre class="crayon-plain-tag">php app/console assets:install web --symlink</pre><p>我们同样需要修改layout文件来包含search.js：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;
    {% block javascripts %}
        &lt;script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/jquery-2.0.3.min.js') }}"&gt;&lt;/script&gt;
        &lt;script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/search.js') }}"&gt;&lt;/script&gt;
    {% endblock %}
&lt;!-- ... --&gt;</pre><p>&nbsp;</p>
<h2>AJAX和Action</h2>
<p>如果Javascript未被禁用，jQuery就会得到用户输入的关键字，然后请求<em>searchAction()</em>。如果Javascript被禁用了，用户可以点击search按钮来请求<em>searchAction()</em>。</p>
<p>那么<em>searchAction()</em>需要判断请求是否是通过AJAX发起的。不管什么时候调用AJAX，request对象的<em>isXmlHttpRequest()</em>方法都返回true。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
use Symfony\Component\HttpFoundation\Response;

class JobController extends Controller
{  
    // ...

    public function searchAction(Request $request)
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();
        $query = $this-&gt;getRequest()-&gt;get('query');

        if(!$query) {
            if(!$request-&gt;isXmlHttpRequest()) {
                return $this-&gt;redirect($this-&gt;generateUrl('ibw_job'));
            } else {
                return new Response('No results.');
            }
        }

        $jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getForLuceneQuery($query);

        if($request-&gt;isXmlHttpRequest()) {

            return $this-&gt;render('IbwJobeetBundle:Job:list.html.twig', array('jobs' =&gt; $jobs));
        }

        return $this-&gt;render('IbwJobeetBundle:Job:search.html.twig', array('jobs' =&gt; $jobs));
    }
}</pre><p>如果搜索的结果不存在，我们需要在页面上显示出一条信息，而不是空白的页面。</p>
<p>我们来返回一个简单的字符串：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
public function searchAction(Request $request)
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();
    $query = $this-&gt;getRequest()-&gt;get('query');

    if(!$query) {
        if(!$request-&gt;isXmlHttpRequest()) {
            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job'));
        } else {
            return new Response('No results.');
        }
    }

    $jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getForLuceneQuery($query);

    if($request-&gt;isXmlHttpRequest()) {
        if('*' == $query || !$jobs || $query == '') {
            return new Response('No results.');
        }

        return $this-&gt;render('IbwJobeetBundle:Job:list.html.twig', array('jobs' =&gt; $jobs));
    }

    return $this-&gt;render('IbwJobeetBundle:Job:search.html.twig', array('jobs' =&gt; $jobs));
}</pre><p>&nbsp;</p>
<h2>测试AJAX</h2>
<p>因为Symfony不能够模拟AJAX请求，所以当测试AJAX调用时，我们需要给它一些帮助。这主要是意味着你需要手动在请求的header中添加X-Requested-With：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
class JobControllerTest extends WebTestCase
{
    // ...

    public function testSearch()
    {
        $client = static::createClient();

        $crawler = $client-&gt;request('GET', '/job/search');
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::searchAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $crawler = $client-&gt;request('GET', '/job/search?query=sens*', array(), array(), array(
            'X-Requested-With' =&gt; 'XMLHttpRequest',
        ));
        $this-&gt;assertTrue($crawler-&gt;filter('tr')-&gt;count()== 2);
    }
}</pre><p>在第十七天中，我们使用Zend Lucene库实现了搜索引擎。而今天我们使用jQuery让搜索引擎的响应性变得更好。Symfony提供了一些列基础工具，它使得构建一个MVC应用变得简单，而且它还能和其它第三方组件很好地集成在一起。总之，我们可以尝试着使用最佳的工具来服务我们的工作，提高工作效率。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/09/03/symfony2-jobeet-day-18-ajax/">http://www.intelligentbee.com/blog/2013/09/03/symfony2-jobeet-day-18-ajax/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/390">jobeet第十八天:ajax</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/390/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第十七天:搜索</title>
		<link>http://www.newlifeclan.com/symfony/archives/387</link>
		<comments>http://www.newlifeclan.com/symfony/archives/387#comments</comments>
		<pubDate>Fri, 27 Mar 2015 09:06:06 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=387</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/387">jobeet第十七天:搜索</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>在第十四天中，我们给Jobeet添加了订阅功能，用户能够实时地接收到最新发布的信息了。为了让Jobeet拥有更好的用户体验，今天我们就来给Jobeet添加新的功能：搜索引擎。</p>
<p><span id="more-387"></span></p>
<h2><a id="user-content-zend-lucene" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-17/chapter-17.md#zend-lucene"></a>Zend Lucene</h2>
<p>今天，我们就要给Jobeet添加搜索功能啦。对于Zend Framework，它提供了一个强大的库，叫做<a href="http://framework.zend.com/manual/en/zend.search.lucene.html">Zend Lucene</a>，它也是著名的Java Lucene项目的一部分。我们的任务不是要给Jobeet写一个搜索引擎，因为那实在是个复杂的任务，所以我们会直接使用Zend Lucene。</p>
<p>我们今天的教程不是讲怎么样使用Zend Lucene库，而是讲怎么样把Zend Lucene集成到Jobeet网站中，或者更广泛地说，是讲怎么样在Symfony中集成其它的第三方库。如果你想要学习更多关于Zend Lucene的相关技术，你可以查阅<a href="http://framework.zend.com/manual/en/zend.search.lucene.html">Zend Lucene</a>的文档。</p>
<h2><a id="user-content-安装和配置zend-framework" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-17/chapter-17.md#%E5%AE%89%E8%A3%85%E5%92%8C%E9%85%8D%E7%BD%AEzend-framework"></a>安装和配置Zend Framework</h2>
<p>Zend Lucene是Zend Framework的一部分。我们会把Zend Framework放在Symfony的<em>vendor/</em>目录下。首先，我们下载Zend Framework，然后把它解压出来，你会看到有<em>library/Zend/</em>目录，然后把它复制到Symfony项目中的<em>vendor/</em>目录下。需要注意的是，2.*版本的Zend Framework没有集成Lucene库，所以你不要去下载它们。</p>
<blockquote><p>下面的内容已经使用1.12.3版本的Zend Framework测试过了。</p></blockquote>
<p><img class="alignnone size-full wp-image-388" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/Day-17-zend.jpg" alt="Day-17-zend" width="413" height="956" /></p>
<p>你可以删除一些文件来清理一下目录，但请不要删除下面这几个文件和目录：</p>
<ul class="task-list">
<li>Exception.php</li>
<li>Loader/</li>
<li>Loader.php</li>
<li>Search/</li>
</ul>
<p>然后在<em>autoload.php</em>文件中添加下面的代码，目的是为了提供一种简单的方式来注册<em>Zend autoloader</em>。</p><pre class="crayon-plain-tag">// app/autoload.php
// ...

set_include_path(__DIR__.'/../vendor'.PATH_SEPARATOR.get_include_path());
require_once __DIR__.'/../vendor/Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

return $loader;</pre><p>&nbsp;</p>
<h2>索引</h2>
<p>Jobeet搜索引擎要能够返回和用户输入关键字相匹配的信息。在实现搜索功能之前，我们需要为<em>Job</em>建立索引。对于Jobeet项目来说，我们会把索引文件放在<em>/web/data/</em>目录下，这个目录我们待会就来创建。</p>
<p>Zend Lucene提供了两个方法来检索索引，使用哪个方法则取决于索引是否已存在。现在，我们给<em>Job</em>实体创建一个助手方法，这个方法能够返回一个已存的索引或者是返回一个新建的索引给我们：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

class Job
{
    // ...

    static public function getLuceneIndex()
    {
        if (file_exists($index = self::getLuceneIndexFile())) {
            return \Zend_Search_Lucene::open($index);
        }

        return \Zend_Search_Lucene::create($index);
    }

    static public function getLuceneIndexFile()
    {
        return __DIR__.'/../../../../web/data/job.index';
    }
}</pre><p>一个job被创建或者被更新时，它的索引也必须被更新。修改ORM文件，使得当一个job被序列化到数据库中时，同时也更新它的索引：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# ...
    # ...
    lifecycleCallbacks:
        # ...
        postPersist: [ upload, updateLuceneIndex ]
        postUpdate: [ upload, updateLuceneIndex ]
        # ...</pre><p>现在运行<code>generate:entities</code>命令，它会在<em>Job</em>类中生成<em>updateLuceneIndex()</em>方法：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>然后修改<em>updateLuceneIndex()</em>方法来处理实际的操作：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
class Job
{
    // ...

    public function updateLuceneIndex()
    {
        $index = self::getLuceneIndex();

        // remove existing entries
        foreach ($index-&gt;find('pk:'.$this-&gt;getId()) as $hit)
        {
          $index-&gt;delete($hit-&gt;id);
        }

        // don't index expired and non-activated jobs
        if ($this-&gt;isExpired() || !$this-&gt;getIsActivated())
        {
          return;
        }

        $doc = new \Zend_Search_Lucene_Document();

        // store job primary key to identify it in the search results
        $doc-&gt;addField(\Zend_Search_Lucene_Field::Keyword('pk', $this-&gt;getId()));

        // index job fields
        $doc-&gt;addField(\Zend_Search_Lucene_Field::UnStored('position', $this-&gt;getPosition(), 'utf-8'));
        $doc-&gt;addField(\Zend_Search_Lucene_Field::UnStored('company', $this-&gt;getCompany(), 'utf-8'));
        $doc-&gt;addField(\Zend_Search_Lucene_Field::UnStored('location', $this-&gt;getLocation(), 'utf-8'));
        $doc-&gt;addField(\Zend_Search_Lucene_Field::UnStored('description', $this-&gt;getDescription(), 'utf-8'));

        // add job to the index
        $index-&gt;addDocument($doc);
        $index-&gt;commit();
    }
}</pre><p>因为Zend Lucene不能够更新已存在的索引，所以我们需要先删除已存在的索引，然后再添加新的索引。</p>
<p>索引job的过程很简单：保存主键是为以后搜索job信息提供了参考，其他的主列（main columns）（position，company，location和description）同样能够被索引，但是它们没有被保存在索引文件中，因为我们会使用真实的对象来显示这些主列（看上面的代码）。我们同样需要创建<em>deleteLuceneIndex()</em>方法来移除已经被删除的job数据的索引。就像我们之前的更新操作一样，我们来添加删除操作。我们在ORM文件的<em>postRemove</em>部分添加<em>deleteLuceneIndex()</em>方法：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# ...
    # ...
    lifecycleCallbacks:
        # ...
        postRemove: [ removeUpload, deleteLuceneIndex ]</pre><p>再次运行命令来生成实体：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>现在修改<em>Job.php</em>，实现<em>deleteLuceneIndex()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
class Job
{
    // ...

    public function deleteLuceneIndex()
    {
        $index = self::getLuceneIndex();

        foreach ($index-&gt;find('pk:'.$this-&gt;getId()) as $hit) {
            $index-&gt;delete($hit-&gt;id);
        }
    }
}</pre><p>不管是通过命令行修改索引文件还是通过web修改索引文件，你都必须修改索引目录的权限，因此这取决于你的配置：</p><pre class="crayon-plain-tag">chmod -R 777 web/data</pre><p>现在我们可以重新加载fixture数据了，这样fixture数据就能被索引了：</p><pre class="crayon-plain-tag">php app/console doctrine:fixtures:load</pre><p>&nbsp;</p>
<h2>搜索</h2>
<p>实现搜索简直就是小菜一碟嘛。首先，我们创建路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/job.yml
# ...

ibw_job_search:
    pattern: /search
    defaults: { _controller: "IbwJobeetBundle:Job:search" }</pre><p>添加action：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
namespace Ibw\JobeetBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ibw\JobeetBundle\Entity\Job;
use Ibw\JobeetBundle\Form\JobType;

class JobController extends Controller
{
    // ...

    public function searchAction(Request $request)
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();
        $query = $this-&gt;getRequest()-&gt;get('query');

        if(!$query) {
            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job'));
        }

        $jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getForLuceneQuery($query);

        return $this-&gt;render('IbwJobeetBundle:Job:search.html.twig', array('jobs' =&gt; $jobs));
    }
}</pre><p>在<em>searchAction()</em>中，如果请求的<em>query</em>不存在或者为空的话，那么用户就会转向到<em>JobController::indexAction()</em>方法。</p>
<p>视图模板也是简单明了的：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/search.html.twig --&gt;
{% extends 'IbwJobeetBundle::layout.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/jobs.css') }}" type="text/css" media="all" /&gt;
{% endblock %}

{% block content %}
    &lt;div id="jobs"&gt;
        {% include 'IbwJobeetBundle:Job:list.html.twig' with {'jobs': jobs} %}
    &lt;/div&gt;
{% endblock %}</pre><p>执行搜索的逻辑在<em>getForLuceneQuery()</em>方法中：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
namespace Ibw\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Ibw\JobeetBundle\Entity\Job;

class JobRepository extends EntityRepository
{
    // ...

    public function getForLuceneQuery($query)
    {
        $hits = Job::getLuceneIndex()-&gt;find($query);

        $pks = array();
        foreach ($hits as $hit)
        {
          $pks[] = $hit-&gt;pk;
        }

        if (empty($pks))
        {
          return array();
        }

        $q = $this-&gt;createQueryBuilder('j')
            -&gt;where('j.id IN (:pks)')
            -&gt;setParameter('pks', $pks)
            -&gt;andWhere('j.is_activated = :active')
            -&gt;setParameter('active', 1)
            -&gt;setMaxResults(20)
            -&gt;getQuery();

        return $q-&gt;getResult();
    }
}</pre><p>我们从Lucene索引中得到结果之后，我们从中过滤掉未激活的job，然后限制结果条数为20。</p>
<p>为了让它能够工作，我们需要修改layout：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;
    &lt;!-- ... --&gt;
        &lt;div class="search"&gt;
            &lt;h2&gt;Ask for a job&lt;/h2&gt;
            &lt;form action="{{ path('ibw_job_search') }}" method="get"&gt;
                &lt;input type="text" name="query" value="{{ app.request.get('query') }}" id="search_keywords" /&gt;
                &lt;input type="submit" value="search" /&gt;
                &lt;div class="help"&gt;
                    Enter some keywords (city, country, position, ...)
                &lt;/div&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    &lt;!-- ... --&gt;
&lt;!-- ... --&gt;</pre><p>&nbsp;</p>
<h2>单元测试</h2>
<p>我们需要为搜索引擎创建哪种类型的单元测试呢？很明显，我们当然不会去测试Zend Lucene库，而是测试和Zend Lucene集成的<em>Job</em>类。</p>
<p>在<em>JobRepositoryTest.php</em>文件末尾添加下面的测试：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepositoryTest.php
// ... 
use Ibw\JobeetBundle\Entity\Job;

class JobRepositoryTest extends WebTestCase
{
    // ...

    public function testGetForLuceneQuery()
    {
        $em = static::$kernel-&gt;getContainer()
            -&gt;get('doctrine')
            -&gt;getManager();

        $job = new Job();
        $job-&gt;setType('part-time');
        $job-&gt;setCompany('Sensio');
        $job-&gt;setPosition('FOO6');
        $job-&gt;setLocation('Paris');
        $job-&gt;setDescription('WebDevelopment');
        $job-&gt;setHowToApply('Send resumee');
        $job-&gt;setEmail('jobeet@example.com');
        $job-&gt;setUrl('http://sensio-labs.com');
        $job-&gt;setIsActivated(false);

        $em-&gt;persist($job);
        $em-&gt;flush();

        $jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getForLuceneQuery('FOO6');
        $this-&gt;assertEquals(count($jobs), 0);

        $job = new Job();
        $job-&gt;setType('part-time');
        $job-&gt;setCompany('Sensio');
        $job-&gt;setPosition('FOO7');
        $job-&gt;setLocation('Paris');
        $job-&gt;setDescription('WebDevelopment');
        $job-&gt;setHowToApply('Send resumee');
        $job-&gt;setEmail('jobeet@example.com');
        $job-&gt;setUrl('http://sensio-labs.com');
        $job-&gt;setIsActivated(true);

        $em-&gt;persist($job);
        $em-&gt;flush();

        $jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getForLuceneQuery('position:FOO7');
        $this-&gt;assertEquals(count($jobs), 1);
        foreach ($jobs as $job_rep) {
            $this-&gt;assertEquals($job_rep-&gt;getId(), $job-&gt;getId());
        }

        $em-&gt;remove($job);
        $em-&gt;flush();

        $jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getForLuceneQuery('position:FOO7');

        $this-&gt;assertEquals(count($jobs), 0);
    }
}</pre><p>我们测试未被激活的job或者已被删除的job都不应该出现在搜索结果中。我们也测试了应该出现指定条件的搜索结果。</p>
<h2><a id="user-content-任务" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-17/chapter-17.md#%E4%BB%BB%E5%8A%A1"></a>任务</h2>
<p>最后，我们需要更新<em>JobeetCleanup</em>任务来清除无用（stale）实体的索引，这样做可以优化索引：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Command/JobeetCleanupCommand.php
// ...
use  Ibw\JobeetBundle\Entity\Job;

class JobeetCleanupCommand extends ContainerAwareCommand
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $days = $input-&gt;getArgument('days');

        $em = $this-&gt;getContainer()-&gt;get('doctrine')-&gt;getManager();

        // cleanup Lucene index
        $index = Job::getLuceneIndex();

        $q = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;createQueryBuilder('j')
          -&gt;where('j.expires_at &lt; :date')
          -&gt;setParameter('date',date('Y-m-d'))
          -&gt;getQuery();

        $jobs = $q-&gt;getResult();
        foreach ($jobs as $job)
        {
          if ($hit = $index-&gt;find('pk:'.$job-&gt;getId()))
          {
            $index-&gt;delete($hit-&gt;id);
          }
        }

        $index-&gt;optimize();

        $output-&gt;writeln('Cleaned up and optimized the job index');

        // Remove stale jobs
        $nb = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;cleanup($days);

        $output-&gt;writeln(sprintf('Removed %d stale jobs', $nb));
    }
}</pre><p>这个任务移除了所有过期的job数据的索引，然后使用了Zend Lucene内建的<em>optimize()</em>方法对索引进行了优化。</p>
<p>在这一天中，我们实现了一个功能齐全的搜索引擎，而且这个过程还用不到一个小时就完成了。每当你想要往项目里添加新功能的时候，请你先去看看是否已经有现成的解决方案可以使用。</p>
<p>明天我们将会使用Javascript来提高搜索引擎的响应性，搜索结果会随着用户在搜索框中输入的关键字的变化而进行实时的更新。当然，我们也会讲解怎么样在Symfony中使用Ajax。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/29/symfony2-jobeet-day-17-search/">http://www.intelligentbee.com/blog/2013/08/29/symfony2-jobeet-day-17-search/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/387">jobeet第十七天:搜索</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/387/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第十六天:邮件</title>
		<link>http://www.newlifeclan.com/symfony/archives/385</link>
		<comments>http://www.newlifeclan.com/symfony/archives/385#comments</comments>
		<pubDate>Fri, 27 Mar 2015 08:56:02 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=385</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/385">jobeet第十六天:邮件</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>昨天我们为Jobeet添加一个只可读的（read-only）web service。现在用户可以申请<em>Affiliate</em>账户了，但是他们需要被管理员激活后才能够使用。为了<em>affiliate</em>能给拿到他们的<em>token</em>，管理员需要发送邮件来通知他们。这些就是我们今天要实现的功能。</p>
<p><span id="more-385"></span></p>
<h2><a id="user-content-swift-mailer" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-16/chapter-16.md#swift-mailer"></a>Swift Mailer</h2>
<p>Symfony框架捆绑了最好的PHP邮件解决方案：<a href="http://www.swiftmailer.org/">Swift Mailer</a>。当然，Symfony已经完全地集成了这个库，并且在它原有的功能上添加了一些很酷很好用的功能。我们开始工作吧，我们通过发送邮件给用户通知他们已被管理员激活了，并且告诉他们对应的<em>token</em>是多少。但在那之前，我们需要配置我们的环境：</p><pre class="crayon-plain-tag"># app/config/parameters.yml
# ...
    # ...
    mailer_transport:  gmail
    mailer_host:       ~
    mailer_user:       address@example.com
    mailer_password:   your_password
    # ...</pre><p></p>
<blockquote><p>为了让上面的配置能够起作用，你应该把<em>mailer_user</em>和<em>mailer_password</em>修改成真实的值。</p></blockquote>
<p>对<em>app/config/parameters_test.yml</em>文件做同样的修改。</p>
<p>修改完这两个文件之后清除<em>test</em>环境和<em>development</em>环境下的缓存：</p><pre class="crayon-plain-tag">php app/console cache:clear --env=dev
php app/console cache:clear --env=prod</pre><p>因为我们把<em>mailer_transport</em>设置为gmail，所以你需要使用google的邮箱地址来作为<em>mailer_user</em>的值。</p>
<p>我们可以想想，平时发送一个邮件的时候，我们都会在邮箱客户端进行哪几步操作呢？我们会填写邮件主题，填写接收人，填写要发送的信息内容。</p>
<p>为了发送邮件，我们需要：</p>
<ul class="task-list">
<li>调用<em>Swift_message</em>的<em>newInstance()</em>方法（通过<a href="http://www.swiftmailer.org/docs">Swift Mailer的官方文档</a>可以学习到这个对象）</li>
<li>通过<em>setFrom()</em>方法来设置发送者的地址</li>
<li>通过<em>setSubject()</em>方法来设置邮件的主题</li>
<li>通过<em>setTo()</em>，<em>setCc()</em>，<em>setBcc()</em>之一来设置接收者</li>
<li>通过<em>setBody()</em>方法设置邮件内容</li>
</ul>
<p>用下面的代码替换<em>activateAction()</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateAdminController.php
// ...

    public function activateAction($id)
    {
       if($this-&gt;admin-&gt;isGranted('EDIT') === false) {
            throw new AccessDeniedException();
        }

        $em = $this-&gt;getDoctrine()-&gt;getManager();
        $affiliate = $em-&gt;getRepository('IbwJobeetBundle:Affiliate')-&gt;findOneById($id);

        try {
            $affiliate-&gt;setIsActive(true);
            $em-&gt;flush();

            $message = \Swift_Message::newInstance()
                -&gt;setSubject('Jobeet affiliate token')
                -&gt;setFrom('address@example.com')
                -&gt;setTo($affiliate-&gt;getEmail())
                -&gt;setBody(
                    $this-&gt;renderView('IbwJobeetBundle:Affiliate:email.txt.twig', array('affiliate' =&gt; $affiliate-&gt;getToken())))
            ;

            $this-&gt;get('mailer')-&gt;send($message);
        } catch(\Exception $e) {
            $this-&gt;get('session')-&gt;setFlash('sonata_flash_error', $e-&gt;getMessage());
        }

        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
    }

// ...</pre><p>发送邮件很简单，我们只需给<em>mailer</em>实例的<em>send()</em>方法传递一个<em>message</em>对象作为参数即可。</p>
<p>对于邮件的内容，我们创建了一个<em>email.txt.twig</em>文件，它准确地包含了我们想通知给<em>affilate</em>的内容。</p><pre class="crayon-plain-tag">Your affiliate account has been activated.
Your secret token is {{affiliate}}.
You can see the jobs list at the following addresses:
http://jobeet.local/app_dev.php/api/{{affiliate}}/jobs.xml
or http://jobeet.local/app_dev.php/api/{{affiliate}}/jobs.json
or http://jobeet.local/app_dev.php/api/{{affiliate}}/jobs.yaml</pre><p>现在我们也为<em>batchActionActivate</em>添加发送邮件的功能，尽管一次性同时选择多个<em>affiliate</em>账户同时激活，他们也能够接收到邮件：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateAdminController.php
// ... 

    public function batchActionActivate(ProxyQueryInterface $selectedModelQuery)
    {
        // ...

        try {
            foreach($selectedModels as $selectedModel) {
                $selectedModel-&gt;activate();
                $modelManager-&gt;update($selectedModel);

                $message = \Swift_Message::newInstance()
                    -&gt;setSubject('Jobeet affiliate token')
                    -&gt;setFrom('address@example.com')
                    -&gt;setTo($selectedModel-&gt;getEmail())
                    -&gt;setBody(
                        $this-&gt;renderView('IbwJobeetBundle:Affiliate:email.txt.twig', array('affiliate' =&gt; $selectedModel-&gt;getToken())))
                ;

                $this-&gt;get('mailer')-&gt;send($message);
            }
        } catch(\Exception $e) {
            $this-&gt;get('session')-&gt;setFlash('sonata_flash_error', $e-&gt;getMessage());

            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
        }

        // ...
    }

// ...</pre><p>&nbsp;</p>
<h2>测试</h2>
<p>我们已经看到了怎么样使用Symfony来发送邮件，现在我们来写一些功能测试以确保它们能正确地工作。</p>
<p>为了测试新功能，我们需要进行登录。为了登录，我们需要用户名和密码。我们来创建先的fixture文件，在这个文件中添加admin用户：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/DataFixtures/ORM/LoadUserData.php
namespace Ibw\JobeetBundle\DataFixtures\ORM;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Ibw\JobeetBundle\Entity\User;

class LoadUserData implements FixtureInterface, OrderedFixtureInterface, ContainerAwareInterface
{
    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * {@inheritDoc}
     */
    public function setContainer(ContainerInterface $container = null)
    {
        $this-&gt;container = $container;
    }

    /**
     * @param \Doctrine\Common\Persistence\ObjectManager $em
     */
    public function load(ObjectManager $em)
    {
        $user = new User();
        $user-&gt;setUsername('admin');
        $encoder = $this-&gt;container
            -&gt;get('security.encoder_factory')
            -&gt;getEncoder($user)
        ;

        $encodedPassword = $encoder-&gt;encodePassword('admin', $user-&gt;getSalt());
        $user-&gt;setPassword($encodedPassword);

        $em-&gt;persist($user);
        $em-&gt;flush();
    }

    public function getOrder()
    {
        return 4; // the order in which fixtures will be loaded
    }
}</pre><p>在测试中，我们会使用分析器（profiler）中为<em>swiftmailer</em>的<em>collector</em>来得到上一个请求中的邮件发送信息。现在我们来添加一些代码检查邮件是否正确发送：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/AffiliateAdminControllerTest.php
namespace Ibw\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\ArrayInput;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;

class AffiliateAdminControllerTest extends WebTestCase
{
    private $em;
    private $application;

    public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel-&gt;boot();

        $this-&gt;application = new Application(static::$kernel);

        // drop the database
        $command = new DropDatabaseDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:database:drop',
            '--force' =&gt; true
        ));
        $command-&gt;run($input, new NullOutput());

        // we have to close the connection after dropping the database so we don't get "No database selected" error
        $connection = $this-&gt;application-&gt;getKernel()-&gt;getContainer()-&gt;get('doctrine')-&gt;getConnection();
        if ($connection-&gt;isConnected()) {
            $connection-&gt;close();
        }

        // create the database
        $command = new CreateDatabaseDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:database:create',
        ));
        $command-&gt;run($input, new NullOutput());

        // create schema
        $command = new CreateSchemaDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:schema:create',
        ));
        $command-&gt;run($input, new NullOutput());

        // get the Entity Manager
        $this-&gt;em = static::$kernel-&gt;getContainer()
            -&gt;get('doctrine')
            -&gt;getManager();

        // load fixtures
        $client = static::createClient();
        $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client-&gt;getContainer());
        $loader-&gt;loadFromDirectory(static::$kernel-&gt;locateResource('@IbwJobeetBundle/DataFixtures/ORM'));
        $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this-&gt;em);
        $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this-&gt;em, $purger);
        $executor-&gt;execute($loader-&gt;getFixtures());
    }

    public function testActivate()
    {
        $client = static::createClient();

        // Enable the profiler for the next request (it does nothing if the profiler is not available)
        $client-&gt;enableProfiler();
        $crawler = $client-&gt;request('GET', '/login');

        $form = $crawler-&gt;selectButton('login')-&gt;form(array(
            '_username'      =&gt; 'admin',
            '_password'      =&gt; 'admin'
        ));

        $crawler = $client-&gt;submit($form);
        $crawler = $client-&gt;followRedirect();

        $this-&gt;assertTrue(200 === $client-&gt;getResponse()-&gt;getStatusCode());

        $crawler = $client-&gt;request('GET', '/admin/ibw/jobeet/affiliate/list');

        $link = $crawler-&gt;filter('.btn.edit_link')-&gt;link();
        $client-&gt;click($link);

        $mailCollector = $client-&gt;getProfile()-&gt;getCollector('swiftmailer');

        // Check that an e-mail was sent
        $this-&gt;assertEquals(1, $mailCollector-&gt;getMessageCount());

        $collectedMessages = $mailCollector-&gt;getMessages();
        $message = $collectedMessages[0];

        // Asserting e-mail data
        $this-&gt;assertInstanceOf('Swift_Message', $message);
        $this-&gt;assertEquals('Jobeet affiliate token', $message-&gt;getSubject());
        $this-&gt;assertRegExp(
            '/Your secret token is symfony/',
            $message-&gt;getBody()
        );
    }
}</pre><p>如果你现在运行测试将会得到错误。为了防止错误发生，我们需要确保<em>config_test.yml</em>文件中的<em>profiler</em>在测试环境里是开启的。如果它被设成fasle，请设成true：</p><pre class="crayon-plain-tag"># app/config/config_test.yml
# ...

framework:
    test: ~
    session:
        storage_id: session.storage.mock_file
    profiler:
        enabled: true

# ...</pre><p>清除缓存后运行测试命令，哈哈，好好享受<em>green bar</em>吧：</p><pre class="crayon-plain-tag">phpunit -c app src/Ibw/JobeetBundle/Tests/Controller/AffiliateAdminControllerTest</pre><p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/24/symfony2-jobeet-day-16-the-mailer/">http://www.intelligentbee.com/blog/2013/08/24/symfony2-jobeet-day-16-the-mailer/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/385">jobeet第十六天:邮件</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/385/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>jobeet第十五天:Web Services</title>
		<link>http://www.newlifeclan.com/symfony/archives/382</link>
		<comments>http://www.newlifeclan.com/symfony/archives/382#comments</comments>
		<pubDate>Fri, 27 Mar 2015 08:50:36 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[web services]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=382</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/382">jobeet第十五天:Web Services</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>在昨天的内容中，我们给<em>Jobeet</em>加上了订阅功能之后，用户就可以实时地接收到最新发布的信息了。</p>
<p>现在我们试着站在发布者的角度来思考，当发布者发布一个<em>Job</em>信息之后，发布者想要让尽可能多的人能够了解到这条信息。如果我们能把这些信息放在很多小网站上，这样一来看的人越多，那么发布者就将有更大几率能够找到适合这个工作的人选了。这就是所谓的<a href="http://en.wikipedia.org/wiki/The_Long_Tail">长尾效应（long tail）</a>。<em>Affiliates</em>能够在他们的网站上发布最新的<em>Job</em>信息，这些都需要<em>web services</em>的支持，那么我们今天就来实现<em>web services</em>。</p>
<p><span id="more-382"></span></p>
<h2>Affiliates</h2>
<p>就像我们在第二天中内容说的那样，一个<em>Affiliate</em>能够得到所有当前已激活的<em>Job</em>列表。</p>
<h3>The fixtures</h3>
<p>我们为<em>Affiliates</em>创建一个新的<em>Fixture</em>文件：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/DataFixtures/ORM/LoadAffiliateData.php
namespace Ibw\JobeetBundle\DataFixtures\ORM;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Ibw\JobeetBundle\Entity\Affiliate;

class LoadAffiliateData extends AbstractFixture implements OrderedFixtureInterface
{
    public function load(ObjectManager $em)
    {
        $affiliate = new Affiliate();

        $affiliate-&gt;setUrl('http://sensio-labs.com/');
        $affiliate-&gt;setEmail('address1@example.com');
        $affiliate-&gt;setToken('sensio-labs');
        $affiliate-&gt;setIsActive(true);
        $affiliate-&gt;addCategorie($em-&gt;merge($this-&gt;getReference('category-programming')));

        $em-&gt;persist($affiliate);

        $affiliate = new Affiliate();

        $affiliate-&gt;setUrl('/');
        $affiliate-&gt;setEmail('address2@example.org');
        $affiliate-&gt;setToken('symfony');
        $affiliate-&gt;setIsActive(false);
        $affiliate-&gt;addCategorie($em-&gt;merge($this-&gt;getReference('category-programming')), $em-&gt;merge($this-&gt;getReference('category-design')));

        $em-&gt;persist($affiliate);
        $em-&gt;flush();

        $this-&gt;addReference('affiliate', $affiliate);
    }

    public function getOrder()
    {
        return 3; // This represents the order in which fixtures will be loaded
    }
}</pre><p>现在运行下面的命令，我们把定义在<em>Fixture</em>文件中的数据持久化到数据库中：</p><pre class="crayon-plain-tag">php app/console doctrine:fixtures:load</pre><p>在<em>Fixture</em>文件中，我们可以看到<em>token</em>是硬编码写上去的，这里的目的是为了方便测试。但当在实际的运行时，当用户需要申请为<em>Affiliate</em>时，这个<em>token</em>将会被自动生成。我们在<em>Affiliate</em>类中创建一个方法来生成<em>token</em>。我们先在<em>ORM</em>文件中的<em>lifecycleCallbacks</em>部分添加<em>setTokenValue</em>方法：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Affiliate.orm.yml
# ... 
    lifecycleCallbacks:
        prePersist: [ setCreatedAtValue, setTokenValue ]</pre><p>运行下面的命令会后，<em>Affiliate</em>类中将会生成<em>setTokenValue()</em>方法：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>现在我们来修改这个方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Affiliate.php
public function setTokenValue()
{
    if(!$this-&gt;getToken()) {
        $token = sha1($this-&gt;getEmail().rand(11111, 99999));
        $this-&gt;token = $token;
    }

    return $this;
}</pre><p>重新加载数据：</p><pre class="crayon-plain-tag">php app/console doctrine:fixtures:load</pre><p>&nbsp;</p>
<h2>The Job Web Service</h2>
<p>和以前一样，我们每次创建新资源的的时候，第一件要做的事情就是定义路由，这是个好习惯：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing.yml
IbwJobeetBundle_api:
    pattern: /api/{token}/jobs.{_format}
    defaults: {_controller: "IbwJobeetBundle:Api:list"}
    requirements:
        _format: xml|json|yaml</pre><p>通常我们修改过路由文件之后，我们都需要去清除<em>cache</em>：</p><pre class="crayon-plain-tag">php app/console cache:clear --env=dev
php app/console cache:clear --env=prod</pre><p>我们的下一步是创建<em>api</em>动作和相应的模板，它们会共享相同的动作。我们新建一个控制器文件，并把它命名为“<em>ApiController</em>”：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/ApiController.php
namespace Ibw\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Ibw\JobeetBundle\Entity\Affiliate;
use Ibw\JobeetBundle\Entity\Job;
use Ibw\JobeetBundle\Repository\AffiliateRepository;

class ApiController extends Controller
{
    public function listAction(Request $request, $token)
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $jobs = array();

        $rep = $em-&gt;getRepository('IbwJobeetBundle:Affiliate');
        $affiliate = $rep-&gt;getForToken($token);

        if(!$affiliate) { 
            throw $this-&gt;createNotFoundException('This affiliate account does not exist!');
        }

        $rep = $em-&gt;getRepository('IbwJobeetBundle:Job');
        $active_jobs = $rep-&gt;getActiveJobs(null, null, null, $affiliate-&gt;getId());

        foreach ($active_jobs as $job) {
            $jobs[$this-&gt;get('router')-&gt;generate('ibw_job_show', array('company' =&gt; $job-&gt;getCompanySlug(), 'location' =&gt; $job-&gt;getLocationSlug(), 'id' =&gt; $job-&gt;getId(), 'position' =&gt; $job-&gt;getPositionSlug()), true)] = $job-&gt;asArray($request-&gt;getHost());
        }

        $format = $request-&gt;getRequestFormat();
        $jsonData = json_encode($jobs);

        if ($format == "json") {
            $headers = array('Content-Type' =&gt; 'application/json'); 
            $response = new Response($jsonData, 200, $headers);

            return $response;
        }

        return $this-&gt;render('IbwJobeetBundle:Api:jobs.' . $format . '.twig', array('jobs' =&gt; $jobs));  
    }
}</pre><p>为了能够通过<em>token</em>获得<em>Affiliate</em>的信息，我们需要创建<em>getForToken()</em>方法。这个方法同样会验证<em>Affiliate</em>是否是已激活的，所以我们这里就不需要再判断<em>Affiliate</em>是否已被激活。到现在为止我们还尚未使用过<em>AffiliateRepository</em>，因为它还不存在。我们现在就来创建它，先修改<em>ORM</em>文件：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Affiliate.orm.yml
Ibw\JobeetBundle\Entity\Affiliate:
    type: entity
    repositoryClass: Ibw\JobeetBundle\Repository\AffiliateRepository
    # ...</pre><p>运行下面的代码：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>创建完毕后，我们马上就可以使用它了：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/AffiliateRepository.php
namespace Ibw\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * AffiliateRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class AffiliateRepository extends EntityRepository
{
    public function getForToken($token)
    {
        $qb = $this-&gt;createQueryBuilder('a')
            -&gt;where('a.is_active = :active')
            -&gt;setParameter('active', 1)
            -&gt;andWhere('a.token = :token')
            -&gt;setParameter('token', $token)
            -&gt;setMaxResults(1)
        ;

        try{
            $affiliate = $qb-&gt;getQuery()-&gt;getSingleResult();
        } catch(\Doctrine\Orm\NoResultException $e){
            $affiliate = null;
        }

        return $affiliate;
    }
}</pre><p>当通过<em>token</em>取出对应的<em>Affiliate</em>之后，我们会调用<em>getActiveJobs()</em>方法返回属于某个分类的所有的<em>Job</em>信息给<em>Affiliate</em>。如果你打开<em>JobRepository.php</em>文件，可以看到<em>getActiveJobs()</em>方法没有提供任何的参数选项给<em>Affiliate</em>使用。为了能够重用这个方法，我们需要对它做一些修改：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
// ...

    public function getActiveJobs($category_id = null, $max = null, $offset = null, $affiliate_id = null)
    {
        $qb = $this-&gt;createQueryBuilder('j')
            -&gt;where('j.expires_at &gt; :date')
            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
            -&gt;andWhere('j.is_activated = :activated')
            -&gt;setParameter('activated', 1)
            -&gt;orderBy('j.expires_at', 'DESC');

        if($max) {
            $qb-&gt;setMaxResults($max);
        }

        if($offset) {
            $qb-&gt;setFirstResult($offset);
        }

        if($category_id) {
            $qb-&gt;andWhere('j.category = :category_id')
                -&gt;setParameter('category_id', $category_id);
        }
        // j.category c, c.affiliate a
        if($affiliate_id) {
            $qb-&gt;leftJoin('j.category', 'c')
               -&gt;leftJoin('c.affiliates', 'a')
               -&gt;andWhere('a.id = :affiliate_id')
               -&gt;setParameter('affiliate_id', $affiliate_id)
            ;
        }

        $query = $qb-&gt;getQuery();

        return $query-&gt;getResult();
    }

// ...</pre><p>就如你所看到的，我们调用了<em>asArray()</em>函数来填充<em>jobs</em>数组。我们来定义它：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
public function asArray($host)
{
    return array(
        'category'     =&gt; $this-&gt;getCategory()-&gt;getName(),
        'type'         =&gt; $this-&gt;getType(),
        'company'      =&gt; $this-&gt;getCompany(),
        'logo'         =&gt; $this-&gt;getLogo() ? 'http://' . $host . '/uploads/jobs/' . $this-&gt;getLogo() : null,
        'url'          =&gt; $this-&gt;getUrl(),
        'position'     =&gt; $this-&gt;getPosition(),
        'location'     =&gt; $this-&gt;getLocation(),
        'description'  =&gt; $this-&gt;getDescription(),
        'how_to_apply' =&gt; $this-&gt;getHowToApply(),
        'expires_at'   =&gt; $this-&gt;getCreatedAt()-&gt;format('Y-m-d H:i:s'),
    );
}</pre><p>&nbsp;</p>
<h3><em>XML</em>格式</h3>
<p>支持<em>XML</em>格式很简单，只需要创建一个模板即可：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Api/Jobs.xml.twig --&gt;
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;jobs&gt;
{% for url, job in jobs %}
    &lt;job url="{{ url }}"&gt;
{% for key,value in job %}
        &lt;{{ key }}&gt;{{ value }}&lt;/{{ key }}&gt;
{% endfor %}
    &lt;/job&gt;
{% endfor %}
&lt;/jobs&gt;</pre><p>&nbsp;</p>
<h3><em>JSON</em>格式</h3>
<p>支持<em>JSON</em>格式也是类似的：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Resources/views/Api/jobs.json.twig
{% for url, job in jobs %}
{% i = 0, count(jobs), ++i %}
[
    "url":"{{ url }}",
{% for key, value in job %} {% j = 0, count(key), ++j %}
    "{{ key }}":"{% if j == count(key)%} {{ json_encode(value) }}, {% else %} {{ json_encode(value) }}
                 {% endif %}"
{% endfor %}]
{% endfor %}</pre><p>&nbsp;</p>
<h3><em>YAML</em>格式</h3>
<p></p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/views/Api/jobs.yaml.twig
{% for url,job in jobs %}
    Url: {{ url }}
{% for key, value in job %}
        {{ key }}: {{ value }}
{% endfor %}
{% endfor %}</pre><p>如果你想通过一个无效的<em>token</em>来调用<em>web service</em>，那么不论你请求的是什么格式的内容，你都将会得到一个<strong>404</strong>响应。如果你想看到我们努力的成果，你可以试试访问下面的链接：http://jobeet.local/app_dev.php/api/sensio-labs/jobs.xml或者是http://jobeet.local/app_dev.php/api/symfony/jobs.xml。你可以修改<em>URL</em>中的扩展名来得到指定格式的内容。</p>
<h2></h2>
<h2>测试<em>Web Service</em></h2>
<p></p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/ApiControllerTest.php
namespace Ibw\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\ArrayInput;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\HttpExceptionInterface;

class ApiControllerTest extends WebTestCase
{
    private $em;

    private $application;

    public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel-&gt;boot();

        $this-&gt;application = new Application(static::$kernel);

        // drop the database
        $command = new DropDatabaseDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:database:drop',
            '--force' =&gt; true
        ));
        $command-&gt;run($input, new NullOutput());

        // we have to close the connection after dropping the database so we don't get "No database selected" error
        $connection = $this-&gt;application-&gt;getKernel()-&gt;getContainer()-&gt;get('doctrine')-&gt;getConnection();
        if ($connection-&gt;isConnected()) {
            $connection-&gt;close();
        }

        // create the database
        $command = new CreateDatabaseDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:database:create',
        ));
        $command-&gt;run($input, new NullOutput());

        // create schema
        $command = new CreateSchemaDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:schema:create',
        ));
        $command-&gt;run($input, new NullOutput());

        // get the Entity Manager
        $this-&gt;em = static::$kernel-&gt;getContainer()
            -&gt;get('doctrine')
            -&gt;getManager();

        // load fixtures
        $client = static::createClient();
        $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client-&gt;getContainer());
        $loader-&gt;loadFromDirectory(static::$kernel-&gt;locateResource('@IbwJobeetBundle/DataFixtures/ORM'));
        $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this-&gt;em);
        $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this-&gt;em, $purger);
        $executor-&gt;execute($loader-&gt;getFixtures());
    }

    public function testList()
    {
        $client = static::createClient();
        $crawler = $client-&gt;request('GET', '/api/sensio-labs/jobs.xml');

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\ApiController::listAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
        $this-&gt;assertTrue($crawler-&gt;filter('description')-&gt;count() == 32);

        $crawler = $client-&gt;request('GET', '/api/sensio-labs87/jobs.xml');

        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());

        $crawler = $client-&gt;request('GET', '/api/symfony/jobs.xml');

        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());

        $crawler = $client-&gt;request('GET', '/api/sensio-labs/jobs.json');

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\ApiController::listAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
        $this-&gt;assertRegExp('/"category"\:"Programming"/', $client-&gt;getResponse()-&gt;getContent());

        $crawler = $client-&gt;request('GET', '/api/sensio-labs87/jobs.json');

        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());

        $crawler = $client-&gt;request('GET', '/api/sensio-labs/jobs.yaml');
        $this-&gt;assertRegExp('/category\: Programming/', $client-&gt;getResponse()-&gt;getContent());

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\ApiController::listAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $crawler = $client-&gt;request('GET', '/api/sensio-labs87/jobs.yaml');

        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());
    }
}</pre><p>&nbsp;</p>
<h2><em>Affiliate</em>申请表单</h2>
<p><em>web service</em>已经可以使用了，现在我们来添加创建<em>Affiliate</em>的表单吧。为了实现这个功能，我们需要写HTML表单，为每个表单域实现验证规则，把表单域的值处理后保存到数据库中，当表单数据有错误时还需要显示出错误信息反馈给用户。</p>
<p>首先我们先创建控制器文件，把它命名为<em>AffiliateCotrolelr</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/AffiliateController.php
namespace Ibw\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ibw\JobeetBundle\Entity\Affiliate;
use Ibw\JobeetBundle\Form\AffiliateType;
use Symfony\Component\HttpFoundation\Request;
use Ibw\JobeetBundle\Entity\Category;

class AffiliateController extends Controller
{
    // Your code goes here
}</pre><p>然后修改<em>layout.html.twig</em>中的链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;
    &lt;li class="last"&gt;&lt;a href="{{ path('ibw_affiliate_new') }}"&gt;Become an affiliate&lt;/a&gt;&lt;/li&gt;
&lt;!-- ... --&gt;</pre><p>现在创建一个<em>action</em>来匹配刚才修改链接的路由：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateController.php
namespace Ibw\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Ibw\JobeetBundle\Entity\Affiliate;
use Ibw\JobeetBundle\Form\AffiliateType;
use Symfony\Component\HttpFoundation\Request;
use Ibw\JobeetBundle\Entity\Category;

class AffiliateController extends Controller
{
    public function newAction()
    {
        $entity = new Affiliate();
        $form = $this-&gt;createForm(new AffiliateType(), $entity);

        return $this-&gt;render('IbwJobeetBundle:Affiliate:affiliate_new.html.twig', array(
            'entity' =&gt; $entity,
            'form'   =&gt; $form-&gt;createView(),
        ));
    }
}</pre><p>我们已经有了路由的名字还有<em>action</em>，但我们还没有路由。所以我们来创建它：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/affiliate.yml
ibw_affiliate_new:
    pattern:  /new
    defaults: { _controller: "IbwJobeetBundle:Affiliate:new" }</pre><p>同样需要在<em>routing.yml</em>文件中加入下面的代码：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing.yml
# ...

IbwJobeetBundle_ibw_affiliate:
    resource: "@IbwJobeetBundle/Resources/config/routing/affiliate.yml"
    prefix:   /affiliate</pre><p>表单类同样需要被创建出来。尽管<em>Affiliate</em>有很多的字段域，但我们没有必要把它们全部显示出来，因为有些字段域是不需要用来填写的。我们来创建<em>Affiliate</em>表单：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Form/AffiliateType.php
namespace Ibw\JobeetBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Ibw\JobeetBundle\Entity\Affiliate;
use Ibw\JobeetBundle\Entity\Category;

class AffiliateType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            -&gt;add('url')
            -&gt;add('email')
            -&gt;add('categories', null, array('expanded'=&gt;true))
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver-&gt;setDefaults(array(
            'data_class' =&gt; 'Ibw\JobeetBundle\Entity\Affiliate',
        ));
    }

    public function getName()
    {
        return 'affiliate';
    }
}</pre><p>现在我们需要验证提交上来的<em>Affiliate</em>表单对象中的数据是否有效。我们在<em>validation.yml</em>文件中添加下面的代码：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/validation.yml
# ...

Ibw\JobeetBundle\Entity\Affiliate:
    constraints:
        - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: email
    properties:
        url:
            - Url: ~
        email:
            - NotBlank: ~
            - Email: ~</pre><p>在上面的代码中，我们使用了一个新的验证器，叫做<em>UniqueEntity</em>。它能够验证<em>Doctrine</em>实体对象中的一个或者多个特殊字段域是否是唯一的。这个十分常用，例如，我们需要防止一个新注册用户的<em>email</em>地址和已存在用户的<em>email</em>地址重复。</p>
<p>添加了验证约束之后不要忘记清除<em>cache</em>！</p>
<p>最后一步需要创建表单试图：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Affiliate/affiliate_new.html.twig --&gt;
{% extends 'IbwJobeetBundle::layout.html.twig' %}

{% set form_themes = _self %}

{% block form_errors %}
{% spaceless %}
    {% if errors|length &gt; 0 %}
        &lt;ul class="error_list"&gt;
            {% for error in errors %}
                &lt;li&gt;{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}&lt;/li&gt;
            {% endfor %}
        &lt;/ul&gt;
    {% endif %}
{% endspaceless %}
{% endblock form_errors %}

{% block stylesheets %}
    {{ parent() }}
    &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/job.css') }}" type="text/css" media="all" /&gt;
{% endblock %}

{% block content %}
    &lt;h1&gt;Become an affiliate&lt;/h1&gt;
        &lt;form action="{{ path('ibw_affiliate_create') }}" method="post" {{ form_enctype(form) }}&gt;
            &lt;table id="job_form"&gt;
                &lt;tfoot&gt;
                    &lt;tr&gt;
                        &lt;td colspan="2"&gt;
                            &lt;input type="submit" value="Submit" /&gt;
                        &lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tfoot&gt;
                &lt;tbody&gt;
                    &lt;tr&gt;
                        &lt;th&gt;{{ form_label(form.url) }}&lt;/th&gt;
                        &lt;td&gt;
                            {{ form_errors(form.url) }}
                            {{ form_widget(form.url) }}
                        &lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;th&gt;{{ form_label(form.email) }}&lt;/th&gt;
                        &lt;td&gt;
                            {{ form_errors(form.email) }}
                            {{ form_widget(form.email) }}
                        &lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;th&gt;{{ form_label(form.categories) }}&lt;/th&gt;
                        &lt;td&gt;
                            {{ form_errors(form.categories) }}
                            {{ form_widget(form.categories) }}
                        &lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tbody&gt;
            &lt;/table&gt;
        {{ form_end(form) }}
{% endblock %}</pre><p>当提交的表单数据如果是有效的，那么表单数据就会被保存到数据库中。我们为<em>AffiliateController</em>添加<em>create</em>动作：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateController.php
class AffiliateController extends Controller 
{
    // ...    

    public function createAction(Request $request)
    {
        $affiliate = new Affiliate();
        $form = $this-&gt;createForm(new AffiliateType(), $affiliate);
        $form-&gt;bind($request);
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        if ($form-&gt;isValid()) {

            $formData = $request-&gt;get('affiliate');
            $affiliate-&gt;setUrl($formData['url']);
            $affiliate-&gt;setEmail($formData['email']);
            $affiliate-&gt;setIsActive(false);

            $em-&gt;persist($affiliate);
            $em-&gt;flush();

            return $this-&gt;redirect($this-&gt;generateUrl('ibw_affiliate_wait'));
        }

        return $this-&gt;render('IbwJobeetBundle:Affiliate:affiliate_new.html.twig', array(
            'entity' =&gt; $affiliate,
            'form'   =&gt; $form-&gt;createView(),
        ));
    }
}</pre><p>当表单一旦被提交，那么<em>createAction()</em>就会被执行，所以我们需要定义路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/affiliate.yml
# ...

ibw_affiliate_create:
    pattern: /create
    defaults: { _controller: "IbwJobeetBundle:Affiliate:create" }
    requirements: { _method: post }</pre><p><em>Affiliate</em>注册之后，他将会被重定向到等待页面。我们来为定义这个动作或试图吧：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateController.php
class AffiliateController extends Controller
{
    // ...

    public function waitAction()
    {
        return $this-&gt;render('IbwJobeetBundle:Affiliate:wait.html.twig');
    }
}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Affiliate/wait.html.twig --&gt;
{% extends "IbwJobeetBundle::layout.html.twig" %}

{% block content %}
    &lt;div class="content"&gt;
        &lt;h1&gt;Your affiliate account has been created&lt;/h1&gt;
        &lt;div style="padding: 20px"&gt;
            Thank you!
            You will receive an email with your affiliate token
            as soon as your account will be activated.
        &lt;/div&gt;
    &lt;/div&gt;
{% endblock %}</pre><p>现在添加路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/affiliate.yml
# ...

ibw_affiliate_wait:
    pattern: /wait
    defaults: { _controller: "IbwJobeetBundle:Affiliate:wait" }</pre><p>定义好路由之后，我们需要清除<em>cache</em>。</p>
<p>现在你可以试着点击首页中的<em>Affiliates</em>链接，你会跳转到<em>Affiliate</em>表单页面。</p>
<h3><a id="user-content-测试" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-15/chapter-15.md#%E6%B5%8B%E8%AF%95"></a>测试</h3>
<p>最后一步是为新功能添加功能测试：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/AffiliateControllerTest.php
namespace Ibw\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\ArrayInput;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
use Symfony\Component\DomCrawler\Crawler;

class AffiliateControllerTest extends WebTestCase
{
    private $em;
    private $application;

    public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel-&gt;boot();

        $this-&gt;application = new Application(static::$kernel);

        // drop the database
        $command = new DropDatabaseDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:database:drop',
            '--force' =&gt; true
        ));
        $command-&gt;run($input, new NullOutput());

        // we have to close the connection after dropping the database so we don't get "No database selected" error
        $connection = $this-&gt;application-&gt;getKernel()-&gt;getContainer()-&gt;get('doctrine')-&gt;getConnection();
        if ($connection-&gt;isConnected()) {
            $connection-&gt;close();
        }

        // create the database
        $command = new CreateDatabaseDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:database:create',
        ));
        $command-&gt;run($input, new NullOutput());

        // create schema
        $command = new CreateSchemaDoctrineCommand();
        $this-&gt;application-&gt;add($command);
        $input = new ArrayInput(array(
            'command' =&gt; 'doctrine:schema:create',
        ));
        $command-&gt;run($input, new NullOutput());

        // get the Entity Manager
        $this-&gt;em = static::$kernel-&gt;getContainer()
            -&gt;get('doctrine')
            -&gt;getManager();

        // load fixtures
        $client = static::createClient();
        $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client-&gt;getContainer());
        $loader-&gt;loadFromDirectory(static::$kernel-&gt;locateResource('@IbwJobeetBundle/DataFixtures/ORM'));
        $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this-&gt;em);
        $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this-&gt;em, $purger);
        $executor-&gt;execute($loader-&gt;getFixtures());
    }

    public function testAffiliateForm()
    {
        $client = static::createClient();
        $crawler = $client-&gt;request('GET', '/affiliate/new');

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::newAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(
            'affiliate[url]' =&gt; 'http://sensio-labs.com/',
            'affiliate[email]' =&gt; 'jobeet@example.com'
        ));

        $client-&gt;submit($form);
        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::createAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        $kernel = static::createKernel();
        $kernel-&gt;boot();
        $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

        $query = $em-&gt;createQuery('SELECT count(a.email) FROM IbwJobeetBundle:Affiliate a WHERE a.email = :email');
        $query-&gt;setParameter('email', 'jobeet@example.com');
        $this-&gt;assertEquals(1, $query-&gt;getSingleScalarResult());

        $crawler = $client-&gt;request('GET', '/affiliate/new');
        $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(
            'affiliate[email]'        =&gt; 'not.an.email',
        ));
        $crawler = $client-&gt;submit($form);

        // check if we have 1 errors
        $this-&gt;assertTrue($crawler-&gt;filter('.error_list')-&gt;count() == 1);
        // check if we have error on affiliate_email field
        $this-&gt;assertTrue($crawler-&gt;filter('#affiliate_email')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);
    }

    public function testCreate()
    {
        $client = static::createClient();
        $crawler = $client-&gt;request('GET', '/affiliate/new');
        $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(
            'affiliate[url]' =&gt; 'http://sensio-labs.com/',
            'affiliate[email]' =&gt; 'address@example.com'
        ));

        $client-&gt;submit($form);
        $client-&gt;followRedirect();

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::waitAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

        return $client;
    }

    public function testWait()
    {
        $client = static::createClient();
        $crawler = $client-&gt;request('GET', '/affiliate/wait');

        $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::waitAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
    }
}</pre><p>&nbsp;</p>
<h2><em>Affiliate</em>后台管理</h2>
<p>对于后台，我们会使用<em>SonataAdminBundle</em>。就像我们之前说过的那样，<em>Affiliate</em>注册之后需要等待<em>admin</em>来激活他。所以，当<em>admin</em>访问<em>Affiliate</em>页面后，他只能看到激活或者不激活这两个操作，这样有利于提高工作效率嘛。</p>
<p>首先，你需要在<em>services.yml</em>中声明<em>Affiliate</em>服务：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/service.yml
# ...
    ibw.jobeet.admin.affiliate:
        class: Ibw\JobeetBundle\Admin\AffiliateAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Affiliates }
        arguments:
            - ~
            - Ibw\JobeetBundle\Entity\Affiliate
            - 'IbwJobeetBundle:AffiliateAdmin'</pre><p>然后创建<em>Admin</em>文件：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php
namespace Ibw\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Ibw\JobeetBundle\Entity\Affiliate;

class AffiliateAdmin extends Admin
{
    protected $datagridValues = array(
        '_sort_order' =&gt; 'ASC',
        '_sort_by' =&gt; 'is_active'
    );

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            -&gt;add('email')
            -&gt;add('url')
        ;
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            -&gt;add('email')
            -&gt;add('is_active');
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            -&gt;add('is_active')
            -&gt;addIdentifier('email')
            -&gt;add('url')
            -&gt;add('created_at')
            -&gt;add('token')
        ;
    }
}</pre><p>为辅助管理员工作，我们想要只显示出未激活的<em>Affiliate</em>。我们能够通过过滤<em>is_active</em>值为<em>false</em>的<em>Affiliate</em>来实现这个功能：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php
// ...
    protected $datagridValues = array(
        '_sort_order' =&gt; 'ASC',
        '_sort_by' =&gt; 'is_active',
        'is_active' =&gt; array('value' =&gt; 2) // The value 2 represents that the displayed affiliate accounts are not activated yet
    );

// ...</pre><p>现在我们创建<em>AffiliateAdminController</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateAdminController.php
namespace Ibw\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

class AffiliateAdminController extends Controller
{
    // Your code goes here
}</pre><p>我们来创建<em>activate</em>和<em>deactivate</em>批量操作：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateAdminController.php
namespace Ibw\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

class AffiliateAdminController extends Controller
{
    public function batchActionActivate(ProxyQueryInterface $selectedModelQuery)
    {
        if($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {
            throw new AccessDeniedException();
        }

        $request = $this-&gt;get('request');
        $modelManager = $this-&gt;admin-&gt;getModelManager();

        $selectedModels = $selectedModelQuery-&gt;execute();

        try {
            foreach($selectedModels as $selectedModel) {
                $selectedModel-&gt;activate();
                $modelManager-&gt;update($selectedModel);
            }
        } catch(\Exception $e) {
            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());

            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
        }

        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('The selected accounts have been activated'));

        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
    }

    public function batchActionDeactivate(ProxyQueryInterface $selectedModelQuery)
    {
        if($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {
            throw new AccessDeniedException();
        }

        $request = $this-&gt;get('request');
        $modelManager = $this-&gt;admin-&gt;getModelManager();

        $selectedModels = $selectedModelQuery-&gt;execute();

        try {
            foreach($selectedModels as $selectedModel) {
                $selectedModel-&gt;deactivate();
                $modelManager-&gt;update($selectedModel);
            }
        } catch(\Exception $e) {
            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());

            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
        }

        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('The selected accounts have been deactivated'));

        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
    }
}</pre><p>为了能够让批量操作生效，我们把它添加到<em>Admin::getBatchActions()</em>方法中：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php
class AffiliateAdmin extends Admin
{
    // ... 

    public function getBatchActions()
    {
        $actions = parent::getBatchActions();

        if($this-&gt;hasRoute('edit') &amp;&amp; $this-&gt;isGranted('EDIT') &amp;&amp; $this-&gt;hasRoute('delete') &amp;&amp; $this-&gt;isGranted('DELETE')) {
            $actions['activate'] = array(
                'label'            =&gt; 'Activate',
                'ask_confirmation' =&gt; true
            );

            $actions['deactivate'] = array(
                'label'            =&gt; 'Deactivate',
                'ask_confirmation' =&gt; true
            );
        }

        return $actions;
    }
}</pre><p>在此我们还需要给<em>Affiliate</em>实体添加两个方法：<em>activate()</em>和<em>deactivate()</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Affiliate.php
// ...

    public function activate()
    {
        if(!$this-&gt;getIsActive()) {
            $this-&gt;setIsActive(true);
        }

        return $this-&gt;is_active;
    }

    public function deactivate()
    {
        if($this-&gt;getIsActive()) {
            $this-&gt;setIsActive(false);
        }

        return $this-&gt;is_active;
    }</pre><p>现在我们为每条<em>Affiliate</em>信息都创建两个独立的动作，<em>activate</em>和<em>deactivate</em>。首先我们先为它们创建路由，这就是为什么我们的<em>Admin</em>类中重写了<em>configureRoutes</em>函数：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;

class AffiliateAdmin extends Admin
{
    // ...

    protected function configureRoutes(RouteCollection $collection) {
        parent::configureRoutes($collection);

        $collection-&gt;add('activate',
            $this-&gt;getRouterIdParameter().'/activate')
        ;

        $collection-&gt;add('deactivate',
            $this-&gt;getRouterIdParameter().'/deactivate')
        ;
    }
}</pre><p>现在我们在<em>AdminController</em>中实现这两个动作：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/AffiliateAdminController.php
class AffiliateAdminController extends Controller
{
    // ...

    public function activateAction($id)
    {
        if($this-&gt;admin-&gt;isGranted('EDIT') === false) {
            throw new AccessDeniedException();
        }

        $em = $this-&gt;getDoctrine()-&gt;getManager();
        $affiliate = $em-&gt;getRepository('IbwJobeetBundle:Affiliate')-&gt;findOneById($id);

        try {
            $affiliate-&gt;setIsActive(true);
            $em-&gt;flush();
        } catch(\Exception $e) {
            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());

            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list', $this-&gt;admin-&gt;getFilterParameters()));
        }

        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));

    }

    public function deactivateAction($id)
    {
        if($this-&gt;admin-&gt;isGranted('EDIT') === false) {
            throw new AccessDeniedException();
        }

        $em = $this-&gt;getDoctrine()-&gt;getManager();
        $affiliate = $em-&gt;getRepository('IbwJobeetBundle:Affiliate')-&gt;findOneById($id);

        try {
            $affiliate-&gt;setIsActive(false);
            $em-&gt;flush();
        } catch(\Exception $e) {
            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());

            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list', $this-&gt;admin-&gt;getFilterParameters()));
        }

        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
    }
}</pre><p>现在为新添加的<em>action</em>创建模板：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/AffiliateAdmin/list__action_activate.html.twig --&gt;
{% if admin.isGranted('EDIT', object) and admin.hasRoute('activate') %}
    &lt;a href="{{ admin.generateObjectUrl('activate', object) }}" class="btn edit_link" title="{{ 'action_activate'|trans({}, 'SonataAdminBundle') }}"&gt;
        &lt;i class="icon-edit"&gt;&lt;/i&gt;
        {{ 'activate'|trans({}, 'SonataAdminBundle') }}
    &lt;/a&gt;
{% endif %}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/AffiliateAdmin/list__action_deactivate.html.twig --&gt;
{% if admin.isGranted('EDIT', object) and admin.hasRoute('deactivate') %}
    &lt;a href="{{ admin.generateObjectUrl('deactivate', object) }}" class="btn edit_link" title="{{ 'action_deactivate'|trans({}, 'SonataAdminBundle') }}"&gt;
        &lt;i class="icon-edit"&gt;&lt;/i&gt;
        {{ 'deactivate'|trans({}, 'SonataAdminBundle') }}
    &lt;/a&gt;
{% endif %}</pre><p>在<em>AffiliateAdmin::configureListFields()</em>方法中添加新的action和button后，我们就能在页面上看到每条<em>Affiliate</em>信息都把它们显示出来：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php
class AffiliateAdmin extends Admin
{
    // ...    

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            -&gt;add('is_active')
            -&gt;addIdentifier('email')
            -&gt;add('url')
            -&gt;add('created_at')
            -&gt;add('token')
            -&gt;add('_action', 'actions', array( 'actions' =&gt; array('activate' =&gt; array('template' =&gt; 'IbwJobeetBundle:AffiliateAdmin:list__action_activate.html.twig'),
                'deactivate' =&gt; array('template' =&gt; 'IbwJobeetBundle:AffiliateAdmin:list__action_deactivate.html.twig'))))
        ;
    }
    /// ...
}</pre><p>好的，马上清除掉<em>cache</em>来试试吧！</p>
<p>今天我们就到这了，明天我们会讲解发送邮件，当<em>Affiliate</em>被激活后将收到一封邮件。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/21/symfony2-jobeet-day-15-web-services/">http://www.intelligentbee.com/blog/2013/08/21/symfony2-jobeet-day-15-web-services/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/382">jobeet第十五天:Web Services</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/382/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>jobeet第十四天:订阅</title>
		<link>http://www.newlifeclan.com/symfony/archives/380</link>
		<comments>http://www.newlifeclan.com/symfony/archives/380#comments</comments>
		<pubDate>Fri, 27 Mar 2015 08:27:31 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[订阅]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=380</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/380">jobeet第十四天:订阅</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>如果你正在寻找工作，那么你可能十分想要第一时间就能了解到最新发布的<em>Job</em>信息。我们总不可能每时每刻都守在电脑面前，不断地刷新网站是否有发布新的<em>Job</em>信息吧，那样很不方便，所以我们会为<em>Jobeet</em>用户提供订阅（feeds）功能，这样用户就能实时地接受到最新发布的<em>Job</em>信息了。</p>
<p><span id="more-380"></span></p>
<h2>模板格式</h2>
<p>模板是能把内容渲染成任意一种格式的通用方式，但我们在大多数情况下都是使用模板来渲染HTML内容。当然，模板也很容易生成Javascript，CSS，XML或者其它任意类型格式的内容。</p>
<p>我们来举个例子，比如说我们需要把相同的“资源”按照不同的格式渲染出来。为了把一个文章的索引页渲染成XML格式，我们只需简单地把把格式名包含进模板名中即可：</p>
<ul class="task-list">
<li>XML模板名：AcmeArticleBundle:Article:index.xml.twig</li>
<li>XML模板文件名：index.xml.twig</li>
</ul>
<p>事实上，这仅仅只是一个命名约定，模板会基于它原本的格式，而并不会再被渲染成其它的格式了。.</p>
<p>在多数情况下，你可能希望只使用单个控制器来渲染用户期望得到的指定格式的内容。出于这个原因，我们通常的做法会是这样的：</p><pre class="crayon-plain-tag">public function indexAction()
{
    $format = $this-&gt;getRequest()-&gt;getRequestFormat();

    return $this-&gt;render('AcmeBlogBundle:Blog:index.'.$format.'.twig');
}</pre><p><em>Request</em>对象的<em>getRequestFormat()</em>方法默认返回的值是<em>html</em>，但它能够基于用户请求的格式返回任意类型的格式。请求格式通常由路由进行管理，路由是可以配置的，所以<em>/contact</em>的请求格式是html（因为默认是html格式），而<em>/contact.xml</em>的请求格式是xml。</p>
<p>为了创建带有格式参数的链接，我们需要在参数列表中加入<em>_format</em>键：</p><pre class="crayon-plain-tag">&lt;a href="{{ path('article_show', {'id': 123, '_format': 'pdf'}) }}"&gt;
    PDF Version
&lt;/a&gt;</pre><p>&nbsp;</p>
<h2>订阅</h2>
<h3><a id="user-content-订阅最新的job信息" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-14/chapter-14.md#%E8%AE%A2%E9%98%85%E6%9C%80%E6%96%B0%E7%9A%84job%E4%BF%A1%E6%81%AF"></a>订阅最新的<em>Job</em>信息</h3>
<p>支持不同格式的内容就像创建模板一样简单。为了给最新发布的<em>Job</em>信息创建一个<em>Atom</em>格式的订阅，我们需要创建一个<em>index.atom.twig</em>模板：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/index.atom.twig --&gt;
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;feed xmlns="http://www.w3.org/2005/Atom"&gt;
    &lt;title&gt;Jobeet&lt;/title&gt;
    &lt;subtitle&gt;Latest Jobs&lt;/subtitle&gt;
    &lt;link href="" rel="self"/&gt;
    &lt;link href=""/&gt;
    &lt;updated&gt;&lt;/updated&gt;
    &lt;author&gt;&lt;name&gt;Jobeet&lt;/name&gt;&lt;/author&gt;
    &lt;id&gt;Unique Id&lt;/id&gt;

    &lt;entry&gt;
        &lt;title&gt;Job title&lt;/title&gt;
        &lt;link href="" /&gt;
        &lt;id&gt;Unique id&lt;/id&gt;
        &lt;updated&gt;&lt;/updated&gt;
        &lt;summary&gt;Job description&lt;/summary&gt;
        &lt;author&gt;&lt;name&gt;Company&lt;/name&gt;&lt;/author&gt;
    &lt;/entry&gt;
&lt;/feed&gt;</pre><p>在<em>Jobeet</em>页面的页脚部分更新订阅的链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;

&lt;li class="feed"&gt;&lt;a href="{{ path('ibw_job', {'_format': 'atom'}) }}"&gt;Full feed&lt;/a&gt;&lt;/li&gt;

&lt;!-- ... --&gt;</pre><p>在<em>layout</em>的标签部分添加一个标签，这让浏览器能够自动察觉到我们的订阅链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;

&lt;link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="{{ url('ibw_job', {'_format': 'atom'}) }}" /&gt;

&lt;!-- ... --&gt;</pre><p>修改<em>JobController::indexAction()</em>方法能够按照<em>_format</em>参数来渲染模板：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

$format = $this-&gt;getRequest()-&gt;getRequestFormat();

return $this-&gt;render('IbwJobeetBundle:Job:index.'.$format.'.twig', array(
    'categories' =&gt; $categories
));

// ...</pre><p>把<em>Atom</em>模板的头部分替换成下面的代码：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/index.atom.twig --&gt;
&lt;!-- ... --&gt;

    &lt;title&gt;Jobeet&lt;/title&gt;
    &lt;subtitle&gt;Latest Jobs&lt;/subtitle&gt;
    &lt;link href="{{ url('ibw_job', {'_format': 'atom'}) }}" rel="self"/&gt;
    &lt;link href="{{ url('ibw_jobeet_homepage') }}"/&gt;
    &lt;updated&gt;{{ lastUpdated }}&lt;/updated&gt;
    &lt;author&gt;&lt;name&gt;Jobeet&lt;/name&gt;&lt;/author&gt;
    &lt;id&gt;{{ feedId }}&lt;/id&gt;

&lt;!-- ... --&gt;</pre><p>在<em>JobController::indexAction()</em>方法中，我们需要把<em>lastUpdated</em>和<em>feedId</em>传递给模板：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

        $latestJob = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getLatestPost();

        if($latestJob) {
            $lastUpdated = $latestJob-&gt;getCreatedAt()-&gt;format(DATE_ATOM);
        } else {
            $lastUpdated = new \DateTime();
            $lastUpdated = $lastUpdated-&gt;format(DATE_ATOM);
        }

        $format = $this-&gt;getRequest()-&gt;getRequestFormat();
        return $this-&gt;render('IbwJobeetBundle:Job:index.'.$format.'.twig', array(
               'categories' =&gt; $categories,
               'lastUpdated' =&gt; $lastUpdated,
               'feedId' =&gt; sha1($this-&gt;get('router')-&gt;generate('ibw_job', array('_format'=&gt; 'atom'), true)),
        ));
// ...</pre><p>为了得到最新发布信息的日期，我们需要在<em>JobRepository</em>类中添加<em>getLatestPost()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
// ...

    public function getLatestPost($category_id = null)
    {
        $query = $this-&gt;createQueryBuilder('j')
            -&gt;where('j.expires_at &gt; :date')
            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
            -&gt;andWhere('j.is_activated = :activated')
            -&gt;setParameter('activated', 1)
            -&gt;orderBy('j.expires_at', 'DESC')
            -&gt;setMaxResults(1);

        if($category_id) {
            $query-&gt;andWhere('j.category = :category_id')
                -&gt;setParameter('category_id', $category_id);
        }

        try{
            $job = $query-&gt;getQuery()-&gt;getSingleResult();
        } catch(\Doctrine\Orm\NoResultException $e){
            $job = null;
        }

        return $job;    
    }
// ...</pre><p>订阅中的<em>entity</em>可以通过下面的代码生成：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/index.atom.twig --&gt;
{% for category in categories %}
    {% for entity in category.activejobs %}
        &lt;entry&gt;
            &lt;title&gt;{{ entity.position }} ({{ entity.location }})&lt;/title&gt;
            &lt;link href="{{ url('ibw_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}" /&gt;
            &lt;id&gt;{{ entity.id }}&lt;/id&gt;
            &lt;updated&gt;{{ entity.createdAt.format(constant('DATE_ATOM')) }}&lt;/updated&gt;
            &lt;summary type="xhtml"&gt;
                &lt;div xmlns="http://www.w3.org/1999/xhtml"&gt;
                    {% if entity.logo %}
                        &lt;div&gt;
                            &lt;a href="{{ entity.url }}"&gt;
                                &lt;img src="http://{{ app.request.host }}/uploads/jobs/{{ entity.logo }}" alt="{{ entity.company }} logo" /&gt;
                            &lt;/a&gt;
                        &lt;/div&gt;
                    {% endif %}
                    &lt;div&gt;
                        {{ entity.description|nl2br }}
                    &lt;/div&gt;
                    &lt;h4&gt;How to apply?&lt;/h4&gt;
                    &lt;p&gt;{{ entity.howtoapply }}&lt;/p&gt;
                &lt;/div&gt;
            &lt;/summary&gt;
            &lt;author&gt;&lt;name&gt;{{ entity.company }}&lt;/name&gt;&lt;/author&gt;
        &lt;/entry&gt;
    {% endfor %}
{% endfor %}</pre><p>&nbsp;</p>
<h3>订阅分类中最新的<em>Job</em>信息</h3>
<p><em>Jobeet</em>的目标之一就是帮助人们找到期望的工作，所以我们需要提供不同类型的<em>Job</em>信息订阅。</p>
<p>首先我们来更新模板中分类订阅的链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/index.html.twig --&gt;
&lt;div class="feed"&gt;
    &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}"&gt;Feed&lt;/a&gt;
&lt;/div&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Category/show.html.twig --&gt;
&lt;div class="feed"&gt;
    &lt;a href="{{ path('IbwJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}"&gt;Feed&lt;/a&gt;
&lt;/div&gt;</pre><p>修改<em>CategoryController::showAction()</em>方法渲染适当的模板：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/CategoryController.php
// ...
    public function showAction($slug, $page)
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $category = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;findOneBySlug($slug);

        if (!$category) {
            throw $this-&gt;createNotFoundException('Unable to find Category entity.');
        }

        $latestJob = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getLatestPost($category-&gt;getId());

        if($latestJob) {
            $lastUpdated = $latestJob-&gt;getCreatedAt()-&gt;format(DATE_ATOM); 
        } else {
            $lastUpdated = new \DateTime();
            $lastUpdated = $lastUpdated-&gt;format(DATE_ATOM);
        }

        $total_jobs = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;countActiveJobs($category-&gt;getId());
        $jobs_per_page = $this-&gt;container-&gt;getParameter('max_jobs_on_category');
        $last_page = ceil($total_jobs / $jobs_per_page);
        $previous_page = $page &gt; 1 ? $page - 1 : 1;
        $next_page = $page &lt; $last_page ? $page + 1 : $last_page; 
        $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId(), $jobs_per_page, ($page - 1) * $jobs_per_page));

        $format = $this-&gt;getRequest()-&gt;getRequestFormat();

        return $this-&gt;render('IbwJobeetBundle:Category:show.' . $format . '.twig', array(
            'category' =&gt; $category,
            'last_page' =&gt; $last_page,
            'previous_page' =&gt; $previous_page,
            'current_page' =&gt; $page,
            'next_page' =&gt; $next_page,
            'total_jobs' =&gt; $total_jobs,
            'feedId' =&gt; sha1($this-&gt;get('router')-&gt;generate('IbwJobeetBundle_category', array('slug' =&gt; $category-&gt;getSlug(), 'format' =&gt; 'atom'), true)),
            'lastUpdated' =&gt; $lastUpdated    
        ));
    }</pre><p>最后，我们创建<em>show.atom.twig</em>模板：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Category/show.atom.twig --&gt;
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;feed xmlns="http://www.w3.org/2005/Atom"&gt;
    &lt;title&gt;Jobeet ({{ category.name }})&lt;/title&gt;
    &lt;subtitle&gt;Latest Jobs&lt;/subtitle&gt;
    &lt;link href="{{ url('IbwJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}" rel="self" /&gt;
    &lt;updated&gt;{{ lastUpdated }}&lt;/updated&gt;
    &lt;author&gt;&lt;name&gt;Jobeet&lt;/name&gt;&lt;/author&gt;
    &lt;id&gt;{{ feedId }}&lt;/id&gt;

    {% for entity in category.activejobs %}
        &lt;entry&gt;
            &lt;title&gt;{{ entity.position }} ({{ entity.location }})&lt;/title&gt;
            &lt;link href="{{ url('ibw_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}" /&gt;
            &lt;id&gt;{{ entity.id }}&lt;/id&gt;
            &lt;updated&gt;{{ entity.createdAt.format(constant('DATE_ATOM')) }}&lt;/updated&gt;
            &lt;summary type="xhtml"&gt;
                &lt;div xmlns="http://www.w3.org/1999/xhtml"&gt;
                    {% if entity.logo %}
                        &lt;div&gt;
                            &lt;a href="{{ entity.url }}"&gt;
                                &lt;img src="http://{{ app.request.host }}/uploads/jobs/{{ entity.logo }}" alt="{{ entity.company }} logo" /&gt;
                            &lt;/a&gt;
                        &lt;/div&gt;
                    {% endif %}
                    &lt;div&gt;
                        {{ entity.description|nl2br }}
                    &lt;/div&gt;
                    &lt;h4&gt;How to apply?&lt;/h4&gt;
                    &lt;p&gt;{{ entity.howtoapply }}&lt;/p&gt;
                &lt;/div&gt;
            &lt;/summary&gt;
            &lt;author&gt;&lt;name&gt;{{ entity.company }}&lt;/name&gt;&lt;/author&gt;
        &lt;/entry&gt;
    {% endfor %}
&lt;/feed&gt;</pre><p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/20/symfony2-jobeet-day-14-feeds/">http://www.intelligentbee.com/blog/2013/08/20/symfony2-jobeet-day-14-feeds/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/380">jobeet第十四天:订阅</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/380/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第十三天:安全</title>
		<link>http://www.newlifeclan.com/symfony/archives/377</link>
		<comments>http://www.newlifeclan.com/symfony/archives/377#comments</comments>
		<pubDate>Fri, 27 Mar 2015 08:19:28 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[安全]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=377</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/377">jobeet第十三天:安全</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<h2><a id="user-content-实现程序的安全性" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-13/chapter-13.md#%E5%AE%9E%E7%8E%B0%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7"></a>实现程序的安全性</h2>
<p>应用程序的安全性是一个两步验证的过程，它的目标是阻止用户访问他/她不应该访问到的资源。实现安全性的第一步是<strong>认证（authentication）</strong>，安全认证系统会取得用户提交的一些标识符，然后通过这个标识符分辨出用户的身份。一旦系统分辨出用户的身份之后，下一步就是系统对用户进行<strong>授权（authorization）</strong>操作，这一步将决定用户能够访问哪些给定的资源（系统会检查用户是否有权限进行这个操作）。我们可以通过配置<em>app/config</em>目录下的<em>security.yml</em>文件来对应用程序的安全组件进行配置。为了实现我们应用的安全性，我们修改<em>scurity.yml</em>文件：</p>
<p><span id="more-377"></span></p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                login_path:  /login
                check_path:  /login_check
                default_target_path: ibw_jobeet_homepage

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

    providers:
        in_memory:
            memory:
                users:
                    admin: { password: adminpass, roles: 'ROLE_ADMIN' }

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext</pre><p>上面的配置会对网站的<em>/admin</em>部分（所有以<em>/admin</em>作为开头的url）进行安全性保护，并且只有<em>ROLE_ADMIN</em>角色的用户才能访问到<em>/admin</em>（可以查看<em>security.yml</em>中的<em>access_controller</em>部分）。在这个例子中，<em>admin</em>用户被定义在<em>security.yml</em>中（<em>providers</em>部分），而且<em>admin</em>用户的密码没有被编码（即没有经过加密算法处理过）（<em>encoders</em>部分）。对于用户认证，我们需要使用一个传统的登录表单，我们需要去实现它。首先我们需要创建两个路由：一个用来显示登录表单（例如：<em>/login</em>），另一个则用来处理提的交登录表单（例如：<em>/login_check</em>）：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing.yml
login:
    pattern:   /login
    defaults:  { _controller: IbwJobeetBundle:Default:login }
login_check:
    pattern:   /login_check

# ...</pre><p>我们不需要为<em>/login_check</em> URL实现一个控制器，因为防火墙（firewall）会自动捕捉并处理所有提交到<em>/login_check</em> URL的表单。我们需要做的就是创建一个路由（<em>/login_check</em>），我们会在登录表单的模板中使用它，这样才能把登录表单提交到<em>/login_check</em>。</p>
<p>下一步我们来创建显示登录表单的<em>action</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/DefaultController.php
namespace Ibw\JobeetBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class DefaultController extends Controller
{
    // ...

    public function loginAction()
    {
        $request = $this-&gt;getRequest();
        $session = $request-&gt;getSession();

        // get the login error if there is one
        if ($request-&gt;attributes-&gt;has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request-&gt;attributes-&gt;get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session-&gt;get(SecurityContext::AUTHENTICATION_ERROR);
            $session-&gt;remove(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this-&gt;render('IbwJobeetBundle:Default:login.html.twig', array(
            // last username entered by the user
            'last_username' =&gt; $session-&gt;get(SecurityContext::LAST_USERNAME),
            'error'         =&gt; $error,
        ));
    }
}</pre><p>当用户提交了表单，安全系统会自动处理提交的表单。如果用户提交了一个无效的用户名或者密码，那么这个操作就会从系统中读取出提交表单的错误信息，并把这个错误信息反馈给用户。我们的工作仅仅只有显示出一个登录表单和可能出现的登录错误信息，而安全系统则会对用户提交上来的用户名和密码进行验证。</p>
<p>最后，我们来创建模板：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Default/login.html.twig --&gt;
{% if error %}
    &lt;div&gt;{{ error.message }}&lt;/div&gt;
{% endif %}

&lt;form action="{{ path('login_check') }}" method="post"&gt;
    &lt;label for="username"&gt;Username:&lt;/label&gt;
    &lt;input type="text" id="username" name="_username" value="{{ last_username }}" /&gt;

    &lt;label for="password"&gt;Password:&lt;/label&gt;
    &lt;input type="password" id="password" name="_password" /&gt;

    &lt;button type="submit"&gt;login&lt;/button&gt;
&lt;/form&gt;</pre><p>现在，如果我们去访问http://jobeet.local/app_dev.php/admin/dashboard URL，我们会看到一个登录表单，我们需要输入定义在<em>security.yml</em>中的用户名和密码（<em>admin/adminpass</em>）才能进入<em>admin</em>部分。</p>
<p>&nbsp;</p>
<h2>User Providers</h2>
<p>在认证过程中，用户提交了一组登录凭证（通常是用户名和密码）。认证系统的工作就是在用户数据列表中逐个匹配登录凭证。那么用户数据列表从何而来呢？</p>
<p>在<em>Symfony2</em>中，用户数据列表可以存放在任何地方，一个配置文件，一个数据库表，一个web service接口，或者是其它你能想象得到的地方。为认证系统提供一个或者多个用户的类或接口就叫做“<em>user provider</em>”。<em>Symfony2</em> 自带了两种最常见的<em>user provider</em>：一种是使用<strong>配置文件</strong>，另一种是使用<strong>数据库表</strong>。</p>
<p>在上面的例子中，我们把用户列表存放在配置文件里：</p><pre class="crayon-plain-tag"># app/config/security.yml
# ...

providers:
    in_memory:
        memory:
            users:
                admin: { password: adminpass, roles: 'ROLE_ADMIN' }

# ...</pre><p>但我们更多时候是需要把用户信息存放在数据表中。为了做到这点，我们需要在数据库中创建一个<em>user</em>表。首先我们来为<em>user</em>表创建orm文件：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/User.orm.yml
Ibw\JobeetBundle\Entity\User:
    type: entity
    table: user
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        username:
            type: string
            length: 255
        password:
            type: string
            length: 255</pre><p>现在运行命令生成<em>User</em>实体类：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>然后更新数据库：</p><pre class="crayon-plain-tag">php app/console doctrine:schema:update --force</pre><p>这里需要为用户类（User）实现<em>UserInterface</em>接口，这就意味着“用户”可是任何的东西，只要它实现了<em>UserInterface</em>接口。打开并修改<em>User.php</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/User.php
namespace Ibw\JobeetBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;

/**
 * User
 */
class User implements UserInterface
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $username;

    /**
     * @var string
     */
    private $password;

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this-&gt;id;
    }

    /**
     * Set username
     *
     * @param string $username
     * @return User
     */
    public function setUsername($username)
    {
        $this-&gt;username = $username;

    }

    /**
     * Get username
     *
     * @return string 
     */
    public function getUsername()
    {
        return $this-&gt;username;
    }

    /**
     * Set password
     *
     * @param string $password
     * @return User
     */
    public function setPassword($password)
    {
        $this-&gt;password = $password;

    }

    /**
     * Get password
     *
     * @return string 
     */
    public function getPassword()
    {
        return $this-&gt;password;
    }

    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }

    public function getSalt()
    {
        return null;
    }

    public function eraseCredentials()
    {

    }

    public function equals(User $user)
    {
        return $user-&gt;getUsername() == $this-&gt;getUsername();
    }        
}</pre><p>我们需要为<em>User</em>类实现<em>UserInterface</em>接口的抽象方法：<em>getRoles</em>，<em>getSalt</em>，<em>eraseCredentials</em>和<em>equals</em>。</p>
<p>接下来配置一个<em>user provider</em>实体，它指向<em>User</em>类：</p><pre class="crayon-plain-tag"># app/config/security.yml
...

    providers:
        main:
            entity: { class: Ibw\JobeetBundle\Entity\User, property: username }

    encoders:
        Ibw\JobeetBundle\Entity\User: sha512</pre><p>我们同样改变了<em>encoder</em>部分，这样就能够使用<em>sha512</em>算法对<em>User</em>类的<em>password</em>属性进行加密了。</p>
<p>所有准备都已经做好了，现在我们需要先创建一个用户。为了做到这点，我们会创建一个新的<em>Symfony</em>命令：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Command/JobeetUsersCommand.php
namespace Ibw\JobeetBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Ibw\JobeetBundle\Entity\User;

class JobeetUsersCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            -&gt;setName('ibw:jobeet:users')
            -&gt;setDescription('Add Jobeet users')
            -&gt;addArgument('username', InputArgument::REQUIRED, 'The username')
            -&gt;addArgument('password', InputArgument::REQUIRED, 'The password')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $username = $input-&gt;getArgument('username');
        $password = $input-&gt;getArgument('password');

        $em = $this-&gt;getContainer()-&gt;get('doctrine')-&gt;getManager();

        $user = new User();
        $user-&gt;setUsername($username);
        // encode the password
        $factory = $this-&gt;getContainer()-&gt;get('security.encoder_factory');
        $encoder = $factory-&gt;getEncoder($user);
        $encodedPassword = $encoder-&gt;encodePassword($password, $user-&gt;getSalt());
        $user-&gt;setPassword($encodedPassword);
        $em-&gt;persist($user);
        $em-&gt;flush();

        $output-&gt;writeln(sprintf('Added %s user with password %s', $username, $password));
    }
}</pre><p>使用命令创建第一个用户：</p><pre class="crayon-plain-tag">php app/console ibw:jobeet:users admin admin</pre><p>上面的命令会创建一个用户名和密码都是“admin”的用户。你可以使用它登录并访问到<em>admin</em>部分。</p>
<h2></h2>
<h2>登出（Logout）</h2>
<p>防火墙（firewall）能够自动处理登出。我们需要做的就是激活<em>logout</em>配置参数：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    firewalls:
        # ...
        secured_area:
            # ...
            logout:
                path:   /logout
                target: /
    # ...</pre><p>我们不需要为<em>/logout</em> URL实现控制器，防火墙（firewall）会自行处理。我们来创建<em>/logout</em>路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing.yml
# ...

logout:
    pattern:   /logout

# ...</pre><p>一旦配置完成了，访问<em>/logout</em>后，当前用户就会被注销认证（un-authenticate）了。然后用户会被重定向到首页（这个值定义在<em>target</em>参数中）。</p>
<p>剩下没有做的事情就是为<em>admin</em>部分添加<em>logout</em>链接。我们会重载<em>SonataAdminBundle</em>的<em>user_block.html.twig</em>。在<em>app/Resources/SonataAdminBundle/views/Core</em>目录下创建<em>user_block.html.twig</em>文件：</p><pre class="crayon-plain-tag">&lt;!-- app/Resources/SonataAdminBundle/views/Core/user_block.html.twig --&gt;
{% block user_block %}&lt;a href="{{ path('logout') }}"&gt;Logout&lt;/a&gt;{% endblock%}</pre><p>现在，当我们访问<em>admin</em>部分（请先清除缓存）时，我们会被要求填写用户名和密码，进入之后我们可以在右上角看到一个<em>logout</em>链接。</p>
<p>&nbsp;</p>
<h2>用户<em>Session</em></h2>
<p><em>Symfony2</em>提供了一个非常好用的<em>session</em>对象，用户的每个不同请求都可以访问到它里面保存的信息。<em>Symfony2</em>默认是使用原生的<em>PHP session</em>把信息保存到一个<em>cookie</em>中。</p>
<p>我们可以在控制器中方便地取出<em>session</em>里保存的信息：</p><pre class="crayon-plain-tag">$session = $this-&gt;getRequest()-&gt;getSession();

// store an attribute for reuse during a later user request
$session-&gt;set('foo', 'bar');

// in another controller for another request
$foo = $session-&gt;get('foo');</pre><p>可惜的是，在我们<em>Jobeet</em>的用户<em>stories</em>中（第二天的内容），并没有需要在用户<em>session</em>中保存信息的需求。所以我们来改改需求：为了方便<em>Job</em>信息的浏览，我们会在菜单栏上显示用户刚刚浏览过的三条<em>Job</em>信息的链接。</p>
<p>当用户访问一条<em>Job</em>页面时，那么这条<em>Job</em>信息对应的<em>Job</em>对象就会被添加到用户的浏览历史中，并且把它保存到<em>session</em>中：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function showAction($id)
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJob($id);

    if (!$entity) {
        throw $this-&gt;createNotFoundException('Unable to find Job entity.');
    }

    $session = $this-&gt;getRequest()-&gt;getSession();

    // fetch jobs already stored in the job history
    $jobs = $session-&gt;get('job_history', array());

    // store the job as an array so we can put it in the session and avoid entity serialize errors
    $job = array('id' =&gt; $entity-&gt;getId(), 'position' =&gt;$entity-&gt;getPosition(), 'company' =&gt; $entity-&gt;getCompany(), 'companyslug' =&gt; $entity-&gt;getCompanySlug(), 'locationslug' =&gt; $entity-&gt;getLocationSlug(), 'positionslug' =&gt; $entity-&gt;getPositionSlug());

    if (!in_array($job, $jobs)) {
        // add the current job at the beginning of the array
        array_unshift($jobs, $job);

        // store the new job history back into the session
        $session-&gt;set('job_history', array_slice($jobs, 0, 3));
    }

    $deleteForm = $this-&gt;createDeleteForm($id);

    return $this-&gt;render('IbwJobeetBundle:Job:show.html.twig', array(
        'entity'      =&gt; $entity,
        'delete_form' =&gt; $deleteForm-&gt;createView(),
    ));
}</pre><p>在<em>layout</em>文件的<em>#content div</em>标签之前添加下面的代码：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;

&lt;div id="job_history"&gt;
    Recent viewed jobs:
    &lt;ul&gt;
        {% for job in app.session.get('job_history') %}
            &lt;li&gt;
                &lt;a href="{{ path('ibw_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}"&gt;{{ job.position }} - {{ job.company }}&lt;/a&gt;
            &lt;/li&gt;
        {% endfor %}
    &lt;/ul&gt;
&lt;/div&gt;

&lt;div id="content"&gt;
    ...
&lt;/div&gt;
&lt;!-- ... --&gt;</pre><p>&nbsp;</p>
<h2><em>flash</em>信息</h2>
<p><em>flash</em>信息是存放在用户<em>session</em>中的简短信息（通常是用来作为反馈给用户的信息），它通常用在下一个请求中显示信息。它对表单很有用：当你想要重定向并请求一个新页面的同时，显示出一些指定的信息。我们已经在项目的发布<em>Job</em>信息的需求中使用过<em>flash</em>信息了：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function publishAction($token)
{
    // ...

    $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('notice', 'Your job is now online for 30 days.');

    // ...
}</pre><p><em>getFlashBag()-&gt;add()</em>函数的第一个参数是用来标志<em>flash</em>的键，第二个参数是需要显示的信息。我们可以为<em>flash</em>定义任何键名，但<em>notic</em>和<em>error</em>是最常用的两个。</p>
<p>为了显示<em>flash</em>信息，我们需要在模板中把它们包含进来。我们已经在<em>layout.html.twig</em>中做过了：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;!-- ... --&gt;

{% for flashMessage in app.session.flashbag.get('notice') %}
    &lt;div&gt;
        {{ flassMessage }}
    &lt;/div&gt;
{% endfor %}

&lt;!-- ... --&gt;</pre><p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/19/symfony2-jobeet-day-13-security/">http://www.intelligentbee.com/blog/2013/08/19/symfony2-jobeet-day-13-security/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/377">jobeet第十三天:安全</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/377/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第十二天:后台管理工具包-Sonata Admin</title>
		<link>http://www.newlifeclan.com/symfony/archives/374</link>
		<comments>http://www.newlifeclan.com/symfony/archives/374#comments</comments>
		<pubDate>Fri, 27 Mar 2015 08:07:51 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[sonataadmin]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=374</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/374">jobeet第十二天:后台管理工具包-Sonata Admin</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>在第十一天中，我们给<em>Jobeet</em>添加了一些测试，这个应用程序已经完全能够被求职者（seekers）和职位发布者（posters）使用了。现在是时候考虑我们应用程序的<em>admin</em>部分了。今天还好有<a href="http://sonata-project.org/bundles/admin/2-0/doc/index.html">Sonata Admin Bundle</a>的帮助，我们会使用它开发出一个完整<em>Jobeet</em>后台管理接口，（这个过程）用不到一个小时。</p>
<p><span id="more-374"></span></p>
<h2>安装<em>Sonata Admin Bundle</em></h2>
<p>下载<em>SonataAdminBundle</em>和它的依赖到<em>vendor</em>目录下：</p><pre class="crayon-plain-tag">php composer.phar require sonata-project/admin-bundle</pre><p>为了安装最新版本的<em>SonataAdminBundle</em>和依赖，这个使用*作为输入。</p><pre class="crayon-plain-tag">ibw@ubuntu:/var/www/jobeet$ php composer.phar require sonata-project/admin-bundle
Please provide a version constraint for the sonata-project/admin-bundle requirement: *</pre><p>我们同样需要安装<em>SonataDoctrineORMADminBundle</em>：</p><pre class="crayon-plain-tag">php composer.phar require sonata-project/doctrine-orm-admin-bundle</pre><p>现在我们需要声明新的<em>bundle</em>和依赖，修改<em>AppKernel.php</em>文件，添加下面代码到文件中：</p><pre class="crayon-plain-tag">// app/AppKernel.php
// ...
public function registerBundles()
{
    $bundles = array(
        // ...
        new Sonata\AdminBundle\SonataAdminBundle(),
        new Sonata\BlockBundle\SonataBlockBundle(),
        new Sonata\jQueryBundle\SonatajQueryBundle(),
        new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
        new Knp\Bundle\MenuBundle\KnpMenuBundle(),
    );
}

// ...</pre><p>修改<em>config.yml</em>，把下面代码添加到文件末尾：</p><pre class="crayon-plain-tag"># app/config/config.yml
# ...
sonata_admin:
    title: Jobeet Admin

sonata_block:
    default_contexts: [cms]
    blocks:
        sonata.admin.block.admin_list:
            contexts:   [admin]

        sonata.block.service.text:
        sonata.block.service.action:
        sonata.block.service.rss:</pre><p>在<em>config.yml</em>中找到<em>translator</em>键。如果它被注释掉了，那么请不要注释它。</p><pre class="crayon-plain-tag"># app/config/config.yml
# ...
framework:
    # ...
    translator: { fallback: %locale%}
    # ...
#...</pre><p>为了能让应用程序跑起来，我们需要导入（import）<em>admin routes</em>：</p><pre class="crayon-plain-tag"># app/config/routing.yml
admin:
    resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
    prefix: /admin

_sonata_admin:
    resource: .
    type: sonata_admin
    prefix: /admin

# ...</pre><p>现在我们从<em>bundle</em>中安装资源：</p><pre class="crayon-plain-tag">php app/console assets:install web --symlink</pre><p>别忘记把<em>cache</em>清除掉：</p><pre class="crayon-plain-tag">php app/console cache:clear --env=dev
php app/console cache:clear --env=prod</pre><p>现在我们应该可以通过URL：http://jobeet.local/app_dev.php/admin/dashboard访问放到<em>admin</em>面板了。</p>
<blockquote><p>以上的安装需要一步步进行，不然的话可能会报一些Bundle未找到的异常，还有就是目录必须是可写的。可能是SonataAdminBundle更新后的原因，如果在安装过程中遇到需要CoreBundle的问题，那么试着按照提示进行操作，但问题都不大。这里附上一个解决问题的链接：<a href="https://github.com/sonata-project/SonataAdminBundle/issues/1832">https://github.com/sonata-project/SonataAdminBundle/issues/1832</a></p></blockquote>
<h2></h2>
<h2><em>CRUD</em>控制器</h2>
<p><em>CRUD</em>控制器包含了基础的<em>CRUD</em>操作。它是通过控制器名称映射到一个正确的<em>Admin</em>类的一个实例。我们可以按照项目的需求来重写任何或者所有的<em>action</em>。控制器使用<em>Admin</em>类来构造不同的操作。在控制器中可以通过<em>configuration</em>属性来访问<em>Admin</em>对象。</p>
<p>现在我们来为每个实体都创建一个控制器。首先是<em>Category</em>实体：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/CategoryAdminController.php
namespace Ibw\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;

class CategoryAdminController extends Controller
{
    // Your code will be here
}</pre><p>然后是<em>Job</em>实体：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobAdminController.php
namespace Ibw\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;

class JobAdminController extends Controller
{
    // Your code will be here
}</pre><p>&nbsp;</p>
<h2>创建<em>Admin</em>类</h2>
<p><em>Admin</em>类代表的是模型的映射和管理页面（表单，列表，页面显示（show））的部分。为模型创建<em>Admin</em>类的最简单方式是去继承<em>Sonata\AdminBundle\Admin\Admin</em>类。我们会在<em>Admin</em>文件夹中创建<em>Admin</em>类。我们先创建<em>Admin</em>目录，然后为<em>Category</em>创建<em>Admin</em>类：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/CategoryAdmin.php
namespace Ibw\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;

class CategoryAdmin extends Admin
{
    // Your code will be here
}</pre><p>&nbsp;</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/JobAdmin.php
namespace Ibw\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Ibw\JobeetBundle\Entity\Job;

class JobAdmin extends Admin
{
    // Your code will be here
}</pre><p>现在我们需要把每个<em>admin</em>类添加到<em>services.yml</em>配置文件中：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/services.yml
services:
    ibw.jobeet.admin.category:
        class: Ibw\JobeetBundle\Admin\CategoryAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Categories }
        arguments:
            - ~
            - Ibw\JobeetBundle\Entity\Category
            - 'IbwJobeetBundle:CategoryAdmin'

    ibw.jobeet.admin.job:
        class: Ibw\JobeetBundle\Admin\JobAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Jobs }
        arguments:
            - ~
            - Ibw\JobeetBundle\Entity\Job
            - 'IbwJobeetBundle:JobAdmin'</pre><p>现在我们可以在管理面板页看到<em>Jobeet</em>分组了，还有<em>Job</em>和<em>Category</em>模块（modules），它们都有各自的<em>Add new</em>和<em>List</em>链接。</p>
<p><img class="alignnone size-large wp-image-375" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/12-01-1024x292.jpg" alt="12-01" width="780" height="222" /></p>
<h2>配置<em>Admin</em>类</h2>
<p>如果我们去点击<em>Add new</em>或者<em>List</em>链接，我们什么都看不到。那是因为我们还没有为<em>list</em>和<em>form</em>配置字段。我们来做个基础的配置，先从<em>Category</em>开始：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/CategoryAdmin.php
namespace Ibw\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;

class CategoryAdmin extends Admin
{
    // setup the default sort column and order
    protected $datagridValues = array(
        '_sort_order' =&gt; 'ASC',
        '_sort_by' =&gt; 'name'
    );

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            -&gt;add('name')
            -&gt;add('slug')
        ;
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            -&gt;add('name')
        ;
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            -&gt;addIdentifier('name')
            -&gt;add('slug')
        ;
    }
}</pre><p>现在是<em>Job</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/JobAdmin.php
namespace Ibw\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Ibw\JobeetBundle\Entity\Job;

class JobAdmin extends Admin
{
    // setup the defaut sort column and order
    protected $datagridValues = array(
        '_sort_order' =&gt; 'DESC',
        '_sort_by' =&gt; 'created_at'
    );

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            -&gt;add('category')
            -&gt;add('type', 'choice', array('choices' =&gt; Job::getTypes(), 'expanded' =&gt; true))
            -&gt;add('company')
            -&gt;add('file', 'file', array('label' =&gt; 'Company logo', 'required' =&gt; false))
            -&gt;add('url')
            -&gt;add('position')
            -&gt;add('location')
            -&gt;add('description')
            -&gt;add('how_to_apply')
            -&gt;add('is_public')
            -&gt;add('email')
            -&gt;add('is_activated')
        ;
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            -&gt;add('category')
            -&gt;add('company')
            -&gt;add('position')
            -&gt;add('description')
            -&gt;add('is_activated')
            -&gt;add('is_public')
            -&gt;add('email')
            -&gt;add('expires_at')
        ;
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            -&gt;addIdentifier('company')
            -&gt;add('position')
            -&gt;add('location')
            -&gt;add('url')
            -&gt;add('is_activated')
            -&gt;add('email')
            -&gt;add('category')
            -&gt;add('expires_at')
            -&gt;add('_action', 'actions', array(
                'actions' =&gt; array(
                    'view' =&gt; array(),
                    'edit' =&gt; array(),
                    'delete' =&gt; array(),
                )
            ))
        ;
    }

    protected function configureShowField(ShowMapper $showMapper)
    {
        $showMapper
            -&gt;add('category')
            -&gt;add('type')
            -&gt;add('company')
            -&gt;add('webPath', 'string', array('template' =&gt; 'IbwJobeetBundle:JobAdmin:list_image.html.twig'))
            -&gt;add('url')
            -&gt;add('position')
            -&gt;add('location')
            -&gt;add('description')
            -&gt;add('how_to_apply')
            -&gt;add('is_public')
            -&gt;add('is_activated')
            -&gt;add('token')
            -&gt;add('email')
            -&gt;add('expires_at')
        ;
    }
}</pre><p>对于<em>show</em>操作，我们使用自定义的模板来显示公司的<em>logo</em>属性：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/JobAdmin/list_image.html.twig --&gt;
&lt;tr&gt;
    &lt;th&gt;Logo&lt;/th&gt;
    &lt;td&gt;&lt;img src="{{ asset(object.webPath) }}" /&gt;&lt;/td&gt;
&lt;/tr&gt;</pre><p>通过上面的操作，我们为<em>Job</em>和<em>Category</em>创建了基础的管理模块。我们可以发现它们有以下功能：</p>
<ul class="task-list">
<li>列表分页显示</li>
<li>列表可以排序</li>
<li>列表可以过滤</li>
<li>可以创建，编辑，删除对象</li>
<li>可以批量选择对象并进行删除</li>
<li>拥有表单验证</li>
<li>快速反馈给用户的<em>flash</em>信息提示</li>
</ul>
<h2></h2>
<h2>批量操作（Batch Actions）</h2>
<p>批量操作是对一个被选择的模型对象集合（所有的对象或者只是它们中的一个子集）进行的操作。我们能够方便地为列表页面添加自定义的批量操作。<em>delete</em>操作默认就允许我们一次删除多个实体。</p>
<p>为了添加新的批量操作，我们需要重写<em>Admin</em>类的<em>getBatchActions()</em>方法。我们来添加一个批量处理<em>extend</em>的操作：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Admin/JobAdmin.php
// ...

public function getBatchActions()
{
    // retrieve the default (currently only the delete action) actions
    $actions = parent::getBatchActions();

    // check user permissions
    if($this-&gt;hasRoute('edit') &amp;&amp; $this-&gt;isGranted('EDIT') &amp;&amp; $this-&gt;hasRoute('delete') &amp;&amp; $this-&gt;isGranted('DELETE')) {
        $actions['extend'] = array(
            'label'            =&gt; 'Extend',
            'ask_confirmation' =&gt; true // If true, a confirmation will be asked before performing the action
        );

    }

    return $actions;
}</pre><p><em>extend</em>批量操作的核心逻辑在<em>JobAdminController::batchActionExtend()</em>方法中。<em>batchActionExtend()</em>方法有一个查询参数，通过这个查询参数我们可以检索出被选择的模型对象。如果出于某种原因，你可能不需要通过默认的选择方法（selection method）来进行批量操作（例如，你可以选择在模板级别上进行细粒度的模型选择），那么可以传递一个值为null的查询参数。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobAdminController.php
namespace Ibw\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class JobAdminController extends Controller
{
    public function batchActionExtend(ProxyQueryInterface $selectedModelQuery)
    {
        if ($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {
            throw new AccessDeniedException();
        }

        $modelManager = $this-&gt;admin-&gt;getModelManager();

        $selectedModels = $selectedModelQuery-&gt;execute();

        try {
            foreach ($selectedModels as $selectedModel) {
                $selectedModel-&gt;extend();
                $modelManager-&gt;update($selectedModel);
            }
        } catch (\Exception $e) {
            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());

            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
        }

        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('The selected jobs validity has been extended until %s.', date('m/d/Y', time() + 86400 * 30)));

        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
    }
}</pre><p>我们再来添加一个批量删除所有在60天内仍未被激活的<em>Job</em>数据的操作。对于这个操作，我们不需要在列表中选择任何的<em>Job</em>数据，因为在这个操作的逻辑中会去检索符合条件的记录并删除它们。</p><pre class="crayon-plain-tag">//src/Ibw/JobeetBundle/Admin/JobAdmin.php
// ...

public function getBatchActions()
{
    // retrieve the default (currently only the delete action) actions
    $actions = parent::getBatchActions();

    // check user permissions
    if($this-&gt;hasRoute('edit') &amp;&amp; $this-&gt;isGranted('EDIT') &amp;&amp; $this-&gt;hasRoute('delete') &amp;&amp; $this-&gt;isGranted('DELETE')){
        $actions['extend'] = array(
            'label'            =&gt; 'Extend',
            'ask_confirmation' =&gt; true // If true, a confirmation will be asked before performing the action
        );

        $actions['deleteNeverActivated'] = array(
            'label'            =&gt; 'Delete never activated jobs',
            'ask_confirmation' =&gt; true // If true, a confirmation will be asked before performing the action
        );
    }

    return $actions;
}</pre><p>除了创建<em>batchActionDeleteNeverActivated</em>操作外，我们还会创建一个<em>JobAdminController:: batchActionDeleteNeverActivatedIsRelevant()</em>方法，这个方法需要得到确认之后才能够执行，以确保用户真的是要进行这个操作（在我们的这个例子中它总是返回true，因为选择需要被删除的<em>Job</em>数据的逻辑在<em>JobRepository::cleanup()</em>方法中）。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobAdminController.php
// ...

public function batchActionDeleteNeverActivatedIsRelevant()
{
    return true;
}

public function batchActionDeleteNeverActivated()
{
    if ($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {
        throw new AccessDeniedException();
    }

    $em = $this-&gt;getDoctrine()-&gt;getManager();
    $nb = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;cleanup(60);

    if ($nb) {
        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('%d never activated jobs have been deleted successfully.', $nb));
    } else {
        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_info',  'No job to delete.');
    }

    return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));
}</pre><p>今天我们就先到这了！明天我们来看看怎么样为管理员部分添加用户名（username）和密码（password），同时也会讨论<em>Symfony</em>的安全机制。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/18/symfony2-jobeet-day-12-sonata-admin-bundle/">http://www.intelligentbee.com/blog/2013/08/18/symfony2-jobeet-day-12-sonata-admin-bundle/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/374">jobeet第十二天:后台管理工具包-Sonata Admin</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/374/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第十一天:表单测试</title>
		<link>http://www.newlifeclan.com/symfony/archives/372</link>
		<comments>http://www.newlifeclan.com/symfony/archives/372#comments</comments>
		<pubDate>Fri, 27 Mar 2015 07:42:15 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[表单测试]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=372</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/372">jobeet第十一天:表单测试</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>在第十天的内容中，我们使用<em>Symfony 2.3</em>创建了我们的第一个表单。现在用户能够在<em>Jobeet</em>上发布<em>Job</em>信息了，但是我们还没来得及给它做测试呢。别担心，我们会沿着这种（边开发边测试的）开发模式进行下去的。</p>
<p><span id="more-372"></span></p>
<h2>提交表单</h2>
<p>打开<em>JobControllerTest.php</em>，为<em>Job</em>信息的创建和表单验证添加功能测试。我们在文件末尾加入下面的代码，测试是否能够正确访问到创建<em>Job</em>的页面：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testJobForm()
{
    $client = static::createClient();

    $crawler = $client-&gt;request('GET', '/job/new');
    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::newAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
}</pre><p>为了让功能测试能够模拟点击按钮来提交表单，我们会使用<em>selectButton()</em>方法。这个方法能够选择<em>button</em>标签和<em>submit</em>类型的<em>input</em>标签。只要你有一个表示<em>button</em>的<em>Crawler</em>，你就可以调用<em>form()</em>方法得到包含这个<em>button</em>结点的表单的<em>Form</em>实例。</p><pre class="crayon-plain-tag">$form = $crawler-&gt;selectButton('Submit Form')-&gt;form();</pre><p></p>
<blockquote><p>上面的例子中选择了属性值为<em>Submit Form</em>的<em>submit</em>类型的<em>input</em>标签。</p></blockquote>
<p>我们可以在调用<em>form()</em>方法的时候给它传递一个数组类型的参数来重载默认的调用：</p><pre class="crayon-plain-tag">$form = $crawler-&gt;selectButton('submit')-&gt;form(array(
    'name' =&gt; 'Fabien',
    'my_form[subject]' =&gt; 'Symfony Rocks!'
));</pre><p>现在我们来实际操作一下选择表单和传递参数的过程：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testJobForm()
{
    $client = static::createClient();

    $crawler = $client-&gt;request('GET', '/job/new');
    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::newAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));

    $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array(
        'job[company]'      =&gt; 'Sensio Labs',
        'job[url]'          =&gt; 'http://www.sensio.com/',
        'job[file]'         =&gt; __DIR__.'/../../../../../web/bundles/ibwjobeet/images/sensio-labs.gif',
        'job[position]'     =&gt; 'Developer',
        'job[location]'     =&gt; 'Atlanta, USA',
        'job[description]'  =&gt; 'You will work with symfony to develop websites for our customers.',
        'job[how_to_apply]' =&gt; 'Send me an email',
        'job[email]'        =&gt; 'for.a.job@example.com',
        'job[is_public]'    =&gt; false,
    ));

    $client-&gt;submit($form);
    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::createAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
}</pre><p>我们也可以模拟浏览器中的文件上传，我们只要把上传文件的绝对路径传递给它就行了。</p>
<p>表单提交之后，我们需要测试处理提交表单的<em>action</em>是否为<em>create</em>。</p>
<p>&nbsp;</p>
<h2><a id="user-content-测试表单" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-11/chapter-11.md#%E6%B5%8B%E8%AF%95%E8%A1%A8%E5%8D%95"></a>测试表单</h2>
<p>如果表单的值是有效的，那么<em>Job</em>信息应该就可以创建成功，然后用户会被重定向到<em>preview</em>页面：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testJobForm()
{
    // ...
    $client-&gt;followRedirect();
    $this-&gt;assertEquals('Ibw\JobeetBundle\Controller\JobController::previewAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));
}</pre><p>&nbsp;</p>
<h2>测试数据库记录</h2>
<p>最后，我们需要测试数据库中是否有刚才创建的<em>Job</em>数据，还需要测试当用户未公布<em>Job</em>信息时<em>is_activated</em>列是否被设置成<em>false</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testJobForm()
{
    // ...
    $kernel = static::createKernel();
    $kernel-&gt;boot();
    $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

    $query = $em-&gt;createQuery('SELECT count(j.id) from IbwJobeetBundle:Job j WHERE j.location = :location AND j.is_activated IS NULL AND j.is_public = 0');
    $query-&gt;setParameter('location', 'Atlanta, USA');
    $this-&gt;assertTrue(0 &lt; $query-&gt;getSingleScalarResult());
}</pre><p>&nbsp;</p>
<h2>测试错误的表单</h2>
<p>正确的<em>Job</em>表单信息提交后的结果和我们预期的结果一样。现在我们来为错误的<em>Job</em>信息表单做测试：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testJobForm()
{
    // ...
    $crawler = $client-&gt;request('GET', '/job/new');
    $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array(
        'job[company]'      =&gt; 'Sensio Labs',
        'job[position]'     =&gt; 'Developer',
        'job[location]'     =&gt; 'Atlanta, USA',
        'job[email]'        =&gt; 'not.an.email',
    ));
    $crawler = $client-&gt;submit($form);

    // check if we have 3 errors
    $this-&gt;assertTrue($crawler-&gt;filter('.error_list')-&gt;count() == 3);

    // check if we have error on job_description field
    $this-&gt;assertTrue($crawler-&gt;filter('#job_description')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);

    // check if we have error on job_how_to_apply field
    $this-&gt;assertTrue($crawler-&gt;filter('#job_how_to_apply')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);

    // check if we have error on job_email field
    $this-&gt;assertTrue($crawler-&gt;filter('#job_email')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);
}</pre><p>现在我们来测试<em>preview</em>页面中的<em>admin</em>栏。当一条<em>Job</em>信息还未被激活时，我们可以对它进行<em>edit</em>，<em>delete</em>或者<em>publish</em>操作。为了测试这三个<em>action</em>，我们首先需要创建一条<em>Job</em>信息。为了避免过多的复制和粘贴，我们给<em>JobControllerTest</em>类添加一个<em>createJob()</em>的方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function createJob($values = array())
{
    $client = static::createClient();
    $crawler = $client-&gt;request('GET', '/job/new');
    $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array_merge(array(
        'job[company]'      =&gt; 'Sensio Labs',
        'job[url]'          =&gt; 'http://www.sensio.com/',
        'job[position]'     =&gt; 'Developer',
        'job[location]'     =&gt; 'Atlanta, USA',
        'job[description]'  =&gt; 'You will work with symfony to develop websites for our customers.',
        'job[how_to_apply]' =&gt; 'Send me an email',
        'job[email]'        =&gt; 'for.a.job@example.com',
        'job[is_public]'    =&gt; false,
  ), $values));

    $client-&gt;submit($form);
    $client-&gt;followRedirect();

    return $client;
}</pre><p><em>createJob()</em>方法创建一个<em>Job</em>对象，然后进行页面跳转（转到<em>preview</em>页面）。我们可以给<em>createJob()</em>方法传递一个数组，这个数组的值会覆盖默认的表单值。测试<em>Public</em>现在就变得很简单了：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testPublishJob()
{
    $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO1'));
    $crawler = $client-&gt;getCrawler();
    $form = $crawler-&gt;selectButton('Publish')-&gt;form();
    $client-&gt;submit($form);

    $kernel = static::createKernel();
    $kernel-&gt;boot();
    $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

    $query = $em-&gt;createQuery('SELECT count(j.id) from IbwJobeetBundle:Job j WHERE j.position = :position AND j.is_activated = 1');
    $query-&gt;setParameter('position', 'FOO1');
    $this-&gt;assertTrue(0 &lt; $query-&gt;getSingleScalarResult());
}</pre><p>测试<em>Delete</em>操作也很类似：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testDeleteJob()
{
    $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO2'));
    $crawler = $client-&gt;getCrawler();
    $form = $crawler-&gt;selectButton('Delete')-&gt;form();
    $client-&gt;submit($form);

    $kernel = static::createKernel();
    $kernel-&gt;boot();
    $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

    $query = $em-&gt;createQuery('SELECT count(j.id) from IbwJobeetBundle:Job j WHERE j.position = :position');
    $query-&gt;setParameter('position', 'FOO2');
    $this-&gt;assertTrue(0 == $query-&gt;getSingleScalarResult());
}</pre><p>&nbsp;</p>
<h2>用测试作保障</h2>
<p>当一条<em>Job</em>信息被发布之后就不能再对它进行编辑了，尽管<em>Edit</em>链接已经不在<em>preview</em>页面中显示出来。我们来给它做个测试吧。</p>
<p>首先我们为<em>createJob()</em>方法添加另外一个参数，让<em>createJob()</em>方法能够自动发布<em>Job</em>信息。我们来添加一个<em>getJobByPosition()</em>方法，它能够按指定的<em>position</em>返回<em>Job</em>数据：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function createJob($values = array(), $publish = false)
{
    $client = static::createClient();
    $crawler = $client-&gt;request('GET', '/job/new');
    $form = $crawler-&gt;selectButton('Preview your job')-&gt;form(array_merge(array(
        'job[company]'      =&gt; 'Sensio Labs',
        'job[url]'          =&gt; 'http://www.sensio.com/',
        'job[position]'     =&gt; 'Developer',
        'job[location]'     =&gt; 'Atlanta, USA',
        'job[description]'  =&gt; 'You will work with symfony to develop websites for our customers.',
        'job[how_to_apply]' =&gt; 'Send me an email',
        'job[email]'        =&gt; 'for.a.job@example.com',
        'job[is_public]'    =&gt; false,
  ), $values));

    $client-&gt;submit($form);
    $client-&gt;followRedirect();

    if($publish) {
      $crawler = $client-&gt;getCrawler();
      $form = $crawler-&gt;selectButton('Publish')-&gt;form();
      $client-&gt;submit($form);
      $client-&gt;followRedirect();
    }

  return $client;
}

public function getJobByPosition($position)
{
    $kernel = static::createKernel();
    $kernel-&gt;boot();
    $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');

    $query = $em-&gt;createQuery('SELECT j from IbwJobeetBundle:Job j WHERE j.position = :position');
    $query-&gt;setParameter('position', $position);
    $query-&gt;setMaxResults(1);
    return $query-&gt;getSingleResult();
}</pre><p>访问一个已发布<em>Job</em>信息的<em>edit</em>页面会返回<strong>404</strong>状态码：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testEditJob()
{
    $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO3'), true);
    $crawler = $client-&gt;getCrawler();
    $crawler = $client-&gt;request('GET', sprintf('/job/%s/edit', $this-&gt;getJobByPosition('FOO3')-&gt;getToken()));
    $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());
}</pre><p>如果你运行一下测试，你不会得到期望的结果，因为我们昨天忘了实现安全性。编写测试同样是一种发现<em>Bug</em>的好方法，就像你去考虑所有的测试边界值一样。</p>
<p>修改这个<em>Bug</em>很简单，我们只需为已激活的<em>Job</em>信息转向到<strong>404页面</strong>即可：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function editAction($token)
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

    if (!$entity) {
        throw $this-&gt;createNotFoundException('Unable to find Job entity.');
    }

    if ($entity-&gt;getIsActivated()) {
        throw $this-&gt;createNotFoundException('Job is activated and cannot be edited.');
    }

  // ...
}</pre><p>&nbsp;</p>
<h2>“穿越未来”的测试</h2>
<p>当一条<em>Job</em>信息过期天数少于5天，或者<em>Job</em>信息已经过期了，用户能够从当前日期开始延期该条<em>Job</em>信息多30天。在浏览器中做这个测试并不容易，因为当一个<em>Job</em>信息被创建出来后需要30天才过期，所以当访问这个刚创建的<em>Job</em>信息页面时，延期链接是不存在的。当然，你也可以修改数据库中该<em>Job</em>数据的过期时间，或者是调整模板让延期链接一直都显示出来，但是这些方法都很乏味而且容易出错。你现在已经可能猜到了，编写测试能够帮助我们解决问题。</p>
<p>一如既往，我们需要先为<em>extend</em>方法添加新路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/Job.yml
# ...

ibw_job_extend:
    pattern:  /{token}/extend
    defaults: { _controller: "IbwJobeetBundle:Job:extend" }
    requirements: { _method: post }</pre><p>然后修改<em>admin.html.twig</em>，用<em>extend</em>表单替换<em>Extend</em>链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/admin.html.twig --&gt;
&lt;!-- ... --&gt;

{% if job.expiresSoon %}
    &lt;form action="{{ path('ibw_job_extend', { 'token': job.token }) }}" method="post"&gt;
        {{ form_widget(extend_form) }}
        &lt;button type="submit"&gt;Extend&lt;/button&gt; for another 30 days
    &lt;/form&gt;
{% endif %}

&lt;!-- ... --&gt;</pre><p>然后再添加<em>extendAction()</em>方法和<em>createExtendForm()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function extendAction(Request $request, $token)
{
    $form = $this-&gt;createExtendForm($token);
    $request = $this-&gt;getRequest();

    $form-&gt;bind($request);

    if($form-&gt;isValid()) {
        $em=$this-&gt;getDoctrine()-&gt;getManager();
        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

        if(!$entity){
            throw $this-&gt;createNotFoundException('Unable to find Job entity.');
        }

        if(!$entity-&gt;extend()){
            throw $this-&gt;createNodFoundException('Unable to extend the Job');
        }

        $em-&gt;persist($entity);
        $em-&gt;flush();

        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('notice', sprintf('Your job validity has been extended until %s', $entity-&gt;getExpiresAt()-&gt;format('m/d/Y')));
    }

    return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(
        'company' =&gt; $entity-&gt;getCompanySlug(),
        'location' =&gt; $entity-&gt;getLocationSlug(),
        'token' =&gt; $entity-&gt;getToken(),
        'position' =&gt; $entity-&gt;getPositionSlug()
    )));
}

private function createExtendForm($token)
{
    return $this-&gt;createFormBuilder(array('token' =&gt; $token))
        -&gt;add('token', 'hidden')
        -&gt;getForm();
}</pre><p>同样地，为<em>previewAction()</em>添加<em>extend</em>表单：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function previewAction($token)
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

    if (!$entity) {
        throw $this-&gt;createNotFoundException('Unable to find Job entity.');
    }

    $deleteForm = $this-&gt;createDeleteForm($entity-&gt;getId());
    $publishForm = $this-&gt;createPublishForm($entity-&gt;getToken());
    $extendForm = $this-&gt;createExtendForm($entity-&gt;getToken());

    return $this-&gt;render('IbwJobeetBundle:Job:show.html.twig', array(
        'entity'      =&gt; $entity,
        'delete_form' =&gt; $deleteForm-&gt;createView(),
        'publish_form' =&gt; $publishForm-&gt;createView(),
        'extend_form' =&gt; $extendForm-&gt;createView(),
    ));
}</pre><p>如果<em>Job</em>信息已经被<em>extend</em>了，那么<em>Job::extend()</em>方法返回<em>true</em>，否则返回<em>false</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

public function extend()
{
    if (!$this-&gt;expiresSoon())
    {
        return false;
    }

    $this-&gt;expires_at = new \DateTime(date('Y-m-d H:i:s', time() + 86400 * 30));

    return true;
}</pre><p>最后我们来添加测试：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testExtendJob()
{
    // A job validity cannot be extended before the job expires soon
    $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO4'), true);
    $crawler = $client-&gt;getCrawler();
    $this-&gt;assertTrue($crawler-&gt;filter('input[type=submit]:contains("Extend")')-&gt;count() == 0);

    // A job validity can be extended when the job expires soon

    // Create a new FOO5 job
    $client = $this-&gt;createJob(array('job[position]' =&gt; 'FOO5'), true);

    // Get the job and change the expire date to today
    $kernel = static::createKernel();
    $kernel-&gt;boot();
    $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');
    $job = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByPosition('FOO5');
    $job-&gt;setExpiresAt(new \DateTime());
    $em-&gt;flush();

    // Go to the preview page and extend the job
    $crawler = $client-&gt;request('GET', sprintf('/job/%s/%s/%s/%s', $job-&gt;getCompanySlug(), $job-&gt;getLocationSlug(), $job-&gt;getToken(), $job-&gt;getPositionSlug()));
    $crawler = $client-&gt;getCrawler();
    $form = $crawler-&gt;selectButton('Extend')-&gt;form();
    $client-&gt;submit($form);

    // Reload the job from db
    $job = $this-&gt;getJobByPosition('FOO5');

    // Check the expiration date
    $this-&gt;assertTrue($job-&gt;getExpiresAt()-&gt;format('y/m/d') == date('y/m/d', time() + 86400 * 30));
}</pre><p>&nbsp;</p>
<h2>维护任务</h2>
<p>尽管<em>Symfony</em>是一个<em>Web</em>框架，但是它也自带了一系列的命令行工具。我们已经使用过它的命令行工具来生成默认的应用程序包（bundle）目录结构和各种各样的<em>model</em>文件。在<em>Symfony</em>中添加自定义的命令也很简单。当用户创建了一条<em>Job</em>信息之后，用户必须去激活它让它上线，否则的话，久而久之数据库中就会有许多无用的（stale）<em>Job</em>数据。现在我们来自定义一个命令清除数据库中所有无用的<em>Job</em>数据，这个命令通常运行在计划任务（cron job）中。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Command/JobeetCleanupCommand.php
namespace Ibw\JobeetBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Ibw\JobeetBundle\Entity\Job;

class JobeetCleanupCommand extends ContainerAwareCommand {

  protected function configure()
  {
      $this
          -&gt;setName('ibw:jobeet:cleanup')
          -&gt;setDescription('Cleanup Jobeet database')
          -&gt;addArgument('days', InputArgument::OPTIONAL, 'The email', 90)
    ;
  }

  protected function execute(InputInterface $input, OutputInterface $output)
  {
      $days = $input-&gt;getArgument('days');

      $em = $this-&gt;getContainer()-&gt;get('doctrine')-&gt;getManager();
      $nb = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;cleanup($days);

      $output-&gt;writeln(sprintf('Removed %d stale jobs', $nb));
  }
}</pre><p>我们需要在<em>JobRepository</em>类中添加<em>cleanup()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
// ...

public function cleanup($days)
{
    $query = $this-&gt;createQueryBuilder('j')
        -&gt;delete()
        -&gt;where('j.is_activated IS NULL')
        -&gt;andWhere('j.created_at &lt; :created_at')    
        -&gt;setParameter('created_at',  date('Y-m-d', time() - 86400 * $days))
        -&gt;getQuery();

    return $query-&gt;execute();
}</pre><p>在项目根目录下运行下面命令来执行：</p><pre class="crayon-plain-tag">php app/console ibw:jobeet:cleanup</pre><p>或者：</p><pre class="crayon-plain-tag">php app/console ibw:jobeet:cleanup 10</pre><p>上面命令将删除近10天内数据库中无用的<em>Job</em>数据。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/17/symfony2-jobeet-day-11-testing-your-forms/">http://www.intelligentbee.com/blog/2013/08/17/symfony2-jobeet-day-11-testing-your-forms/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/372">jobeet第十一天:表单测试</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/372/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>jobeet第十天:表单</title>
		<link>http://www.newlifeclan.com/symfony/archives/366</link>
		<comments>http://www.newlifeclan.com/symfony/archives/366#comments</comments>
		<pubDate>Fri, 27 Mar 2015 06:40:29 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[symfony表单]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=366</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/366">jobeet第十天:表单</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<p>从简单的<strong>联系表单（contact form）</strong>到带有很多个表单域的复杂表单，任何一个网站都会有表单的身影出现。对于一位<em>Web</em>开发者来说，编写表单是一个复杂而且乏味的工作之一：我们需要使用<em>HTML</em>代码把表单显示出来，然后需要为每个表单域添加验证规则，再把提交到服务器的表单值处理后保存到数据库中，（如果表单数据有错误）还需要把错误信息反馈给用户，而且还需要填充完用户没有错误的表单域等等&#8230;<span id="more-366"></span></p>
<p>在第三天的内容中，我门使用了<code>doctrine:generate:crud</code>命令为<em>Job</em>实体类生成了一个简单的<em>CRUD</em>控制器。同时也生成了一个<em>Job</em>表单，我们可以在<em>/src/Ibw/JobeetBundle/Form/JobType.php</em>文件中找到它。</p>
<h2><a id="user-content-自定义job表单" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-10/chapter-10.md#%E8%87%AA%E5%AE%9A%E4%B9%89job%E8%A1%A8%E5%8D%95"></a>自定义<em>Job</em>表单</h2>
<p><em>Job</em>表单是一个学习如何自定义表单的一个好例子。我们来一步步地学习怎样进行自定义表单。</p>
<p>首先，修改<em>layout</em>中的<em>Post a Job</em>，让它能够正确转向到<em>Post</em>页面：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --&gt;
&lt;a href="{{ path('ibw_job_new') }}"&gt;Post a Job&lt;/a&gt;</pre><p>修改<em>JobController::createAction()</em>中<em>ibw_job_show</em>的路由参数，让它能够匹配我们在第五天中修改过的<em>ibw_job_show</em>路由：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function createAction(Request $request)
{
    $entity  = new Job();
    $form = $this-&gt;createForm(new JobType(), $entity);
    $form-&gt;bind($request);

    if ($form-&gt;isValid()) {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $em-&gt;persist($entity);
        $em-&gt;flush();

        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_show', array(
            'company' =&gt; $entity-&gt;getCompanySlug(),
            'location' =&gt; $entity-&gt;getLocationSlug(),
            'id' =&gt; $entity-&gt;getId(),
            'position' =&gt; $entity-&gt;getPositionSlug()
        )));
    }

    return $this-&gt;render('IbwJobeetBundle:Job:new.html.twig', array(
        'entity' =&gt; $entity,
        'form'   =&gt; $form-&gt;createView(),
    ));
}

// ...</pre><p><em>Doctrine</em>默认是按照数据表中的列（columns）来生成表单域的。但对于<em>Job</em>表单来说，有些表单域是不需要被最终用户（the end user）填写的。我们把<em>Job</em>表单编辑如下：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Form/JobType.php
namespace Ibw\JobeetBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class JobType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            -&gt;add('type')
            -&gt;add('category')
            -&gt;add('company')
            -&gt;add('logo')
            -&gt;add('url')
            -&gt;add('position')
            -&gt;add('location')
            -&gt;add('description')
            -&gt;add('how_to_apply')
            -&gt;add('token')
            -&gt;add('is_public')
            -&gt;add('email')
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver-&gt;setDefaults(array(
            'data_class' =&gt; 'Ibw\JobeetBundle\Entity\Job'
        ));
    }

    public function getName()
    {
        return 'job';
    }
}</pre><p>表单的设置要比数据表的设置更加精确。举个例子来说吧，<em>email</em>列在数据表中仅仅只要求是<em>varchar</em>类型的，而表单中则需要验证<em>email</em>要有正确的格式。在<em>Symfony2</em>中，表单验证被隐含在实体对象之中（比如<em>Job</em>）。换句话说，我们的问题不是验证表单（form）是否有效，而是验证<em>Job</em>对象是否有效（表单提交后的数据会被一一映射成<em>Job</em>对象中的属性值）。为了做到这点，我们在<em>Resources/config</em>目录下创建一个<em>validation.yml</em>文件：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/validation.yml
Ibw\JobeetBundle\Entity\Job:
    properties:
        email:
            - NotBlank: ~
            - Email: ~</pre><p>尽管<em>type</em>列（column）在数据表中是<em>varchar</em>类型的，但我们想限制它的取值只能在：<em>full time</em>，<em>part time</em>和<em>freelance</em>三个值之中。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Form/JobType.php
// ...
use Ibw\JobeetBundle\Entity\Job;

class JobType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            -&gt;add('type', 'choice', array('choices' =&gt; Job::getTypes(), 'expanded' =&gt; true))
            // ...
    }

    // ...

}</pre><p>为了让上面的代码能够运行，我们需要在<em>Job</em>实体中添加下面的代码：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

public static function getTypes()
{
    return array('full-time' =&gt; 'Full time', 'part-time' =&gt; 'Part time', 'freelance' =&gt; 'Freelance');
}

public static function getTypeValues()
{
    return array_keys(self::getTypes());
}

// ...</pre><p>表单利用<em>getTypes()</em>方法生成<em>Job</em>可以选择的类型，<em>getTypeValues()</em>方法被用来验证<em>type</em>表单域值的有效性。</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/validation.yml
Ibw\JobeetBundle\Entity\Job:
    properties:
        type:
            - NotBlank: ~
            - Choice: { callback: getTypeValues }
        email:
            - NotBlank: ~
            - Email: ~</pre><p>对于每个表单域，<em>Symfony</em>会为每个表单域自动生成一个<em>label</em>。我们能通过<em>label</em>选项来修改默认的<em>label</em>值：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Form/JobType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        -&gt;add('logo', null, array('label' =&gt; 'Company logo'))
        // ...
        -&gt;add('how_to_apply', null, array('label' =&gt; 'How to apply?'))
        // ...
        -&gt;add('is_public', null, array('label' =&gt; 'Public?'))
        // ...
}</pre><p>我们同样需要为剩下的表单域添加验证约束：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/validation.yml
Ibw\JobeetBundle\Entity\Job:
    properties:
        category:
            - NotBlank: ~
        type:
            - NotBlank: ~
            - Choice: {callback: getTypeValues}
        company:
            - NotBlank: ~
        position:
            - NotBlank: ~
        location:
            - NotBlank: ~
        description:
            - NotBlank: ~
        how_to_apply:
            - NotBlank: ~
        token:
            - NotBlank: ~
        email:
            - NotBlank: ~
            - Email: ~
        url:
            - Url: ~</pre><p>这里约束<em>url</em>表单域值只能像<em>http://www.sitename.domain</em>或者<em>https://www.sitename.domain</em>这样的格式。</p>
<p>修改完<em>validation.yml</em>之后，我们需要清除<em>cache</em>。</p>
<h2>Symfony2处理上传文件</h2>
<p>为了能够在表单中处理上传文件，我们会给<em>Job</em>实体添加一个新的<em>file</em>属性：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

public $file;

// ...</pre><p>现在我们需要把<em>logo</em>文本域替换成文件域：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Form/JobType.php
// ...

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        -&gt;add('file', 'file', array('label' =&gt; 'Company logo', 'required' =&gt; false))
        // ...
}
// ...</pre><p>为了确保上传的文件是图片，我们需要添加图片文件的验证：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/validation.yml
Ibw\JobeetBundle\Entity\Job:
    properties:
        # ...
        file:
            - Image: ~</pre><p>当表单提交之后，文件域会被映射成<em>UploadedFile</em>类的一个实例。我们能通过给它一个参数来改变上传文件存放的位置。在这之后我们将会看到<em>Job</em>类的<em>logo</em>属性会被设置成上传文件的名字。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function createAction(Request $request)
{
    // ...

    if ($form-&gt;isValid()) {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $entity-&gt;file-&gt;move(__DIR__.'/../../../../web/uploads/jobs', $entity-&gt;file-&gt;getClientOriginalName());
        $entity-&gt;setLogo($entity-&gt;file-&gt;getClientOriginalName());

        $em-&gt;persist($entity);
        $em-&gt;flush();

        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_show', array(
            'company' =&gt; $entity-&gt;getCompanySlug(),
            'location' =&gt; $entity-&gt;getLocationSlug(),
            'id' =&gt; $entity-&gt;getId(),
            'position' =&gt; $entity-&gt;getPositionSlug()
        )));
    }
    // ...
}

// ...</pre><p>我们需要创存放<em>logo</em>图片的目录（<em>web/uploads/jobs</em>），并且确保这个目录是可以写的。</p>
<p>尽管能有这种方式处理文件的上传，但我们有更好的方式，那就是使用<em>Job</em>实体。</p>
<p>首先在<em>Job</em>实体中添加下面的代码：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
class Job
{
    // ... 
    protected function getUploadDir()
    {
        return 'uploads/jobs';
    }

    protected function getUploadRootDir()
    {
        return __DIR__.'/../../../../web/'.$this-&gt;getUploadDir();
    }

    public function getWebPath()
    {
        return null === $this-&gt;logo ? null : $this-&gt;getUploadDir().'/'.$this-&gt;logo;
    }

    public function getAbsolutePath()
    {
        return null === $this-&gt;logo ? null : $this-&gt;getUploadRootDir().'/'.$this-&gt;logo;
    }
}</pre><p><em>logo</em>属性保存的值是上传文件的相对路径，它会被保存到数据库中。<em>getAbsolutePath()</em>方法返回的是上传文件的绝对路径，而<em>getWebPath()</em>方法是返回的是web路径，我们可以在模板中使用它。</p>
<p>我们会把数据库持久化和上传文件实现得具有“原子性（atomic）”：即如果不能成功保存<em>Job</em>实体到数据库或者文件上传失败，那么这两件事都将会失败（比如实体成功保存到数据库了，但文件上传失败了，这种情况属于操作失败，那么数据库保存的数据也必须撤销掉）。我们在当<em>Doctrine</em>成功保存实体后才把上传文件存在目录中。我们可以通过<em>Job</em>实体的<em>lifecycle callback</em>来完成这项操作。就像我们在第三天内容中所做的，我们在<em>Job.orm.yml</em>中添加<em>preUpload</em>，<em>upload</em> 和<em>removeUpload</em>回调方法：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Job.orm.yml
Ibw\JobeetBundle\Entity\Job:
    # ...

    lifecycleCallbacks:
        prePersist: [ preUpload, setCreatedAtValue, setExpiresAtValue ]
        preUpdate: [ preUpload, setUpdatedAtValue ]
        postPersist: [ upload ]
        postUpdate: [ upload ]
        postRemove: [ removeUpload ]</pre><p>现在运行<code>generate:entities</code>命令为<em>Job</em>实体生成新的方法：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>修改<em>Job.php</em>中新增的方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
class Job
{
    // ...

    /**
     * @ORM\PrePersist
     */
    public function preUpload()
    {
         if (null !== $this-&gt;file) {
             $this-&gt;logo = uniqid().'.'.$this-&gt;file-&gt;guessExtension();
         }
    }

    /**
     * @ORM\PostPersist
     */
    public function upload()
    {
        if (null === $this-&gt;file) {
            return;
        }

        // If there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        $this-&gt;file-&gt;move($this-&gt;getUploadRootDir(), $this-&gt;logo);

        unset($this-&gt;file);
    }

    /**
     * @ORM\PostRemove
     */
    public function removeUpload()
    {
        if(file_exists($this-&gt;file)) {
            if ($this-&gt;file = $this-&gt;getAbsolutePath()) {
                unlink($this-&gt;file);
            }
        }    
    }
}</pre><p>现在<em>Job</em>实体类已经能够完成我们需要的工作了：它会在保存到数据库之前生成一个唯一的文件名，保存数据库后就会把文件存放到目录下，当实体被删除的时候上传文件也将被删除。<em>Job</em>实体已经能够自动处理文件上传了，我们现在需要删除<em>JobController</em>中处理文件上传的代码：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

    public function createAction(Request $request)
    {
        $entity  = new Job();
        $form = $this-&gt;createForm(new JobType(), $entity);
        $form-&gt;bind($request);

        if ($form-&gt;isValid()) {
            $em = $this-&gt;getDoctrine()-&gt;getManager();

            $em-&gt;persist($entity);
            $em-&gt;flush();

            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_show', array(
                'company' =&gt; $entity-&gt;getCompanySlug(),
                'location' =&gt; $entity-&gt;getLocationSlug(),
                'id' =&gt; $entity-&gt;getId(),
                'position' =&gt; $entity-&gt;getPositionSlug()
            )));
        }

        return $this-&gt;render('IbwJobeetBundle:Job:new.html.twig', array(
            'entity' =&gt; $entity,
            'form'   =&gt; $form-&gt;createView(),
        ));
    }

// ...</pre><p>&nbsp;</p>
<h2>表单模板</h2>
<p>我们已经完成了表单类的自定义，现在我们需要把它显示出来。修改<em>new.html.twig</em>模板：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/new.html.twig --&gt;
{% extends 'IbwJobeetBundle::layout.html.twig' %}

{% form_theme form _self %}

{% block form_errors %}
{% spaceless %}
    {% if errors|length &gt; 0 %}
        &lt;ul class="error_list"&gt;
            {% for error in errors %}
                &lt;li&gt;{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}&lt;/li&gt;
            {% endfor %}
        &lt;/ul&gt;
    {% endif %}
{% endspaceless %}
{% endblock form_errors %}

{% block stylesheets %}
    {{ parent() }}
    &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/job.css') }}" type="text/css" media="all" /&gt;
{% endblock %}

{% block content %}
    &lt;h1&gt;Job creation&lt;/h1&gt;
    &lt;form action="{{ path('ibw_job_create') }}" method="post" {{ form_enctype(form) }}&gt;
        &lt;table id="job_form"&gt;
            &lt;tfoot&gt;
                &lt;tr&gt;
                    &lt;td colspan="2"&gt;
                        &lt;input type="submit" value="Preview your job" /&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/tfoot&gt;
            &lt;tbody&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.category) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.category) }}
                        {{ form_widget(form.category) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.type) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.type) }}
                        {{ form_widget(form.type) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.company) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.company) }}
                        {{ form_widget(form.company) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.file) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.file) }}
                        {{ form_widget(form.file) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.url) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.url) }}
                        {{ form_widget(form.url) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.position) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.position) }}
                        {{ form_widget(form.position) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.location) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.location) }}
                        {{ form_widget(form.location) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.description) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.description) }}
                        {{ form_widget(form.description) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.how_to_apply) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.how_to_apply) }}
                        {{ form_widget(form.how_to_apply) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.token) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.token) }}
                        {{ form_widget(form.token) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.is_public) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.is_public) }}
                        {{ form_widget(form.is_public) }}
                        &lt;br /&gt; Whether the job can also be published on affiliate websites or not.
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(form.email) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(form.email) }}
                        {{ form_widget(form.email) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/tbody&gt;
        &lt;/table&gt;
    {{ form_end(form) }}
{% endblock %}</pre><p>我们可以使用下面那一行代码来显示表单，但我们需要更多的自定义内容，所以我们选择手动来添加表单域。</p><pre class="crayon-plain-tag">{{ form(form) }}</pre><p>&nbsp;</p>
<p><code>form(form)</code>会显示每个表单域，而且还会带有<em>label</em>和<em>error</em>信息（如果有的话）。虽然这种方式简单，但是它不是很灵活。更多时候我们需要去自定义表单域的显示，以更好地控制它们的样式外观。</p>
<p>我们也使用一种叫做<a href="http://symfony.com/doc/current/book/forms.html#form-theming">form theming</a>的技术来自定义<a href="http://symfony.com/doc/current/cookbook/form/form_customization.html#customizing-error-output">form errors</a>的渲染。你可以参考<a href="http://symfony.com/doc/current/book/index.html">Symfony2的官方文档</a>。</p>
<p>对<em>eidt.html.twig</em>做同样的修改：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/edit.html.twig --&gt;
{% extends 'IbwJobeetBundle::layout.html.twig' %}

{% form_theme edit_form _self %}

{% block form_errors %}
{% spaceless %}
    {% if errors|length &gt; 0 %}
        &lt;ul class="error_list"&gt;
            {% for error in errors %}
                &lt;li&gt;{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}&lt;/li&gt;
            {% endfor %}
        &lt;/ul&gt;
    {% endif %}
{% endspaceless %}
{% endblock form_errors %}

{% block stylesheets %}
    {{ parent() }}
    &lt;link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/job.css') }}" type="text/css" media="all" /&gt;
{% endblock %}

{% block content %}
    &lt;h1&gt;Job edit&lt;/h1&gt;
    &lt;form action="{{ path('ibw_job_update', { 'id': entity.id }) }}" method="post" {{ form_enctype(edit_form) }}&gt;
        &lt;table id="job_form"&gt;
            &lt;tfoot&gt;
                &lt;tr&gt;
                    &lt;td colspan="2"&gt;
                        &lt;input type="submit" value="Preview your job" /&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/tfoot&gt;
            &lt;tbody&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.category) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.category) }}
                        {{ form_widget(edit_form.category) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.type) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.type) }}
                        {{ form_widget(edit_form.type) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.company) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.company) }}
                        {{ form_widget(edit_form.company) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.file) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.file) }}
                        {{ form(edit_form.file) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.url) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.url) }}
                        {{ form_widget(edit_form.url) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.position) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.position) }}
                        {{ form_widget(edit_form.position) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.location) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.location) }}
                        {{ form_widget(edit_form.location) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.description) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.description) }}
                        {{ form_widget(edit_form.description) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.how_to_apply) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.how_to_apply) }}
                        {{ form_widget(edit_form.how_to_apply) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.token) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.token) }}
                        {{ form_widget(edit_form.token) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.is_public) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.is_public) }}
                        {{ form_widget(edit_form.is_public) }}
                        &lt;br /&gt; Whether the job can also be published on affiliate websites or not.
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;th&gt;{{ form_label(edit_form.email) }}&lt;/th&gt;
                    &lt;td&gt;
                        {{ form_errors(edit_form.email) }}
                        {{ form_widget(edit_form.email) }}
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/tbody&gt;
        &lt;/table&gt;
    {{ form_end(edit_form) }}
{% endblock %}</pre><p>&nbsp;</p>
<h2>表单行为（<em>Action</em>）</h2>
<p>现在我们已经有了一个表单类（<em>JobType</em>）和一个利用它生成表单的模板。现在是时候让它能够做一点实际的行为了。<em>Job</em>表单被<em>JobController</em>中的四个方法所管理着：</p>
<ul class="task-list">
<li><em>newAction</em>：显示一个空表单用来创建一个新<em>job</em></li>
<li><em>createAction</em>：处理表单（表单验证，表单填充）和利用用户提交的表单值实例化一个<em>Job</em>对象</li>
<li><em>editAction</em>：显示一个已存在<em>job</em>的编辑表单</li>
<li><em>updateAction</em>：处理表单（表单验证，表单填充）和利用用户提交的表单值更新已存在的<em>Job</em>对象</li>
</ul>
<p>当你浏览<em>/job/new page</em>时，<em>createForm()</em>方法会实例化一个新的<em>job</em>表单实例，然后把这个表单实例传递给模板（<em>newAction</em>）。</p>
<p>当用户提交了表单（<em>createAction</em>），表单就绑定有用户提交的值（<em>bind($request)</em>方法），同时也会触发表单验证。一旦表单被绑定了，那么就可以使用<em>isValid()</em>方法验证表单是否有效：如果表单有效（返回true），那么<em>Job</em>数据就会被保存到数据库中（<em>$em-&gt;persist($entity)</em>），然后用户会被重定向到<em>Job</em>数据的预览页面；如果表单无效，那么<em>new.html.twig</em>模板会被重新渲染，而且表单还会自动填充用户提交上来的表单值，同时还会显示出错误信息。修改一个已存在的<em>Job</em>和这个过程很相似。<em>new</em>和<em>edit</em>行为唯一不同的是，需要被修改的<em>Job</em>对象是被作为<em>crateForm()</em>方法的第二个参数所使用。在模板中，这个对象的值的会被用来当做默认表单域的值。</p>
<p>我们也可以为<em>creation</em>表单定义默认的值。我们会传递一个预修改的<em>Job</em>对象给<em>createForm()</em>方法来设置<em>type</em>的默认值为<em>full-time</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/jobController.php
// ...

public function newAction()
{
    $entity = new Job();
    $entity-&gt;setType('full-time');
    $form = $this-&gt;createForm(new JobType(), $entity);

    return $this-&gt;render('IbwJobeetBundle:Job:new.html.twig', array(
        'entity' =&gt; $entity,
        'form'   =&gt; $form-&gt;createView()
    ));
}

// ...</pre><p>&nbsp;</p>
<h2>使用<em>Token</em>来保护表单</h2>
<p>现在一切都进展顺利，可是用户必须得填写<em>token</em>才能添加<em>Job</em>数据。通常来说，<em>token</em>应该是在这个<em>Job</em>数据被创建的时候自动生成的，我们不想让用户自己去提供这个唯一值的<em>token</em>。给<em>Job</em>实体的<em>prePersist lifecycleCallbacks</em>添加<em>setTokenValue</em>方法：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# ...

lifecycleCallbacks:
    prePersist: [ setTokenValue, preUpload, setCreatedAtValue, setExpiresAtValue ]
     # ...</pre><p>重新生成实体：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>修改<em>Job</em>实体的<em>setTokenValue()</em>方法，给它添加生成<em>token</em>的逻辑代码：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

public function setTokenValue()
{
    if(!$this-&gt;getToken()) {
        $this-&gt;token = sha1($this-&gt;getEmail().rand(11111, 99999));
    }
}

// ...</pre><p>删除表单中的<em>token</em>域：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Form/JobType.php
// ...

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        -&gt;add('category')
        -&gt;add('type', 'choice', array('choices' =&gt; Job::getTypes(), 'expanded' =&gt; true))
        -&gt;add('company')
        -&gt;add('file', 'file', array('label' =&gt; 'Company logo', 'required' =&gt; false))
        -&gt;add('url')
        -&gt;add('position')
        -&gt;add('location')
        -&gt;add('description')
        -&gt;add('how_to_apply', null, array('label' =&gt; 'How to apply?'))
        -&gt;add('is_public', null, array('label' =&gt; 'Public?'))
        -&gt;add('email')
    ;
}

// ...</pre><p>删除<em>/new.html.twig</em>和<em>edit.html.twig</em>中的<em>token</em>域：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/new.html.twig --&gt;
&lt;!-- ... --&gt;
&lt;tr&gt;
    &lt;th&gt;{{ form_label(form.token) }}&lt;/th&gt;
    &lt;td&gt;
        {{ form_errors(form.token) }}
        {{ form_widget(form.token) }}
    &lt;/td&gt;
&lt;/tr&gt;
&lt;!-- ... --&gt;</pre><p>&nbsp;</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/edit.html.twig --&gt;
&lt;!-- ... --&gt;
&lt;tr&gt;
    &lt;th&gt;{{ form_label(edit_form.token) }}&lt;/th&gt;
    &lt;td&gt;
        {{ form_errors(edit_form.token) }}
        {{ form(edit_form.token) }}
    &lt;/td&gt;
&lt;/tr&gt;
&lt;!-- ... --&gt;</pre><p>删除<em>validation.yml</em>中的<em>token</em>域：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/validation.yml
# ...
    # ...
    token:
        - NotBlank: ~</pre><p>还记得第二天内容中的用户<em>stories</em>吗，只有用户知道<em>Job</em>的<em>token</em>才能对该<em>Job</em>信息进行修改。很好，修改或者删除一个<em>Job</em>数据是多么得简单呀，你只需要去猜那个URL中的<em>token</em>是什么值就可以办到了，你说这不是很简单（keng ren）吗？。修改<em>Job</em>信息的访问URL类似于<em>/job/ID/edit</em>，这里的<em>ID</em>是<em>Job</em>的主键。</p>
<p>我们来修改路由，只有URL带上<em>token</em>才能修改和删除<em>Job</em>信息：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/job.yml
# ...

ibw_job_edit:
    pattern:  /{token}/edit
    defaults: { _controller: "IbwJobeetBundle:Job:edit" }

ibw_job_update:
    pattern:  /{token}/update
    defaults: { _controller: "IbwJobeetBundle:Job:update" }
    requirements: { _method: post|put }

ibw_job_delete:
    pattern:  /{token}/delete
    defaults: { _controller: "IbwJobeetBundle:Job:delete" }
    requirements: { _method: post|delete }</pre><p>现在修改<em>JobController</em>，使用<em>token</em>来代替<em>id</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...
class JobController extends Controller
{
    // ...

    public function editAction($token)
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

        if (!$entity) {
            throw $this-&gt;createNotFoundException('Unable to find Job entity.');
        }

        $editForm = $this-&gt;createForm(new JobType(), $entity);
        $deleteForm = $this-&gt;createDeleteForm($token);

        return $this-&gt;render('IbwJobeetBundle:Job:edit.html.twig', array(
            'entity'      =&gt; $entity,
            'edit_form'   =&gt; $editForm-&gt;createView(),
            'delete_form' =&gt; $deleteForm-&gt;createView(),
        ));
    }   

    public function updateAction(Request $request, $token)
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

        if (!$entity) {
            throw $this-&gt;createNotFoundException('Unable to find Job entity.');
        }

        $editForm   = $this-&gt;createForm(new JobType(), $entity);
        $deleteForm = $this-&gt;createDeleteForm($token);

        $editForm-&gt;bind($request);

        if ($editForm-&gt;isValid()) {
            $em-&gt;persist($entity);
            $em-&gt;flush();

            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_edit', array('token' =&gt; $token)));
        }

        return $this-&gt;render('IbwJobeetBundle:Job:edit.html.twig', array(
            'entity'      =&gt; $entity,
            'edit_form'   =&gt; $editForm-&gt;createView(),
            'delete_form' =&gt; $deleteForm-&gt;createView(),
        ));
    }

    public function deleteAction(Request $request, $token)
    {
        $form = $this-&gt;createDeleteForm($token);
        $form-&gt;bind($request);

        if ($form-&gt;isValid()) {
            $em = $this-&gt;getDoctrine()-&gt;getManager();
            $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

            if (!$entity) {
                throw $this-&gt;createNotFoundException('Unable to find Job entity.');
            }

            $em-&gt;remove($entity);
            $em-&gt;flush();
        }

        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job'));
    }

    /**
     * Creates a form to delete a Job entity by id.
     *
     * @param mixed $id The entity id
     *
     * @return Symfony\Component\Form\Form The form
     */
    private function createDeleteForm($token)
    {
        return $this-&gt;createFormBuilder(array('token' =&gt; $token))
            -&gt;add('token', 'hidden')
            -&gt;getForm()
        ;
    }
}</pre><p>修改<em>show.html.twig</em>模板中的<em>ibw_job_edit</em>路由参数：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/show.html.twig --&gt;
&lt;a href="{{ path('ibw_job_edit', {'token': entity.token}) }}"&gt;</pre><p>修改<em>edit.html.twig</em>模板中的<em>ibw_job_update</em>路由：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/edit.html.twig --&gt;
&lt;form action="{{ path('ibw_job_update', {'token': entity.token}) }}" method="post" {{ form_enctype(edit_form) }}&gt;</pre><p>除了<em>job_show_user</em>路由外，其他和<em>Job</em>相关的路由都已经带上了<em>token</em>。现在，一个<em>Job</em>的URL样式应该是类似于这样的：<em>http://jobeet.local/job/TOKEN/edit</em></p>
<p>&nbsp;</p>
<h2>预览页面</h2>
<p>预览页面和<em>Job</em>页面显示的内容是一样的。唯一不同的是，预览页面是通过<em>token</em>访问的，而不是通过<em>id</em>：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/job.yml
# ...

ibw_job_show:
    pattern:  /{company}/{location}/{id}/{position}
    defaults: { _controller: "IbwJobeetBundle:Job:show" }
    requirements:
        id:  \d+

ibw_job_preview:
    pattern:  /{company}/{location}/{token}/{position}
    defaults: { _controller: "IbwJobeetBundle:Job:preview" }
    requirements:
        token:  \w+

# ...</pre><p><em>previewAction()</em>（它和<em>showAction()</em>不同的是，它通过<em>token</em>来检索<em>job</em>，而不是通过<em>id</em>）：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function previewAction($token)
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

    if (!$entity) {
        throw $this-&gt;createNotFoundException('Unable to find Job entity.');
    }

    $deleteForm = $this-&gt;createDeleteForm($entity-&gt;getId());

    return $this-&gt;render('IbwJobeetBundle:Job:show.html.twig', array(
        'entity'      =&gt; $entity,
        'delete_form' =&gt; $deleteForm-&gt;createView(),
    ));
}

// ...</pre><p>如果用户是通过带<em>token</em>的URL访问到页面的，那么我们就会在页面顶部显示一个<em>admin</em>栏。在<em>show.html.twig</em>的开头包含（include）一个模板，这个模板是用来显示<em>admin</em>栏的。同时删除底部的<em>edit</em>链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/show.html.twig --&gt;
&lt;!-- ... --&gt;

{% block content %}
    {% if app.request.get('token') %}
        {% include 'IbwJobeetBundle:Job:admin.html.twig' with {'job': entity} %}
    {% endif %}

 &lt;!-- ... --&gt;

{% endblock %}</pre><p>然后创建<em>admin.html.twig</em>模板：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/admin.html.twig --&gt;
&lt;div id="job_actions"&gt;
    &lt;h3&gt;Admin&lt;/h3&gt;
    &lt;ul&gt;
        {% if not job.isActivated %}
            &lt;li&gt;&lt;a href="{{ path('ibw_job_edit', { 'token': job.token }) }}"&gt;Edit&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href="{{ path('ibw_job_edit', { 'token': job.token }) }}"&gt;Publish&lt;/a&gt;&lt;/li&gt;
        {% endif %}
        &lt;li&gt;
            &lt;form action="{{ path('ibw_job_delete', { 'token': job.token }) }}" method="post"&gt;
                {{ form_widget(delete_form) }}
                &lt;button type="submit" onclick="if(!confirm('Are you sure?')) { return false; }"&gt;Delete&lt;/button&gt;
            &lt;/form&gt;
        &lt;/li&gt;
        {% if job.isActivated %}
            &lt;li {% if job.expiresSoon %} class="expires_soon" {% endif %}&gt;
                {% if job.isExpired %}
                    Expired
                {% else %}
                    Expires in &lt;strong&gt;{{ job.getDaysBeforeExpires }}&lt;/strong&gt; days
                {% endif %}

                {% if job.expiresSoon %}
                    - &lt;a href=""&gt;Extend&lt;/a&gt; for another 30 days
                {% endif %}
            &lt;/li&gt;
        {% else %}
            &lt;li&gt;
                [Bookmark this &lt;a href="{{ url('ibw_job_preview', { 'token': job.token, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}"&gt;URL&lt;/a&gt; to manage this job in the future.]
            &lt;/li&gt;
        {% endif %}
    &lt;/ul&gt;
&lt;/div&gt;</pre><p>这里虽然代码很多，但是都是简单并且容易理解的。</p>
<p>为了让模板的可读性更好，我们为<em>Job</em>实体添加一组简单的方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

public function isExpired()
{
    return $this-&gt;getDaysBeforeExpires() &lt; 0;
}

public function expiresSoon()
{
    return $this-&gt;getDaysBeforeExpires() &lt; 5;    
}

public function getDaysBeforeExpires()
{
    return ceil(($this-&gt;getExpiresAt()-&gt;format('U') - time()) / 86400);
}

// ...</pre><p><em>admin</em>栏随着<em>Job</em>状态的不同显示出来的样式也会不同：</p>
<p><img class="alignnone size-full wp-image-368" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/Day-10-admin-bar.png" alt="Day-10-admin-bar" width="858" height="288" /></p>
<p><img class="alignnone size-full wp-image-367" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/Day-10-admin-badr-2.png" alt="Day-10-admin-badr-2" width="862" height="279" /></p>
<p>现在修改<em>JobController</em>中的<em>createAction()</em>和<em>updateAction()</em>，让它们重定向到预览页面：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
public function createAction(Request $request)
{
    // ...
    if ($form-&gt;isValid()) {
        // ... 
        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(
            'company' =&gt; $entity-&gt;getCompanySlug(),
            'location' =&gt; $entity-&gt;getLocationSlug(),
            'token' =&gt; $entity-&gt;getToken(),
            'position' =&gt; $entity-&gt;getPositionSlug()
        )));
    }
    // ...
}

public function updateAction(Request $request, $token)
{
    // ...
    if ($editForm-&gt;isValid()) {
        // ... 

        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(
            'company' =&gt; $entity-&gt;getCompanySlug(),
            'location' =&gt; $entity-&gt;getLocationSlug(),
            'token' =&gt; $entity-&gt;getToken(), 
            'position' =&gt; $entity-&gt;getPositionSlug()
        )));
    }
    // ...
}</pre><p>就像我们之前所说的，如果你知道<em>token</em>或者你是<em>Jobeet</em>的管理员的话，你就有权可以修改一条<em>Job</em>信息。但是我们现在去访问<em>Job</em>页面的话，我们可以看到页面上有一个<em>Edit</em>链接，这样的用户体验让人感觉不好。我们来修改<em>show.html.twig</em>，删除<em>Edit</em>链接：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/show.html.twig --&gt;
&lt;div style="padding: 20px 0"&gt;
    &lt;a href="{{ path('ibw_job_edit', { 'token': entity.token }) }}"&gt;
        Edit
    &lt;/a&gt;
&lt;/div&gt;</pre><p>&nbsp;</p>
<h2><em>Job</em>激活和发布</h2>
<p>在<em>Job</em>页面的上部分有一个发布<em>Job</em>信息的链接，这个链接需要被指定到<em>publishAction()</em>上。我们先创建一个新的路由：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/routing/job.yml
# ...

ibw_job_publish:
    pattern:  /{token}/publish
    defaults: { _controller: "IbwJobeetBundle:Job:publish" }
    requirements: { _method: post }</pre><p>现在我们来修改<em>Publish</em>链接（我们在这里使用表单，就像删除一个<em>Job</em>一样，它会发送一个<em>POST</em>请求）：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/admin.html.twig --&gt;
&lt;!-- ... --&gt;

{% if not job.isActivated %}
    &lt;li&gt;&lt;a href="{{ path('ibw_job_edit', { 'token': job.token }) }}"&gt;Edit&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;
        &lt;form action="{{ path('ibw_job_publish', { 'token': job.token }) }}" method="post"&gt;
            {{ form_widget(publish_form) }}
            &lt;button type="submit"&gt;Publish&lt;/button&gt;
        &lt;/form&gt;
    &lt;/li&gt;
{% endif %}

&lt;!-- ... --&gt;</pre><p>最后一步是创建<em>publishAction()</em>，同时修改<em>previewAction()</em>给模板传递一个<em>publish</em>表单：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function previewAction($token)
{
    // ...

    $deleteForm = $this-&gt;createDeleteForm($entity-&gt;getToken());
    $publishForm = $this-&gt;createPublishForm($entity-&gt;getToken());

    return $this-&gt;render('IbwJobeetBundle:Job:show.html.twig', array(
        'entity'      =&gt; $entity,
        'delete_form' =&gt; $deleteForm-&gt;createView(),
        'publish_form' =&gt; $publishForm-&gt;createView(),
    ));
}

public function publishAction(Request $request, $token)
{
    $form = $this-&gt;createPublishForm($token);
    $form-&gt;bind($request);

    if ($form-&gt;isValid()) {
        $em = $this-&gt;getDoctrine()-&gt;getManager();
        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);

        if (!$entity) {
            throw $this-&gt;createNotFoundException('Unable to find Job entity.');
        }

        $entity-&gt;publish();
        $em-&gt;persist($entity);
        $em-&gt;flush();

        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('notice', 'Your job is now online for 30 days.');
    }

    return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(
        'company' =&gt; $entity-&gt;getCompanySlug(),
        'location' =&gt; $entity-&gt;getLocationSlug(),
        'token' =&gt; $entity-&gt;getToken(),
        'position' =&gt; $entity-&gt;getPositionSlug()
    )));
}

private function createPublishForm($token)
{
    return $this-&gt;createFormBuilder(array('token' =&gt; $token))
        -&gt;add('token', 'hidden')
        -&gt;getForm()
    ;
}

// ...</pre><p><em>PublishAction()</em>方法使用的<em>publish()</em>可以在<em>Job</em>实体中定义：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity
// ...

public function publish()
{
    $this-&gt;setIsActivated(true);
}

// ...</pre><p>现在我们可以在浏览中测试<em>publish</em>功能了。</p>
<p>但现在我们依然还有问题需要去修正。那些未被激活的<em>Job</em>数据是不可以被访问到的，也不能出现在首页中。我们需要修改<em>JobRepository</em>：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
namespace Ibw\JobeetBundle\Repository;
use Doctrine\ORM\EntityRepository;

class JobRepository extends EntityRepository
{
    public function getActiveJobs($category_id = null, $max = null, $offset = null)
    {
        $qb = $this-&gt;createQueryBuilder('j')
            -&gt;where('j.expires_at &gt; :date')
            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
            -&gt;andWhere('j.is_activated = :activated')
            -&gt;setParameter('activated', 1)
            -&gt;orderBy('j.expires_at', 'DESC');

        if($max) {
            $qb-&gt;setMaxResults($max);
        }

        if($offset) {
            $qb-&gt;setFirstResult($offset);
        }

        if($category_id) {
            $qb-&gt;andWhere('j.category = :category_id')
                -&gt;setParameter('category_id', $category_id);
        }

        $query = $qb-&gt;getQuery();

        return $query-&gt;getResult();
    }

    public function countActiveJobs($category_id = null)
    {
        $qb = $this-&gt;createQueryBuilder('j')
            -&gt;select('count(j.id)')
            -&gt;where('j.expires_at &gt; :date')
            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
            -&gt;andWhere('j.is_activated = :activated')
            -&gt;setParameter('activated', 1);

        if($category_id) {
            $qb-&gt;andWhere('j.category = :category_id')
                -&gt;setParameter('category_id', $category_id);
        }

        $query = $qb-&gt;getQuery();

        return $query-&gt;getSingleScalarResult();
    }

    public function getActiveJob($id)
    {
        $query = $this-&gt;createQueryBuilder('j')
            -&gt;where('j.id = :id')
            -&gt;setParameter('id', $id)
            -&gt;andWhere('j.expires_at &gt; :date')
            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
            -&gt;andWhere('j.is_activated = :activated')
            -&gt;setParameter('activated', 1)
            -&gt;setMaxResults(1)
            -&gt;getQuery();

        try {
            $job = $query-&gt;getSingleResult();
        } catch (\Doctrine\Orm\NoResultException $e) {
        $job = null;
          }

        return $job;
    }
}</pre><p>修改<em>CategoryRepository</em>中的<em>getWithJobs()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/CategoryRepository.php
namespace Ibw\JobeetBundle\Repository;
use Doctrine\ORM\EntityRepository;

class CategoryRepository extends EntityRepository
{
    public function getWithJobs()
    {
        $query = $this-&gt;getEntityManager()
            -&gt;createQuery('SELECT c FROM IbwJobeetBundle:Category c LEFT JOIN c.jobs j WHERE j.expires_at &gt; :date AND j.is_activated = :activated')
            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
            -&gt;setParameter('activated', 1);

        return $query-&gt;getResult();
    }
}</pre><p>好了，今天就这些了。你可以在浏览器中测试一下今天的实现的内容。所有未被激活的<em>Job</em>数据是不会显示到首页中的，即使你知道URL链接，它们也是不可以被访问到的。不过，如果你知道带<em>token</em>的URL的话就可以访问到，在这种情况下，<em>Job</em>预览页面顶部会显示出<em>admin</em>栏。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/16/symfony2-jobeet-day-10-the-forms/">http://www.intelligentbee.com/blog/2013/08/16/symfony2-jobeet-day-10-the-forms/</a></span></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/366">jobeet第十天:表单</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/366/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
