*这一系列文章来源于Fabien Potencier,基于Symfony1.4编写的Jobeet Tutirual。
在昨天的内容中,我们给Jobeet加上了订阅功能之后,用户就可以实时地接收到最新发布的信息了。
现在我们试着站在发布者的角度来思考,当发布者发布一个Job信息之后,发布者想要让尽可能多的人能够了解到这条信息。如果我们能把这些信息放在很多小网站上,这样一来看的人越多,那么发布者就将有更大几率能够找到适合这个工作的人选了。这就是所谓的长尾效应(long tail)。Affiliates能够在他们的网站上发布最新的Job信息,这些都需要web services的支持,那么我们今天就来实现web services。
Affiliates
就像我们在第二天中内容说的那样,一个Affiliate能够得到所有当前已激活的Job列表。
The fixtures
我们为Affiliates创建一个新的Fixture文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// 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->setUrl('http://sensio-labs.com/'); $affiliate->setEmail('address1@example.com'); $affiliate->setToken('sensio-labs'); $affiliate->setIsActive(true); $affiliate->addCategorie($em->merge($this->getReference('category-programming'))); $em->persist($affiliate); $affiliate = new Affiliate(); $affiliate->setUrl('/'); $affiliate->setEmail('address2@example.org'); $affiliate->setToken('symfony'); $affiliate->setIsActive(false); $affiliate->addCategorie($em->merge($this->getReference('category-programming')), $em->merge($this->getReference('category-design'))); $em->persist($affiliate); $em->flush(); $this->addReference('affiliate', $affiliate); } public function getOrder() { return 3; // This represents the order in which fixtures will be loaded } } |
现在运行下面的命令,我们把定义在Fixture文件中的数据持久化到数据库中:
1 |
php app/console doctrine:fixtures:load |
在Fixture文件中,我们可以看到token是硬编码写上去的,这里的目的是为了方便测试。但当在实际的运行时,当用户需要申请为Affiliate时,这个token将会被自动生成。我们在Affiliate类中创建一个方法来生成token。我们先在ORM文件中的lifecycleCallbacks部分添加setTokenValue方法:
1 2 3 4 |
# src/Ibw/JobeetBundle/Resources/config/doctrine/Affiliate.orm.yml # ... lifecycleCallbacks: prePersist: [ setCreatedAtValue, setTokenValue ] |
运行下面的命令会后,Affiliate类中将会生成setTokenValue()方法:
1 |
php app/console doctrine:generate:entities IbwJobeetBundle |
现在我们来修改这个方法:
1 2 3 4 5 6 7 8 9 10 |
// src/Ibw/JobeetBundle/Entity/Affiliate.php public function setTokenValue() { if(!$this->getToken()) { $token = sha1($this->getEmail().rand(11111, 99999)); $this->token = $token; } return $this; } |
重新加载数据:
1 |
php app/console doctrine:fixtures:load |
The Job Web Service
和以前一样,我们每次创建新资源的的时候,第一件要做的事情就是定义路由,这是个好习惯:
1 2 3 4 5 6 |
# src/Ibw/JobeetBundle/Resources/config/routing.yml IbwJobeetBundle_api: pattern: /api/{token}/jobs.{_format} defaults: {_controller: "IbwJobeetBundle:Api:list"} requirements: _format: xml|json|yaml |
通常我们修改过路由文件之后,我们都需要去清除cache:
1 2 |
php app/console cache:clear --env=dev php app/console cache:clear --env=prod |
我们的下一步是创建api动作和相应的模板,它们会共享相同的动作。我们新建一个控制器文件,并把它命名为“ApiController”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// 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->getDoctrine()->getManager(); $jobs = array(); $rep = $em->getRepository('IbwJobeetBundle:Affiliate'); $affiliate = $rep->getForToken($token); if(!$affiliate) { throw $this->createNotFoundException('This affiliate account does not exist!'); } $rep = $em->getRepository('IbwJobeetBundle:Job'); $active_jobs = $rep->getActiveJobs(null, null, null, $affiliate->getId()); foreach ($active_jobs as $job) { $jobs[$this->get('router')->generate('ibw_job_show', array('company' => $job->getCompanySlug(), 'location' => $job->getLocationSlug(), 'id' => $job->getId(), 'position' => $job->getPositionSlug()), true)] = $job->asArray($request->getHost()); } $format = $request->getRequestFormat(); $jsonData = json_encode($jobs); if ($format == "json") { $headers = array('Content-Type' => 'application/json'); $response = new Response($jsonData, 200, $headers); return $response; } return $this->render('IbwJobeetBundle:Api:jobs.' . $format . '.twig', array('jobs' => $jobs)); } } |
为了能够通过token获得Affiliate的信息,我们需要创建getForToken()方法。这个方法同样会验证Affiliate是否是已激活的,所以我们这里就不需要再判断Affiliate是否已被激活。到现在为止我们还尚未使用过AffiliateRepository,因为它还不存在。我们现在就来创建它,先修改ORM文件:
1 2 3 4 5 |
# src/Ibw/JobeetBundle/Resources/config/doctrine/Affiliate.orm.yml Ibw\JobeetBundle\Entity\Affiliate: type: entity repositoryClass: Ibw\JobeetBundle\Repository\AffiliateRepository # ... |
运行下面的代码:
1 |
php app/console doctrine:generate:entities IbwJobeetBundle |
创建完毕后,我们马上就可以使用它了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// 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->createQueryBuilder('a') ->where('a.is_active = :active') ->setParameter('active', 1) ->andWhere('a.token = :token') ->setParameter('token', $token) ->setMaxResults(1) ; try{ $affiliate = $qb->getQuery()->getSingleResult(); } catch(\Doctrine\Orm\NoResultException $e){ $affiliate = null; } return $affiliate; } } |
当通过token取出对应的Affiliate之后,我们会调用getActiveJobs()方法返回属于某个分类的所有的Job信息给Affiliate。如果你打开JobRepository.php文件,可以看到getActiveJobs()方法没有提供任何的参数选项给Affiliate使用。为了能够重用这个方法,我们需要对它做一些修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// src/Ibw/JobeetBundle/Repository/JobRepository.php // ... public function getActiveJobs($category_id = null, $max = null, $offset = null, $affiliate_id = null) { $qb = $this->createQueryBuilder('j') ->where('j.expires_at > :date') ->setParameter('date', date('Y-m-d H:i:s', time())) ->andWhere('j.is_activated = :activated') ->setParameter('activated', 1) ->orderBy('j.expires_at', 'DESC'); if($max) { $qb->setMaxResults($max); } if($offset) { $qb->setFirstResult($offset); } if($category_id) { $qb->andWhere('j.category = :category_id') ->setParameter('category_id', $category_id); } // j.category c, c.affiliate a if($affiliate_id) { $qb->leftJoin('j.category', 'c') ->leftJoin('c.affiliates', 'a') ->andWhere('a.id = :affiliate_id') ->setParameter('affiliate_id', $affiliate_id) ; } $query = $qb->getQuery(); return $query->getResult(); } // ... |
就如你所看到的,我们调用了asArray()函数来填充jobs数组。我们来定义它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/Ibw/JobeetBundle/Entity/Job.php public function asArray($host) { return array( 'category' => $this->getCategory()->getName(), 'type' => $this->getType(), 'company' => $this->getCompany(), 'logo' => $this->getLogo() ? 'http://' . $host . '/uploads/jobs/' . $this->getLogo() : null, 'url' => $this->getUrl(), 'position' => $this->getPosition(), 'location' => $this->getLocation(), 'description' => $this->getDescription(), 'how_to_apply' => $this->getHowToApply(), 'expires_at' => $this->getCreatedAt()->format('Y-m-d H:i:s'), ); } |
XML格式
支持XML格式很简单,只需要创建一个模板即可:
1 2 3 4 5 6 7 8 9 10 11 |
<!-- src/Ibw/JobeetBundle/Resources/views/Api/Jobs.xml.twig --> <?xml version="1.0" encoding="utf-8"?> <jobs> {% for url, job in jobs %} <job url="{{ url }}"> {% for key,value in job %} <{{ key }}>{{ value }}</{{ key }}> {% endfor %} </job> {% endfor %} </jobs> |
JSON格式
支持JSON格式也是类似的:
1 2 3 4 5 6 7 8 9 10 |
// 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 %} |
YAML格式
1 2 3 4 5 6 7 |
# 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 %} |
如果你想通过一个无效的token来调用web service,那么不论你请求的是什么格式的内容,你都将会得到一个404响应。如果你想看到我们努力的成果,你可以试试访问下面的链接:http://jobeet.local/app_dev.php/api/sensio-labs/jobs.xml或者是http://jobeet.local/app_dev.php/api/symfony/jobs.xml。你可以修改URL中的扩展名来得到指定格式的内容。
测试Web Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// 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->boot(); $this->application = new Application(static::$kernel); // drop the database $command = new DropDatabaseDoctrineCommand(); $this->application->add($command); $input = new ArrayInput(array( 'command' => 'doctrine:database:drop', '--force' => true )); $command->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->application->getKernel()->getContainer()->get('doctrine')->getConnection(); if ($connection->isConnected()) { $connection->close(); } // create the database $command = new CreateDatabaseDoctrineCommand(); $this->application->add($command); $input = new ArrayInput(array( 'command' => 'doctrine:database:create', )); $command->run($input, new NullOutput()); // create schema $command = new CreateSchemaDoctrineCommand(); $this->application->add($command); $input = new ArrayInput(array( 'command' => 'doctrine:schema:create', )); $command->run($input, new NullOutput()); // get the Entity Manager $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); // load fixtures $client = static::createClient(); $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client->getContainer()); $loader->loadFromDirectory(static::$kernel->locateResource('@IbwJobeetBundle/DataFixtures/ORM')); $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this->em); $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this->em, $purger); $executor->execute($loader->getFixtures()); } public function testList() { $client = static::createClient(); $crawler = $client->request('GET', '/api/sensio-labs/jobs.xml'); $this->assertEquals('Ibw\JobeetBundle\Controller\ApiController::listAction', $client->getRequest()->attributes->get('_controller')); $this->assertTrue($crawler->filter('description')->count() == 32); $crawler = $client->request('GET', '/api/sensio-labs87/jobs.xml'); $this->assertTrue(404 === $client->getResponse()->getStatusCode()); $crawler = $client->request('GET', '/api/symfony/jobs.xml'); $this->assertTrue(404 === $client->getResponse()->getStatusCode()); $crawler = $client->request('GET', '/api/sensio-labs/jobs.json'); $this->assertEquals('Ibw\JobeetBundle\Controller\ApiController::listAction', $client->getRequest()->attributes->get('_controller')); $this->assertRegExp('/"category"\:"Programming"/', $client->getResponse()->getContent()); $crawler = $client->request('GET', '/api/sensio-labs87/jobs.json'); $this->assertTrue(404 === $client->getResponse()->getStatusCode()); $crawler = $client->request('GET', '/api/sensio-labs/jobs.yaml'); $this->assertRegExp('/category\: Programming/', $client->getResponse()->getContent()); $this->assertEquals('Ibw\JobeetBundle\Controller\ApiController::listAction', $client->getRequest()->attributes->get('_controller')); $crawler = $client->request('GET', '/api/sensio-labs87/jobs.yaml'); $this->assertTrue(404 === $client->getResponse()->getStatusCode()); } } |
Affiliate申请表单
web service已经可以使用了,现在我们来添加创建Affiliate的表单吧。为了实现这个功能,我们需要写HTML表单,为每个表单域实现验证规则,把表单域的值处理后保存到数据库中,当表单数据有错误时还需要显示出错误信息反馈给用户。
首先我们先创建控制器文件,把它命名为AffiliateCotrolelr:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 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 } |
然后修改layout.html.twig中的链接:
1 2 3 4 |
<!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --> <!-- ... --> <li class="last"><a href="{{ path('ibw_affiliate_new') }}">Become an affiliate</a></li> <!-- ... --> |
现在创建一个action来匹配刚才修改链接的路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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->createForm(new AffiliateType(), $entity); return $this->render('IbwJobeetBundle:Affiliate:affiliate_new.html.twig', array( 'entity' => $entity, 'form' => $form->createView(), )); } } |
我们已经有了路由的名字还有action,但我们还没有路由。所以我们来创建它:
1 2 3 4 |
# src/Ibw/JobeetBundle/Resources/config/routing/affiliate.yml ibw_affiliate_new: pattern: /new defaults: { _controller: "IbwJobeetBundle:Affiliate:new" } |
同样需要在routing.yml文件中加入下面的代码:
1 2 3 4 5 6 |
# src/Ibw/JobeetBundle/Resources/config/routing.yml # ... IbwJobeetBundle_ibw_affiliate: resource: "@IbwJobeetBundle/Resources/config/routing/affiliate.yml" prefix: /affiliate |
表单类同样需要被创建出来。尽管Affiliate有很多的字段域,但我们没有必要把它们全部显示出来,因为有些字段域是不需要用来填写的。我们来创建Affiliate表单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// 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 ->add('url') ->add('email') ->add('categories', null, array('expanded'=>true)) ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Ibw\JobeetBundle\Entity\Affiliate', )); } public function getName() { return 'affiliate'; } } |
现在我们需要验证提交上来的Affiliate表单对象中的数据是否有效。我们在validation.yml文件中添加下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 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: ~ |
在上面的代码中,我们使用了一个新的验证器,叫做UniqueEntity。它能够验证Doctrine实体对象中的一个或者多个特殊字段域是否是唯一的。这个十分常用,例如,我们需要防止一个新注册用户的email地址和已存在用户的email地址重复。
添加了验证约束之后不要忘记清除cache!
最后一步需要创建表单试图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<!-- src/Ibw/JobeetBundle/Resources/views/Affiliate/affiliate_new.html.twig --> {% extends 'IbwJobeetBundle::layout.html.twig' %} {% set form_themes = _self %} {% block form_errors %} {% spaceless %} {% if errors|length > 0 %} <ul class="error_list"> {% for error in errors %} <li>{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}</li> {% endfor %} </ul> {% endif %} {% endspaceless %} {% endblock form_errors %} {% block stylesheets %} {{ parent() }} <link rel="stylesheet" href="{{ asset('bundles/ibwjobeet/css/job.css') }}" type="text/css" media="all" /> {% endblock %} {% block content %} <h1>Become an affiliate</h1> <form action="{{ path('ibw_affiliate_create') }}" method="post" {{ form_enctype(form) }}> <table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Submit" /> </td> </tr> </tfoot> <tbody> <tr> <th>{{ form_label(form.url) }}</th> <td> {{ form_errors(form.url) }} {{ form_widget(form.url) }} </td> </tr> <tr> <th>{{ form_label(form.email) }}</th> <td> {{ form_errors(form.email) }} {{ form_widget(form.email) }} </td> </tr> <tr> <th>{{ form_label(form.categories) }}</th> <td> {{ form_errors(form.categories) }} {{ form_widget(form.categories) }} </td> </tr> </tbody> </table> {{ form_end(form) }} {% endblock %} |
当提交的表单数据如果是有效的,那么表单数据就会被保存到数据库中。我们为AffiliateController添加create动作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// src/Ibw/JobeetBundle/Controller/AffiliateController.php class AffiliateController extends Controller { // ... public function createAction(Request $request) { $affiliate = new Affiliate(); $form = $this->createForm(new AffiliateType(), $affiliate); $form->bind($request); $em = $this->getDoctrine()->getManager(); if ($form->isValid()) { $formData = $request->get('affiliate'); $affiliate->setUrl($formData['url']); $affiliate->setEmail($formData['email']); $affiliate->setIsActive(false); $em->persist($affiliate); $em->flush(); return $this->redirect($this->generateUrl('ibw_affiliate_wait')); } return $this->render('IbwJobeetBundle:Affiliate:affiliate_new.html.twig', array( 'entity' => $affiliate, 'form' => $form->createView(), )); } } |
当表单一旦被提交,那么createAction()就会被执行,所以我们需要定义路由:
1 2 3 4 5 6 7 |
# src/Ibw/JobeetBundle/Resources/config/routing/affiliate.yml # ... ibw_affiliate_create: pattern: /create defaults: { _controller: "IbwJobeetBundle:Affiliate:create" } requirements: { _method: post } |
Affiliate注册之后,他将会被重定向到等待页面。我们来为定义这个动作或试图吧:
1 2 3 4 5 6 7 8 9 10 |
// src/Ibw/JobeetBundle/Controller/AffiliateController.php class AffiliateController extends Controller { // ... public function waitAction() { return $this->render('IbwJobeetBundle:Affiliate:wait.html.twig'); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- src/Ibw/JobeetBundle/Resources/views/Affiliate/wait.html.twig --> {% extends "IbwJobeetBundle::layout.html.twig" %} {% block content %} <div class="content"> <h1>Your affiliate account has been created</h1> <div style="padding: 20px"> Thank you! You will receive an email with your affiliate token as soon as your account will be activated. </div> </div> {% endblock %} |
现在添加路由:
1 2 3 4 5 6 |
# src/Ibw/JobeetBundle/Resources/config/routing/affiliate.yml # ... ibw_affiliate_wait: pattern: /wait defaults: { _controller: "IbwJobeetBundle:Affiliate:wait" } |
定义好路由之后,我们需要清除cache。
现在你可以试着点击首页中的Affiliates链接,你会跳转到Affiliate表单页面。
测试
最后一步是为新功能添加功能测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
// 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->boot(); $this->application = new Application(static::$kernel); // drop the database $command = new DropDatabaseDoctrineCommand(); $this->application->add($command); $input = new ArrayInput(array( 'command' => 'doctrine:database:drop', '--force' => true )); $command->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->application->getKernel()->getContainer()->get('doctrine')->getConnection(); if ($connection->isConnected()) { $connection->close(); } // create the database $command = new CreateDatabaseDoctrineCommand(); $this->application->add($command); $input = new ArrayInput(array( 'command' => 'doctrine:database:create', )); $command->run($input, new NullOutput()); // create schema $command = new CreateSchemaDoctrineCommand(); $this->application->add($command); $input = new ArrayInput(array( 'command' => 'doctrine:schema:create', )); $command->run($input, new NullOutput()); // get the Entity Manager $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); // load fixtures $client = static::createClient(); $loader = new \Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader($client->getContainer()); $loader->loadFromDirectory(static::$kernel->locateResource('@IbwJobeetBundle/DataFixtures/ORM')); $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this->em); $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor($this->em, $purger); $executor->execute($loader->getFixtures()); } public function testAffiliateForm() { $client = static::createClient(); $crawler = $client->request('GET', '/affiliate/new'); $this->assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::newAction', $client->getRequest()->attributes->get('_controller')); $form = $crawler->selectButton('Submit')->form(array( 'affiliate[url]' => 'http://sensio-labs.com/', 'affiliate[email]' => 'jobeet@example.com' )); $client->submit($form); $this->assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::createAction', $client->getRequest()->attributes->get('_controller')); $kernel = static::createKernel(); $kernel->boot(); $em = $kernel->getContainer()->get('doctrine.orm.entity_manager'); $query = $em->createQuery('SELECT count(a.email) FROM IbwJobeetBundle:Affiliate a WHERE a.email = :email'); $query->setParameter('email', 'jobeet@example.com'); $this->assertEquals(1, $query->getSingleScalarResult()); $crawler = $client->request('GET', '/affiliate/new'); $form = $crawler->selectButton('Submit')->form(array( 'affiliate[email]' => 'not.an.email', )); $crawler = $client->submit($form); // check if we have 1 errors $this->assertTrue($crawler->filter('.error_list')->count() == 1); // check if we have error on affiliate_email field $this->assertTrue($crawler->filter('#affiliate_email')->siblings()->first()->filter('.error_list')->count() == 1); } public function testCreate() { $client = static::createClient(); $crawler = $client->request('GET', '/affiliate/new'); $form = $crawler->selectButton('Submit')->form(array( 'affiliate[url]' => 'http://sensio-labs.com/', 'affiliate[email]' => 'address@example.com' )); $client->submit($form); $client->followRedirect(); $this->assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::waitAction', $client->getRequest()->attributes->get('_controller')); return $client; } public function testWait() { $client = static::createClient(); $crawler = $client->request('GET', '/affiliate/wait'); $this->assertEquals('Ibw\JobeetBundle\Controller\AffiliateController::waitAction', $client->getRequest()->attributes->get('_controller')); } } |
Affiliate后台管理
对于后台,我们会使用SonataAdminBundle。就像我们之前说过的那样,Affiliate注册之后需要等待admin来激活他。所以,当admin访问Affiliate页面后,他只能看到激活或者不激活这两个操作,这样有利于提高工作效率嘛。
首先,你需要在services.yml中声明Affiliate服务:
1 2 3 4 5 6 7 8 9 10 |
# 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' |
然后创建Admin文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// 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' => 'ASC', '_sort_by' => 'is_active' ); protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('email') ->add('url') ; } protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('email') ->add('is_active'); } protected function configureListFields(ListMapper $listMapper) { $listMapper ->add('is_active') ->addIdentifier('email') ->add('url') ->add('created_at') ->add('token') ; } } |
为辅助管理员工作,我们想要只显示出未激活的Affiliate。我们能够通过过滤is_active值为false的Affiliate来实现这个功能:
1 2 3 4 5 6 7 8 9 |
// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php // ... protected $datagridValues = array( '_sort_order' => 'ASC', '_sort_by' => 'is_active', 'is_active' => array('value' => 2) // The value 2 represents that the displayed affiliate accounts are not activated yet ); // ... |
现在我们创建AffiliateAdminController:
1 2 3 4 5 6 7 8 9 10 11 |
// 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 } |
我们来创建activate和deactivate批量操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
// 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->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) { throw new AccessDeniedException(); } $request = $this->get('request'); $modelManager = $this->admin->getModelManager(); $selectedModels = $selectedModelQuery->execute(); try { foreach($selectedModels as $selectedModel) { $selectedModel->activate(); $modelManager->update($selectedModel); } } catch(\Exception $e) { $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage()); return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } $this->get('session')->getFlashBag()->add('sonata_flash_success', sprintf('The selected accounts have been activated')); return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } public function batchActionDeactivate(ProxyQueryInterface $selectedModelQuery) { if($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) { throw new AccessDeniedException(); } $request = $this->get('request'); $modelManager = $this->admin->getModelManager(); $selectedModels = $selectedModelQuery->execute(); try { foreach($selectedModels as $selectedModel) { $selectedModel->deactivate(); $modelManager->update($selectedModel); } } catch(\Exception $e) { $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage()); return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } $this->get('session')->getFlashBag()->add('sonata_flash_success', sprintf('The selected accounts have been deactivated')); return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } } |
为了能够让批量操作生效,我们把它添加到Admin::getBatchActions()方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php class AffiliateAdmin extends Admin { // ... public function getBatchActions() { $actions = parent::getBatchActions(); if($this->hasRoute('edit') && $this->isGranted('EDIT') && $this->hasRoute('delete') && $this->isGranted('DELETE')) { $actions['activate'] = array( 'label' => 'Activate', 'ask_confirmation' => true ); $actions['deactivate'] = array( 'label' => 'Deactivate', 'ask_confirmation' => true ); } return $actions; } } |
在此我们还需要给Affiliate实体添加两个方法:activate()和deactivate():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// src/Ibw/JobeetBundle/Entity/Affiliate.php // ... public function activate() { if(!$this->getIsActive()) { $this->setIsActive(true); } return $this->is_active; } public function deactivate() { if($this->getIsActive()) { $this->setIsActive(false); } return $this->is_active; } |
现在我们为每条Affiliate信息都创建两个独立的动作,activate和deactivate。首先我们先为它们创建路由,这就是为什么我们的Admin类中重写了configureRoutes函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php use Sonata\AdminBundle\Route\RouteCollection; class AffiliateAdmin extends Admin { // ... protected function configureRoutes(RouteCollection $collection) { parent::configureRoutes($collection); $collection->add('activate', $this->getRouterIdParameter().'/activate') ; $collection->add('deactivate', $this->getRouterIdParameter().'/deactivate') ; } } |
现在我们在AdminController中实现这两个动作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// src/Ibw/JobeetBundle/Controller/AffiliateAdminController.php class AffiliateAdminController extends Controller { // ... public function activateAction($id) { if($this->admin->isGranted('EDIT') === false) { throw new AccessDeniedException(); } $em = $this->getDoctrine()->getManager(); $affiliate = $em->getRepository('IbwJobeetBundle:Affiliate')->findOneById($id); try { $affiliate->setIsActive(true); $em->flush(); } catch(\Exception $e) { $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage()); return new RedirectResponse($this->admin->generateUrl('list', $this->admin->getFilterParameters())); } return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } public function deactivateAction($id) { if($this->admin->isGranted('EDIT') === false) { throw new AccessDeniedException(); } $em = $this->getDoctrine()->getManager(); $affiliate = $em->getRepository('IbwJobeetBundle:Affiliate')->findOneById($id); try { $affiliate->setIsActive(false); $em->flush(); } catch(\Exception $e) { $this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage()); return new RedirectResponse($this->admin->generateUrl('list', $this->admin->getFilterParameters())); } return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } } |
现在为新添加的action创建模板:
1 2 3 4 5 6 7 |
<!-- src/Ibw/JobeetBundle/Resources/views/AffiliateAdmin/list__action_activate.html.twig --> {% if admin.isGranted('EDIT', object) and admin.hasRoute('activate') %} <a href="{{ admin.generateObjectUrl('activate', object) }}" class="btn edit_link" title="{{ 'action_activate'|trans({}, 'SonataAdminBundle') }}"> <i class="icon-edit"></i> {{ 'activate'|trans({}, 'SonataAdminBundle') }} </a> {% endif %} |
1 2 3 4 5 6 7 |
<!-- src/Ibw/JobeetBundle/Resources/views/AffiliateAdmin/list__action_deactivate.html.twig --> {% if admin.isGranted('EDIT', object) and admin.hasRoute('deactivate') %} <a href="{{ admin.generateObjectUrl('deactivate', object) }}" class="btn edit_link" title="{{ 'action_deactivate'|trans({}, 'SonataAdminBundle') }}"> <i class="icon-edit"></i> {{ 'deactivate'|trans({}, 'SonataAdminBundle') }} </a> {% endif %} |
在AffiliateAdmin::configureListFields()方法中添加新的action和button后,我们就能在页面上看到每条Affiliate信息都把它们显示出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/Ibw/JobeetBundle/Admin/AffiliateAdmin.php class AffiliateAdmin extends Admin { // ... protected function configureListFields(ListMapper $listMapper) { $listMapper ->add('is_active') ->addIdentifier('email') ->add('url') ->add('created_at') ->add('token') ->add('_action', 'actions', array( 'actions' => array('activate' => array('template' => 'IbwJobeetBundle:AffiliateAdmin:list__action_activate.html.twig'), 'deactivate' => array('template' => 'IbwJobeetBundle:AffiliateAdmin:list__action_deactivate.html.twig')))) ; } /// ... } |
好的,马上清除掉cache来试试吧!
今天我们就到这了,明天我们会讲解发送邮件,当Affiliate被激活后将收到一封邮件。
原文链接:http://www.intelligentbee.com/blog/2013/08/21/symfony2-jobeet-day-15-web-services/
One comment