<?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; web services</title>
	<atom:link href="http://www.newlifeclan.com/symfony/archives/tag/web-services/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第十五天: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>
	</channel>
</rss>
