{"id":2268,"date":"2013-08-21T07:14:03","date_gmt":"2013-08-21T07:14:03","guid":{"rendered":"https:\/\/intelligentbee.com\/blog\/?p=2268"},"modified":"2024-09-30T07:39:24","modified_gmt":"2024-09-30T07:39:24","slug":"symfony2-jobeet-day-15-web-services","status":"publish","type":"post","link":"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/","title":{"rendered":"Symfony2 Jobeet Day 15: Web Services"},"content":{"rendered":"<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_68_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title \" >Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#_This_article_is_part_of_the_original_Jobeet_Tutorial_created_by_Fabien_Potencier_for_Symfony_14\" title=\"* This article is part of the original Jobeet Tutorial, created by Fabien Potencier, for Symfony 1.4.\">* This article is part of the original Jobeet Tutorial, created by Fabien Potencier, for Symfony 1.4.<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#Web_services_in_jobeet\" title=\"Web services in jobeet\">Web services in jobeet<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#Affiliates\" title=\"Affiliates\">Affiliates<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#The_fixtures\" title=\"The fixtures\">The fixtures<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#The_Affiliate_Application_Form\" title=\"The Affiliate Application Form\">The Affiliate Application Form<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#Tests\" title=\"Tests\">Tests<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-15-web-services\/#The_Affiliate_Backend\" title=\"The Affiliate Backend\">The Affiliate Backend<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"_This_article_is_part_of_the_original_Jobeet_Tutorial_created_by_Fabien_Potencier_for_Symfony_14\"><\/span><span style=\"font-family: timesnew roman; font-size: 12px;\">* This article is part of the original <a href=\"http:\/\/symfony.com\/legacy\/doc\/jobeet?orm=Doctrine\" target=\"_blank\" rel=\"noopener\">Jobeet Tutorial<\/a>, created by Fabien Potencier, for Symfony 1.4.<\/span><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h2><span class=\"ez-toc-section\" id=\"Web_services_in_jobeet\"><\/span>Web services in jobeet<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>With the addition of feeds on Jobeet, job seekers can now be informed of new jobs in real-time.<\/p>\n<p>On the other side of the fence, when you post a job, you will want to have the greatest exposure possible. If your job is syndicated on a lot of small websites, you will have a better chance to find the right person. That\u2019s the power of the long tail. Affiliates will be able to publish the latest posted jobs on their websites thanks to the web services we will develop today.<span id=\"more-192\"><\/span><\/p>\n<h2><span class=\"ez-toc-section\" id=\"Affiliates\"><\/span>Affiliates<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As we already said in day 2 of this tutorial, an affiliate retrieves the current active job list.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_fixtures\"><\/span>The fixtures<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let\u2019s create a new <code>fixture<\/code> file for the affiliates:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/DataFixtures\/ORM\/LoadAffiliateData.php\">namespace IbwJobeetBundleDataFixturesORM;\r\n\r\nuse DoctrineCommonPersistenceObjectManager;\r\nuse DoctrineCommonDataFixturesAbstractFixture;\r\nuse DoctrineCommonDataFixturesOrderedFixtureInterface;\r\nuse IbwJobeetBundleEntityAffiliate;\r\n\r\nclass LoadAffiliateData extends AbstractFixture implements OrderedFixtureInterface\r\n{\r\n    public function load(ObjectManager $em)\r\n    {\r\n        $affiliate = new Affiliate();\r\n\r\n        $affiliate-&gt;setUrl('http:\/\/sensio-labs.com\/');\r\n        $affiliate-&gt;setEmail('address1@example.com');\r\n        $affiliate-&gt;setToken('sensio-labs');\r\n        $affiliate-&gt;setIsActive(true);\r\n        $affiliate-&gt;addCategorie($em-&gt;merge($this-&gt;getReference('category-programming')));\r\n\r\n        $em-&gt;persist($affiliate);\r\n\r\n        $affiliate = new Affiliate();\r\n\r\n        $affiliate-&gt;setUrl('\/');\r\n        $affiliate-&gt;setEmail('address2@example.org');\r\n        $affiliate-&gt;setToken('symfony');\r\n        $affiliate-&gt;setIsActive(false);\r\n        $affiliate-&gt;addCategorie($em-&gt;merge($this-&gt;getReference('category-programming')), $em-&gt;merge($this-&gt;getReference('category-design')));\r\n\r\n        $em-&gt;persist($affiliate);\r\n        $em-&gt;flush();\r\n\r\n        $this-&gt;addReference('affiliate', $affiliate);\r\n    }\r\n\r\n    public function getOrder()\r\n    {\r\n        return 3; \/\/ This represents the order in which fixtures will be loaded\r\n    }\r\n}<\/pre>\n<p>Now, to persist the data defined in your fixture file, just run the following command:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:fixtures:load<\/pre>\n<p>In the fixture file, the tokens are hardcoded to simplify the testing, but when an actual user applies for an account, the token will need to be generated Let\u2019s create a function to do that in our <code>Affiliate<\/code> class. Start by adding the <code>setTokenValue<\/code> method to <code>lifecycleCallbacks <\/code>section, inside your <code>ORM<\/code> file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Affiliate.orm.yml\"># ... \r\n    lifecycleCallbacks:\r\n        prePersist: [ setCreatedAtValue, setTokenValue ]<\/pre>\n<p>Now, the <code>setTokenValue<\/code> method will be generated inside the entity file when you will run the following command:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:generate:entities IbwJobeetBundle<\/pre>\n<p>Let\u2019s modify the method now:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Affiliate.php\">    public function setTokenValue()\r\n    {\r\n        if(!$this-&gt;getToken()) {\r\n            $token = sha1($this-&gt;getEmail().rand(11111, 99999));\r\n            $this-&gt;token = $token;\r\n        }\r\n\r\n        return $this;\r\n    }<\/pre>\n<p>Reload the data:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:fixtures:load<\/pre>\n<h3>The Job Web Service<\/h3>\n<p>As always, when you create a new resource, it\u2019s a good habbit to define the route first:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing.yml\">IbwJobeetBundle_api:\r\n    pattern: \/api\/{token}\/jobs.{_format}\r\n    defaults: {_controller: \"IbwJobeetBundle:Api:list\"}\r\n    requirements:\r\n        _format: xml|json|yaml<\/pre>\n<p>As usually, after you modify a <code>routing<\/code> file, you need to clear the cache:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console cache:clear --env=dev\r\nphp app\/console cache:clear --env=prod<\/pre>\n<p>The next step is to create the <code>api<\/code> action and the templates, that will share the same action. Let us now create a new controller file, called <code>ApiController<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/ApiController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SymfonyBundleFrameworkBundleControllerController;\r\nuse SymfonyComponentHttpFoundationRequest;\r\nuse SymfonyComponentHttpFoundationResponse;\r\nuse IbwJobeetBundleEntityAffiliate;\r\nuse IbwJobeetBundleEntityJob;\r\nuse IbwJobeetBundleRepositoryAffiliateRepository;\r\n\r\nclass ApiController extends Controller\r\n{\r\n    public function listAction(Request $request, $token)\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $jobs = array();\r\n\r\n        $rep = $em-&gt;getRepository('IbwJobeetBundle:Affiliate');\r\n        $affiliate = $rep-&gt;getForToken($token);\r\n\r\n        if(!$affiliate) { \r\n            throw $this-&gt;createNotFoundException('This affiliate account does not exist!');\r\n        }\r\n\r\n        $rep = $em-&gt;getRepository('IbwJobeetBundle:Job');\r\n        $active_jobs = $rep-&gt;getActiveJobs(null, null, null, $affiliate-&gt;getId());\r\n\r\n        foreach ($active_jobs as $job) {\r\n            $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());\r\n        }\r\n\r\n        $format = $request-&gt;getRequestFormat();\r\n        $jsonData = json_encode($jobs);\r\n\r\n        if ($format == \"json\") {\r\n            $headers = array('Content-Type' =&gt; 'application\/json'); \r\n            $response = new Response($jsonData, 200, $headers);\r\n\r\n            return $response;\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Api:jobs.' . $format . '.twig', array('jobs' =&gt; $jobs));  \r\n    }\r\n}<\/pre>\n<p>To retrieve the affiliate using his token, we will create the <code>getForToken()<\/code> method. This method also verifies if the affiliate account is activated, so there is no need for us to check this one more time. Until now, we haven\u2019t used the <code>AffiliateRepository yet<\/code>, so it doesn\u2019t exist. To create it, modify the ORM file as following, then run the command you used before to generate the entities.<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Affiliate.orm.yml\">IbwJobeetBundleEntityAffiliate:\r\n    type: entity\r\n    repositoryClass: IbwJobeetBundleRepositoryAffiliateRepository\r\n    # ...<\/pre>\n<p>Once created, it is ready to be used:<\/p>\n<pre class=\"lang:php decode:true\" title=\"src\/Ibw\/JobeetBundle\/Repository\/AffiliateRepository.php\">namespace IbwJobeetBundleRepository;\r\n\r\nuse DoctrineORMEntityRepository;\r\n\r\n\/**\r\n * AffiliateRepository\r\n *\r\n * This class was generated by the Doctrine ORM. Add your own custom\r\n * repository methods below.\r\n *\/\r\nclass AffiliateRepository extends EntityRepository\r\n{\r\n    public function getForToken($token)\r\n    {\r\n        $qb = $this-&gt;createQueryBuilder('a')\r\n            -&gt;where('a.is_active = :active')\r\n            -&gt;setParameter('active', 1)\r\n            -&gt;andWhere('a.token = :token')\r\n            -&gt;setParameter('token', $token)\r\n            -&gt;setMaxResults(1)\r\n        ;\r\n\r\n        try{\r\n            $affiliate = $qb-&gt;getQuery()-&gt;getSingleResult();\r\n        } catch(DoctrineOrmNoResultException $e){\r\n            $affiliate = null;\r\n        }\r\n\r\n        return $affiliate;\r\n    }\r\n}<\/pre>\n<p>After identifying the affiliate by his token, we will use the <code>getActiveJobs()<\/code> method to give the affiliate the jobs he required, belonging to the selected categories. If you open your <code>JobRepository<\/code> file now, you will see that the <code>getActiveJobs()<\/code> method doesn\u2019t share any connection with the affiliates. Because we want to reuse that method, we need to make some modifications inside of it:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/JobRepository.php\">\/\/ ...\r\n\r\n    public function getActiveJobs($category_id = null, $max = null, $offset = null, $affiliate_id = null)\r\n    {\r\n        $qb = $this-&gt;createQueryBuilder('j')\r\n            -&gt;where('j.expires_at &gt; :date')\r\n            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))\r\n            -&gt;andWhere('j.is_activated = :activated')\r\n            -&gt;setParameter('activated', 1)\r\n            -&gt;orderBy('j.expires_at', 'DESC');\r\n\r\n        if($max) {\r\n            $qb-&gt;setMaxResults($max);\r\n        }\r\n\r\n        if($offset) {\r\n            $qb-&gt;setFirstResult($offset);\r\n        }\r\n\r\n        if($category_id) {\r\n            $qb-&gt;andWhere('j.category = :category_id')\r\n                -&gt;setParameter('category_id', $category_id);\r\n        }\r\n        \/\/ j.category c, c.affiliate a\r\n        if($affiliate_id) {\r\n            $qb-&gt;leftJoin('j.category', 'c')\r\n               -&gt;leftJoin('c.affiliates', 'a')\r\n               -&gt;andWhere('a.id = :affiliate_id')\r\n               -&gt;setParameter('affiliate_id', $affiliate_id)\r\n            ;\r\n        }\r\n\r\n        $query = $qb-&gt;getQuery();\r\n\r\n        return $query-&gt;getResult();\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>As you can see, we populate the jobs array using a function called <code>asArray()<\/code>. Let\u2019s define it:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">public function asArray($host)\r\n{\r\n    return array(\r\n        'category'     =&gt; $this-&gt;getCategory()-&gt;getName(),\r\n        'type'         =&gt; $this-&gt;getType(),\r\n        'company'      =&gt; $this-&gt;getCompany(),\r\n        'logo'         =&gt; $this-&gt;getLogo() ? 'http:\/\/' . $host . '\/uploads\/jobs\/' . $this-&gt;getLogo() : null,\r\n        'url'          =&gt; $this-&gt;getUrl(),\r\n        'position'     =&gt; $this-&gt;getPosition(),\r\n        'location'     =&gt; $this-&gt;getLocation(),\r\n        'description'  =&gt; $this-&gt;getDescription(),\r\n        'how_to_apply' =&gt; $this-&gt;getHowToApply(),\r\n        'expires_at'   =&gt; $this-&gt;getCreatedAt()-&gt;format('Y-m-d H:i:s'),\r\n    );\r\n}<\/pre>\n<h3>The xml Format<\/h3>\n<p>Supporting the <code>xml<\/code> format is as simple as creating a template:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Api\/jobs.xml.twig\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;jobs&gt;\r\n{% for url, job in jobs %}\r\n    &lt;job url=\"{{ url }}\"&gt;\r\n{% for key,value in job %}\r\n        &lt;{{ key }}&gt;{{ value }}&lt;\/{{ key }}&gt;\r\n{% endfor %}\r\n    &lt;\/job&gt;\r\n{% endfor %}\r\n&lt;\/jobs&gt;<\/pre>\n<h3>The json Format<\/h3>\n<p>Support the <code>JSON<\/code> format is similar:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Api\/jobs.json.twig\">{% for url, job in jobs %}\r\n{% i = 0, count(jobs), ++i %}\r\n[\r\n    \"url\":\"{{ url }}\",\r\n{% for key, value in job %} {% j = 0, count(key), ++j %}\r\n    \"{{ key }}\":\"{% if j == count(key)%} {{ json_encode(value) }}, {% else %} {{ json_encode(value) }}\r\n                 {% endif %}\"\r\n{% endfor %}]\r\n{% endfor %}<\/pre>\n<h3>The yaml Format<\/h3>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Api\/jobs.yaml.twig\">{% for url,job in jobs %}\r\n    Url: {{ url }}\r\n{% for key, value in job %}\r\n        {{ key }}: {{ value }}\r\n{% endfor %}\r\n{% endfor %}<\/pre>\n<p>If you try to call the web service with a non-valid token, you will receive a <code>404 page<\/code> as a response, for all the formats. To see what you accomplished until now, access the following links: http:\/\/jobeet.local\/app_dev.php\/api\/sensio-labs\/jobs.xml or http:\/\/jobeet.local\/app_dev.php\/api\/symfony\/jobs.xml. Change the extension in the URL, depending on which format you prefer.<\/p>\n<h3>Web Service Tests<\/h3>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Tests\/Controller\/ApiControllerTest.php\">namespace IbwJobeetBundleTestsController;\r\n\r\nuse SymfonyBundleFrameworkBundleTestWebTestCase;\r\nuse SymfonyBundleFrameworkBundleConsoleApplication;\r\nuse SymfonyComponentConsoleOutputNullOutput;\r\nuse SymfonyComponentConsoleInputArrayInput;\r\nuse DoctrineBundleDoctrineBundleCommandDropDatabaseDoctrineCommand;\r\nuse DoctrineBundleDoctrineBundleCommandCreateDatabaseDoctrineCommand;\r\nuse DoctrineBundleDoctrineBundleCommandProxyCreateSchemaDoctrineCommand;\r\nuse SymfonyComponentDomCrawlerCrawler;\r\nuse SymfonyComponentHttpFoundationHttpExceptionInterface;\r\n\r\nclass ApiControllerTest extends WebTestCase\r\n{\r\n    private $em;\r\n\r\n    private $application;\r\n\r\n    public function setUp()\r\n    {\r\n        static::$kernel = static::createKernel();\r\n        static::$kernel-&gt;boot();\r\n\r\n        $this-&gt;application = new Application(static::$kernel);\r\n\r\n        \/\/ drop the database\r\n        $command = new DropDatabaseDoctrineCommand();\r\n        $this-&gt;application-&gt;add($command);\r\n        $input = new ArrayInput(array(\r\n            'command' =&gt; 'doctrine:database:drop',\r\n            '--force' =&gt; true\r\n        ));\r\n        $command-&gt;run($input, new NullOutput());\r\n\r\n        \/\/ we have to close the connection after dropping the database so we don't get \"No database selected\" error\r\n        $connection = $this-&gt;application-&gt;getKernel()-&gt;getContainer()-&gt;get('doctrine')-&gt;getConnection();\r\n        if ($connection-&gt;isConnected()) {\r\n            $connection-&gt;close();\r\n        }\r\n\r\n        \/\/ create the database\r\n        $command = new CreateDatabaseDoctrineCommand();\r\n        $this-&gt;application-&gt;add($command);\r\n        $input = new ArrayInput(array(\r\n            'command' =&gt; 'doctrine:database:create',\r\n        ));\r\n        $command-&gt;run($input, new NullOutput());\r\n\r\n        \/\/ create schema\r\n        $command = new CreateSchemaDoctrineCommand();\r\n        $this-&gt;application-&gt;add($command);\r\n        $input = new ArrayInput(array(\r\n            'command' =&gt; 'doctrine:schema:create',\r\n        ));\r\n        $command-&gt;run($input, new NullOutput());\r\n\r\n        \/\/ get the Entity Manager\r\n        $this-&gt;em = static::$kernel-&gt;getContainer()\r\n            -&gt;get('doctrine')\r\n            -&gt;getManager();\r\n\r\n        \/\/ load fixtures\r\n        $client = static::createClient();\r\n        $loader = new SymfonyBridgeDoctrineDataFixturesContainerAwareLoader($client-&gt;getContainer());\r\n        $loader-&gt;loadFromDirectory(static::$kernel-&gt;locateResource('@IbwJobeetBundle\/DataFixtures\/ORM'));\r\n        $purger = new DoctrineCommonDataFixturesPurgerORMPurger($this-&gt;em);\r\n        $executor = new DoctrineCommonDataFixturesExecutorORMExecutor($this-&gt;em, $purger);\r\n        $executor-&gt;execute($loader-&gt;getFixtures());\r\n    }\r\n\r\n    public function testList()\r\n    {\r\n        $client = static::createClient();\r\n        $crawler = $client-&gt;request('GET', '\/api\/sensio-labs\/jobs.xml');\r\n\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerApiController::listAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n        $this-&gt;assertTrue($crawler-&gt;filter('description')-&gt;count() == 32);\r\n\r\n        $crawler = $client-&gt;request('GET', '\/api\/sensio-labs87\/jobs.xml');\r\n\r\n        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());\r\n\r\n        $crawler = $client-&gt;request('GET', '\/api\/symfony\/jobs.xml');\r\n\r\n        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());\r\n\r\n        $crawler = $client-&gt;request('GET', '\/api\/sensio-labs\/jobs.json');\r\n\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerApiController::listAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n        $this-&gt;assertRegExp('\/\"category\":\"Programming\"\/', $client-&gt;getResponse()-&gt;getContent());\r\n\r\n        $crawler = $client-&gt;request('GET', '\/api\/sensio-labs87\/jobs.json');\r\n\r\n        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());\r\n\r\n        $crawler = $client-&gt;request('GET', '\/api\/sensio-labs\/jobs.yaml');\r\n        $this-&gt;assertRegExp('\/category: Programming\/', $client-&gt;getResponse()-&gt;getContent());\r\n\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerApiController::listAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n\r\n        $crawler = $client-&gt;request('GET', '\/api\/sensio-labs87\/jobs.yaml');\r\n\r\n        $this-&gt;assertTrue(404 === $client-&gt;getResponse()-&gt;getStatusCode());\r\n    }\r\n}<\/pre>\n<p>Inside the <code>ApiControllerTest<\/code> file, we test that the request formats are correctly received and the pages requested are correctly returned.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Affiliate_Application_Form\"><\/span>The Affiliate Application Form<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Now that the web service is ready to be used, let\u2019s create the account <code>creation form<\/code> for affiliates. For that, you need to write the HTML form, implement validation rules for each field, process the values to store them in a database, display error messages and repopulate fields in case of errors.<\/p>\n<p>First, create a new controller file, named <code>AffiliateController<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/AffiliateController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SymfonyBundleFrameworkBundleControllerController;\r\nuse IbwJobeetBundleEntityAffiliate;\r\nuse IbwJobeetBundleFormAffiliateType;\r\nuse SymfonyComponentHttpFoundationRequest;\r\nuse IbwJobeetBundleEntityCategory;\r\n\r\nclass AffiliateController extends Controller\r\n{\r\n    \/\/ Your code goes here\r\n}<\/pre>\n<p>Then, change the <code>Affiliates<\/code> link in the layout:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/layout.html.twig\">&lt;!-- ... --&gt;\r\n    &lt;li class=\"last\"&gt;&lt;a href=\"{{ path('ibw_affiliate_new') }}\"&gt;Become an affiliate&lt;\/a&gt;&lt;\/li&gt;\r\n&lt;!-- ... --&gt;<\/pre>\n<p>Now, we need to create an action to match the route from the link you just modified it earlier:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/AffiliateController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SymfonyBundleFrameworkBundleControllerController;\r\nuse IbwJobeetBundleEntityAffiliate;\r\nuse IbwJobeetBundleFormAffiliateType;\r\nuse SymfonyComponentHttpFoundationRequest;\r\nuse IbwJobeetBundleEntityCategory;\r\n\r\nclass AffiliateController extends Controller\r\n{\r\n    public function newAction()\r\n    {\r\n        $entity = new Affiliate();\r\n        $form = $this-&gt;createForm(new AffiliateType(), $entity);\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Affiliate:affiliate_new.html.twig', array(\r\n            'entity' =&gt; $entity,\r\n            'form'   =&gt; $form-&gt;createView(),\r\n        ));\r\n    }\r\n}<\/pre>\n<p>We have the name of the route, we have the action, but we do not have the route. so let\u2019s create it:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing\/affiliate.yml\">ibw_affiliate_new:\r\n    pattern:  \/new\r\n    defaults: { _controller: \"IbwJobeetBundle:Affiliate:new\" }<\/pre>\n<p>Also, add this to your <code>routing<\/code> file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing.yml\"># ...\r\n\r\nIbwJobeetBundle_ibw_affiliate:\r\n    resource: \"@IbwJobeetBundle\/Resources\/config\/routing\/affiliate.yml\"\r\n    prefix:   \/affiliate<\/pre>\n<p>The form file also needs to be created. But, even if the <code>Affiliate<\/code> has more fields, we won\u2019t display them all, because some of them must not be editable by the end user. Create your <code>Affiliate<\/code> form:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Form\/AffiliateType.php\">namespace IbwJobeetBundleForm;\r\n\r\nuse SymfonyComponentFormAbstractType;\r\nuse SymfonyComponentFormFormBuilderInterface;\r\nuse SymfonyComponentOptionsResolverOptionsResolverInterface;\r\nuse IbwJobeetBundleEntityAffiliate;\r\nuse IbwJobeetBundleEntityCategory;\r\n\r\nclass AffiliateType extends AbstractType\r\n{\r\n    public function buildForm(FormBuilderInterface $builder, array $options)\r\n    {\r\n        $builder\r\n            -&gt;add('url')\r\n            -&gt;add('email')\r\n            -&gt;add('categories', null, array('expanded'=&gt;true))\r\n        ;\r\n    }\r\n\r\n    public function setDefaultOptions(OptionsResolverInterface $resolver)\r\n    {\r\n        $resolver-&gt;setDefaults(array(\r\n            'data_class' =&gt; 'IbwJobeetBundleEntityAffiliate',\r\n        ));\r\n    }\r\n\r\n    public function getName()\r\n    {\r\n        return 'affiliate';\r\n    }\r\n}<\/pre>\n<p>Now, we need to decide whether or not the <code>Affiliate<\/code> object is valid after the form has applied the submitted data to it. To do this, add the following code to your <code>validation<\/code> file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/validation.yml\"># ...\r\n\r\nIbwJobeetBundleEntityAffiliate:\r\n    constraints:\r\n        - SymfonyBridgeDoctrineValidatorConstraintsUniqueEntity: email\r\n    properties:\r\n        url:\r\n            - Url: ~\r\n        email:\r\n            - NotBlank: ~\r\n            - Email: ~<\/pre>\n<p>In the validation schema, we used a new validator, called <code>UniqueEntity<\/code>. It validates that a particular field (or fields) in a Doctrine entity is (are) unique. This is commonly used, for example, to prevent a new user to register using an email address that already exists in the system.<\/p>\n<p>Don\u2019t forget to clear your cache after applying the validation constraints!<\/p>\n<p>Finally, let\u2019s create the view for the form too:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Affiliate\/affiliate_new.html.twig\">{% extends 'IbwJobeetBundle::layout.html.twig' %}\r\n\r\n{% set form_themes = _self %}\r\n\r\n{% block form_errors %}\r\n{% spaceless %}\r\n    {% if errors|length &gt; 0 %}\r\n        &lt;ul class=\"error_list\"&gt;\r\n            {% for error in errors %}\r\n                &lt;li&gt;{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}&lt;\/li&gt;\r\n            {% endfor %}\r\n        &lt;\/ul&gt;\r\n    {% endif %}\r\n{% endspaceless %}\r\n{% endblock form_errors %}\r\n\r\n{% block stylesheets %}\r\n    {{ parent() }}\r\n    &lt;link rel=\"stylesheet\" href=\"{{ asset('bundles\/ibwjobeet\/css\/job.css') }}\" type=\"text\/css\" media=\"all\" \/&gt;\r\n{% endblock %}\r\n\r\n{% block content %}\r\n    &lt;h1&gt;Become an affiliate&lt;\/h1&gt;\r\n        &lt;form action=\"{{ path('ibw_affiliate_create') }}\" method=\"post\" {{ form_enctype(form) }}&gt;\r\n            &lt;table id=\"job_form\"&gt;\r\n                &lt;tfoot&gt;\r\n                    &lt;tr&gt;\r\n                        &lt;td colspan=\"2\"&gt;\r\n                            &lt;input type=\"submit\" value=\"Submit\" \/&gt;\r\n                        &lt;\/td&gt;\r\n                    &lt;\/tr&gt;\r\n                &lt;\/tfoot&gt;\r\n                &lt;tbody&gt;\r\n                    &lt;tr&gt;\r\n                        &lt;th&gt;{{ form_label(form.url) }}&lt;\/th&gt;\r\n                        &lt;td&gt;\r\n                            {{ form_errors(form.url) }}\r\n                            {{ form_widget(form.url) }}\r\n                        &lt;\/td&gt;\r\n                    &lt;\/tr&gt;\r\n                    &lt;tr&gt;\r\n                        &lt;th&gt;{{ form_label(form.email) }}&lt;\/th&gt;\r\n                        &lt;td&gt;\r\n                            {{ form_errors(form.email) }}\r\n                            {{ form_widget(form.email) }}\r\n                        &lt;\/td&gt;\r\n                    &lt;\/tr&gt;\r\n                    &lt;tr&gt;\r\n                        &lt;th&gt;{{ form_label(form.categories) }}&lt;\/th&gt;\r\n                        &lt;td&gt;\r\n                            {{ form_errors(form.categories) }}\r\n                            {{ form_widget(form.categories) }}\r\n                        &lt;\/td&gt;\r\n                    &lt;\/tr&gt;\r\n                &lt;\/tbody&gt;\r\n            &lt;\/table&gt;\r\n        {{ form_end(form) }}\r\n{% endblock %}<\/pre>\n<p>When the user submits a form, the form data must be persisted into database, if valid. Add the new <code>create<\/code> action to your <code>Affiliate<\/code> controller:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/AffiliateController.php\">class AffiliateController extends Controller \r\n{\r\n    \/\/ ...    \r\n\r\n    public function createAction(Request $request)\r\n    {\r\n        $affiliate = new Affiliate();\r\n        $form = $this-&gt;createForm(new AffiliateType(), $affiliate);\r\n        $form-&gt;bind($request);\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        if ($form-&gt;isValid()) {\r\n\r\n            $formData = $request-&gt;get('affiliate');\r\n            $affiliate-&gt;setUrl($formData['url']);\r\n            $affiliate-&gt;setEmail($formData['email']);\r\n            $affiliate-&gt;setIsActive(false);\r\n\r\n            $em-&gt;persist($affiliate);\r\n            $em-&gt;flush();\r\n\r\n            return $this-&gt;redirect($this-&gt;generateUrl('ibw_affiliate_wait'));\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Affiliate:affiliate_new.html.twig', array(\r\n            'entity' =&gt; $affiliate,\r\n            'form'   =&gt; $form-&gt;createView(),\r\n        ));\r\n    }\r\n}<\/pre>\n<p>When submitting, the create action is performed, so we need to define the route:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing\/affiliate.yml\"># ...\r\n\r\nibw_affiliate_create:\r\n    pattern: \/create\r\n    defaults: { _controller: \"IbwJobeetBundle:Affiliate:create\" }\r\n    requirements: { _method: post }<\/pre>\n<p>After the affiliate registers, he is redirected to a waiting page. Let\u2019s define that action and create the view too:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/AffiliateController.php\">class AffiliateController extends Controller\r\n{\r\n    \/\/ ...\r\n\r\n    public function waitAction()\r\n    {\r\n        return $this-&gt;render('IbwJobeetBundle:Affiliate:wait.html.twig');\r\n    }\r\n}<\/pre>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Affiliate\/wait.html.twig\">{% extends \"IbwJobeetBundle::layout.html.twig\" %}\r\n\r\n{% block content %}\r\n    &lt;div class=\"content\"&gt;\r\n        &lt;h1&gt;Your affiliate account has been created&lt;\/h1&gt;\r\n        &lt;div style=\"padding: 20px\"&gt;\r\n            Thank you!\r\n            You will receive an email with your affiliate token\r\n            as soon as your account will be activated.\r\n        &lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n{% endblock %}<\/pre>\n<p>Now, the route:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing\/affiliate.yml\"># ...\r\n\r\nibw_affiliate_wait:\r\n    pattern: \/wait\r\n    defaults: { _controller: \"IbwJobeetBundle:Affiliate:wait\" }<\/pre>\n<p>After defining to routes, in order to work, you need to clear the cache.<\/p>\n<p>Now, if you click on the <code>Affiliates<\/code> link on the homepage, you will be directed to the affiliate form page.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Tests\"><\/span>Tests<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The last step is to write some functional tests for the new feature.<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Tests\/Controller\/AffiliateControllerTest.php\">namespace IbwJobeetBundleTestsController;\r\n\r\nuse SymfonyBundleFrameworkBundleTestWebTestCase;\r\nuse SymfonyBundleFrameworkBundleConsoleApplication;\r\nuse SymfonyComponentConsoleOutputNullOutput;\r\nuse SymfonyComponentConsoleInputArrayInput;\r\nuse DoctrineBundleDoctrineBundleCommandDropDatabaseDoctrineCommand;\r\nuse DoctrineBundleDoctrineBundleCommandCreateDatabaseDoctrineCommand;\r\nuse DoctrineBundleDoctrineBundleCommandProxyCreateSchemaDoctrineCommand;\r\nuse SymfonyComponentDomCrawlerCrawler;\r\n\r\nclass AffiliateControllerTest extends WebTestCase\r\n{\r\n    private $em;\r\n    private $application;\r\n\r\n    public function setUp()\r\n    {\r\n        static::$kernel = static::createKernel();\r\n        static::$kernel-&gt;boot();\r\n\r\n        $this-&gt;application = new Application(static::$kernel);\r\n\r\n        \/\/ drop the database\r\n        $command = new DropDatabaseDoctrineCommand();\r\n        $this-&gt;application-&gt;add($command);\r\n        $input = new ArrayInput(array(\r\n            'command' =&gt; 'doctrine:database:drop',\r\n            '--force' =&gt; true\r\n        ));\r\n        $command-&gt;run($input, new NullOutput());\r\n\r\n        \/\/ we have to close the connection after dropping the database so we don't get \"No database selected\" error\r\n        $connection = $this-&gt;application-&gt;getKernel()-&gt;getContainer()-&gt;get('doctrine')-&gt;getConnection();\r\n        if ($connection-&gt;isConnected()) {\r\n            $connection-&gt;close();\r\n        }\r\n\r\n        \/\/ create the database\r\n        $command = new CreateDatabaseDoctrineCommand();\r\n        $this-&gt;application-&gt;add($command);\r\n        $input = new ArrayInput(array(\r\n            'command' =&gt; 'doctrine:database:create',\r\n        ));\r\n        $command-&gt;run($input, new NullOutput());\r\n\r\n        \/\/ create schema\r\n        $command = new CreateSchemaDoctrineCommand();\r\n        $this-&gt;application-&gt;add($command);\r\n        $input = new ArrayInput(array(\r\n            'command' =&gt; 'doctrine:schema:create',\r\n        ));\r\n        $command-&gt;run($input, new NullOutput());\r\n\r\n        \/\/ get the Entity Manager\r\n        $this-&gt;em = static::$kernel-&gt;getContainer()\r\n            -&gt;get('doctrine')\r\n            -&gt;getManager();\r\n\r\n        \/\/ load fixtures\r\n        $client = static::createClient();\r\n        $loader = new SymfonyBridgeDoctrineDataFixturesContainerAwareLoader($client-&gt;getContainer());\r\n        $loader-&gt;loadFromDirectory(static::$kernel-&gt;locateResource('@IbwJobeetBundle\/DataFixtures\/ORM'));\r\n        $purger = new DoctrineCommonDataFixturesPurgerORMPurger($this-&gt;em);\r\n        $executor = new DoctrineCommonDataFixturesExecutorORMExecutor($this-&gt;em, $purger);\r\n        $executor-&gt;execute($loader-&gt;getFixtures());\r\n    }\r\n\r\n    public function testAffiliateForm()\r\n    {\r\n        $client = static::createClient();\r\n        $crawler = $client-&gt;request('GET', '\/affiliate\/new');\r\n\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerAffiliateController::newAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n\r\n        $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(\r\n            'affiliate[url]' =&gt; 'http:\/\/sensio-labs.com\/',\r\n            'affiliate[email]' =&gt; 'jobeet@example.com'\r\n        ));\r\n\r\n        $client-&gt;submit($form);\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerAffiliateController::createAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n\r\n        $kernel = static::createKernel();\r\n        $kernel-&gt;boot();\r\n        $em = $kernel-&gt;getContainer()-&gt;get('doctrine.orm.entity_manager');\r\n\r\n        $query = $em-&gt;createQuery('SELECT count(a.email) FROM IbwJobeetBundle:Affiliate a WHERE a.email = :email');\r\n        $query-&gt;setParameter('email', 'jobeet@example.com');\r\n        $this-&gt;assertEquals(1, $query-&gt;getSingleScalarResult());\r\n\r\n        $crawler = $client-&gt;request('GET', '\/affiliate\/new');\r\n        $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(\r\n            'affiliate[email]'        =&gt; 'not.an.email',\r\n        ));\r\n        $crawler = $client-&gt;submit($form);\r\n\r\n        \/\/ check if we have 1 errors\r\n        $this-&gt;assertTrue($crawler-&gt;filter('.error_list')-&gt;count() == 1);\r\n        \/\/ check if we have error on affiliate_email field\r\n        $this-&gt;assertTrue($crawler-&gt;filter('#affiliate_email')-&gt;siblings()-&gt;first()-&gt;filter('.error_list')-&gt;count() == 1);\r\n    }\r\n\r\n    public function testCreate()\r\n    {\r\n        $client = static::createClient();\r\n        $crawler = $client-&gt;request('GET', '\/affiliate\/new');\r\n        $form = $crawler-&gt;selectButton('Submit')-&gt;form(array(\r\n            'affiliate[url]' =&gt; 'http:\/\/sensio-labs.com\/',\r\n            'affiliate[email]' =&gt; 'address@example.com'\r\n        ));\r\n\r\n        $client-&gt;submit($form);\r\n        $client-&gt;followRedirect();\r\n\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerAffiliateController::waitAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n\r\n        return $client;\r\n    }\r\n\r\n    public function testWait()\r\n    {\r\n        $client = static::createClient();\r\n        $crawler = $client-&gt;request('GET', '\/affiliate\/wait');\r\n\r\n        $this-&gt;assertEquals('IbwJobeetBundleControllerAffiliateController::waitAction', $client-&gt;getRequest()-&gt;attributes-&gt;get('_controller'));\r\n    }\r\n}<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"The_Affiliate_Backend\"><\/span>The Affiliate Backend<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>For the backend, we will work with <code>SonataAdminBundle<\/code>. As we said before, after an affiliate registers, he needs to wait for the admin to activate his account. So, when the admin will access the affiliates page, he will see only the inactivated accounts, to help him be more productive.<\/p>\n<p>First of all, you need to declare the new affiliate service inside your services.yml file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/services.yml\"># ...\r\n    ibw.jobeet.admin.affiliate:\r\n        class: IbwJobeetBundleAdminAffiliateAdmin\r\n        tags:\r\n            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Affiliates }\r\n        arguments:\r\n            - ~\r\n            - IbwJobeetBundleEntityAffiliate\r\n            - 'IbwJobeetBundle:AffiliateAdmin'<\/pre>\n<p>After that, create the Admin file:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/AffiliateAdmin.php\">namespace IbwJobeetBundleAdmin;\r\n\r\nuse SonataAdminBundleAdminAdmin;\r\nuse SonataAdminBundleDatagridListMapper;\r\nuse SonataAdminBundleDatagridDatagridMapper;\r\nuse SonataAdminBundleValidatorErrorElement;\r\nuse SonataAdminBundleFormFormMapper;\r\nuse SonataAdminBundleShowShowMapper;\r\nuse IbwJobeetBundleEntityAffiliate;\r\n\r\nclass AffiliateAdmin extends Admin\r\n{\r\n    protected $datagridValues = array(\r\n        '_sort_order' =&gt; 'ASC',\r\n        '_sort_by' =&gt; 'is_active'\r\n    );\r\n\r\n    protected function configureFormFields(FormMapper $formMapper)\r\n    {\r\n        $formMapper\r\n            -&gt;add('email')\r\n            -&gt;add('url')\r\n        ;\r\n    }\r\n\r\n    protected function configureDatagridFilters(DatagridMapper $datagridMapper)\r\n    {\r\n        $datagridMapper\r\n            -&gt;add('email')\r\n            -&gt;add('is_active');\r\n    }\r\n\r\n    protected function configureListFields(ListMapper $listMapper)\r\n    {\r\n        $listMapper\r\n            -&gt;add('is_active')\r\n            -&gt;addIdentifier('email')\r\n            -&gt;add('url')\r\n            -&gt;add('created_at')\r\n            -&gt;add('token')\r\n        ;\r\n    }\r\n}<\/pre>\n<p>To help the administrator, we want to display only the inactivated accounts. This can be made by setting the \u2018is_active\u2019 filter to <code>false<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/AffiliateAdmin.php\">\/\/ ...\r\n    protected $datagridValues = array(\r\n        '_sort_order' =&gt; 'ASC',\r\n        '_sort_by' =&gt; 'is_active',\r\n        'is_active' =&gt; array('value' =&gt; 2) \/\/ The value 2 represents that the displayed affiliate accounts are not activated yet\r\n    );\r\n\r\n\/\/ ...<\/pre>\n<p>Now, create the AffiliateAdmin controller file:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/AffiliateAdminController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SonataAdminBundleControllerCRUDController as Controller;\r\nuse SonataDoctrineORMAdminBundleDatagridProxyQuery as ProxyQueryInterface;\r\nuse SymfonyComponentHttpFoundationRedirectResponse;\r\n\r\nclass AffiliateAdminController extends Controller\r\n{\r\n    \/\/ Your code goes here\r\n}<\/pre>\n<p>Let\u2019s create the <code>activate<\/code> and <code>deactivate<\/code> batch actions:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/AffiliateAdminController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SonataAdminBundleControllerCRUDController as Controller;\r\nuse SonataDoctrineORMAdminBundleDatagridProxyQuery as ProxyQueryInterface;\r\nuse SymfonyComponentHttpFoundationRedirectResponse;\r\n\r\nclass AffiliateAdminController extends Controller\r\n{\r\n    public function batchActionActivate(ProxyQueryInterface $selectedModelQuery)\r\n    {\r\n        if($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {\r\n            throw new AccessDeniedException();\r\n        }\r\n\r\n        $request = $this-&gt;get('request');\r\n        $modelManager = $this-&gt;admin-&gt;getModelManager();\r\n\r\n        $selectedModels = $selectedModelQuery-&gt;execute();\r\n\r\n        try {\r\n            foreach($selectedModels as $selectedModel) {\r\n                $selectedModel-&gt;activate();\r\n                $modelManager-&gt;update($selectedModel);\r\n            }\r\n        } catch(Exception $e) {\r\n            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());\r\n\r\n            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n        }\r\n\r\n        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('The selected accounts have been activated'));\r\n\r\n        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n    }\r\n\r\n    public function batchActionDeactivate(ProxyQueryInterface $selectedModelQuery)\r\n    {\r\n        if($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {\r\n            throw new AccessDeniedException();\r\n        }\r\n\r\n        $request = $this-&gt;get('request');\r\n        $modelManager = $this-&gt;admin-&gt;getModelManager();\r\n\r\n        $selectedModels = $selectedModelQuery-&gt;execute();\r\n\r\n        try {\r\n            foreach($selectedModels as $selectedModel) {\r\n                $selectedModel-&gt;deactivate();\r\n                $modelManager-&gt;update($selectedModel);\r\n            }\r\n        } catch(Exception $e) {\r\n            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());\r\n\r\n            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n        }\r\n\r\n        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('The selected accounts have been deactivated'));\r\n\r\n        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n    }\r\n}<\/pre>\n<p>For the new batch actions to be functional, we have to add them in the <code>getBatchActions<\/code> from the <code>Admin<\/code> class:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/AffiliateAdmin.php\">class AffiliateAdmin extends Admin\r\n{\r\n    \/\/ ... \r\n\r\n    public function getBatchActions()\r\n    {\r\n        $actions = parent::getBatchActions();\r\n\r\n        if($this-&gt;hasRoute('edit') &amp;&amp; $this-&gt;isGranted('EDIT') &amp;&amp; $this-&gt;hasRoute('delete') &amp;&amp; $this-&gt;isGranted('DELETE')) {\r\n            $actions['activate'] = array(\r\n                'label'            =&gt; 'Activate',\r\n                'ask_confirmation' =&gt; true\r\n            );\r\n\r\n            $actions['deactivate'] = array(\r\n                'label'            =&gt; 'Deactivate',\r\n                'ask_confirmation' =&gt; true\r\n            );\r\n        }\r\n\r\n        return $actions;\r\n    }\r\n}<\/pre>\n<p>For this to work, you need to add the two methods, activate and deactivate, in the entity file:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Affiliate.php\">\/\/ ...\r\n\r\n    public function activate()\r\n    {\r\n        if(!$this-&gt;getIsActive()) {\r\n            $this-&gt;setIsActive(true);\r\n        }\r\n\r\n        return $this-&gt;is_active;\r\n    }\r\n\r\n    public function deactivate()\r\n    {\r\n        if($this-&gt;getIsActive()) {\r\n            $this-&gt;setIsActive(false);\r\n        }\r\n\r\n        return $this-&gt;is_active;\r\n    }<\/pre>\n<p>Let\u2019s now create two individual actions, activate and deactivate, for each item. Firstly, we will create routes for them. That\u2019s why, in your Admin class, you will extend the configureRoutes function:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/AffiliateAdmin.php\">use SonataAdminBundleRouteRouteCollection;\r\n\r\nclass AffiliateAdmin extends Admin\r\n{\r\n    \/\/ ...\r\n\r\n    protected function configureRoutes(RouteCollection $collection) {\r\n        parent::configureRoutes($collection);\r\n\r\n        $collection-&gt;add('activate',\r\n            $this-&gt;getRouterIdParameter().'\/activate')\r\n        ;\r\n\r\n        $collection-&gt;add('deactivate',\r\n            $this-&gt;getRouterIdParameter().'\/deactivate')\r\n        ;\r\n    }\r\n}<\/pre>\n<p>It\u2019s time to implement the actions in the AdminController:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibbw\/JobeetBundle\/Controller\/AffiliateAdminController.php\">class AffiliateAdminController extends Controller\r\n{\r\n    \/\/ ...\r\n\r\n    public function activateAction($id)\r\n    {\r\n        if($this-&gt;admin-&gt;isGranted('EDIT') === false) {\r\n            throw new AccessDeniedException();\r\n        }\r\n\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n        $affiliate = $em-&gt;getRepository('IbwJobeetBundle:Affiliate')-&gt;findOneById($id);\r\n\r\n        try {\r\n            $affiliate-&gt;setIsActive(true);\r\n            $em-&gt;flush();\r\n        } catch(Exception $e) {\r\n            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());\r\n\r\n            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list', $this-&gt;admin-&gt;getFilterParameters()));\r\n        }\r\n\r\n        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n\r\n    }\r\n\r\n    public function deactivateAction($id)\r\n    {\r\n        if($this-&gt;admin-&gt;isGranted('EDIT') === false) {\r\n            throw new AccessDeniedException();\r\n        }\r\n\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n        $affiliate = $em-&gt;getRepository('IbwJobeetBundle:Affiliate')-&gt;findOneById($id);\r\n\r\n        try {\r\n            $affiliate-&gt;setIsActive(false);\r\n            $em-&gt;flush();\r\n        } catch(Exception $e) {\r\n            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());\r\n\r\n            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list', $this-&gt;admin-&gt;getFilterParameters()));\r\n        }\r\n\r\n        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n    }\r\n}<\/pre>\n<p>Now, create the templates for the new added action buttons:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/AffiliateAdmin\/list__action_activate.html.twig\">{% if admin.isGranted('EDIT', object) and admin.hasRoute('activate') %}\r\n    &lt;a href=\"{{ admin.generateObjectUrl('activate', object) }}\" class=\"btn edit_link\" title=\"{{ 'action_activate'|trans({}, 'SonataAdminBundle') }}\"&gt;\r\n        &lt;i class=\"icon-edit\"&gt;&lt;\/i&gt;\r\n        {{ 'activate'|trans({}, 'SonataAdminBundle') }}\r\n    &lt;\/a&gt;\r\n{% endif %}<\/pre>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/AffiliateAdmin\/list__action_deactivate.html.twig\">{% if admin.isGranted('EDIT', object) and admin.hasRoute('deactivate') %}\r\n    &lt;a href=\"{{ admin.generateObjectUrl('deactivate', object) }}\" class=\"btn edit_link\" title=\"{{ 'action_deactivate'|trans({}, 'SonataAdminBundle') }}\"&gt;\r\n        &lt;i class=\"icon-edit\"&gt;&lt;\/i&gt;\r\n        {{ 'deactivate'|trans({}, 'SonataAdminBundle') }}\r\n    &lt;\/a&gt;\r\n{% endif %}<\/pre>\n<p>Inside your Admin file, add the new actions and buttons to the configureListFields function, so that they would appear on the page, to each account individually:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/AffiliateAdmin.php\">class AffiliateAdmin extends Admin\r\n{\r\n    \/\/ ...    \r\n\r\n    protected function configureListFields(ListMapper $listMapper)\r\n    {\r\n        $listMapper\r\n            -&gt;add('is_active')\r\n            -&gt;addIdentifier('email')\r\n            -&gt;add('url')\r\n            -&gt;add('created_at')\r\n            -&gt;add('token')\r\n            -&gt;add('_action', 'actions', array( 'actions' =&gt; array('activate' =&gt; array('template' =&gt; 'IbwJobeetBundle:AffiliateAdmin:list__action_activate.html.twig'),\r\n                'deactivate' =&gt; array('template' =&gt; 'IbwJobeetBundle:AffiliateAdmin:list__action_deactivate.html.twig'))))\r\n        ;\r\n    }\r\n    \/\/\/ ...\r\n}<\/pre>\n<p>Now, clear your cache and try it on!<\/p>\n<p>That\u2019s all for today! Tomorrow, we will take care of the emails the affiliates will receive when their accounts have been activated.<br \/>\n<a href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/\" rel=\"license\"><img decoding=\"async\" style=\"border-width: 0;\" src=\"\/\/i.creativecommons.org\/l\/by-sa\/3.0\/88x31.png\" alt=\"Creative Commons License\" \/><\/a><br \/>\nThis work is licensed under a <a href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/\" target=\"_blank\" rel=\"license noopener\">Creative Commons Attribution-ShareAlike 3.0 Unported License<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>* This article is part of the original Jobeet Tutorial, created by Fabien Potencier, for Symfony 1.4. Web services in [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[82],"tags":[],"yst_prominent_words":[798,1013,1369],"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2268"}],"collection":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/comments?post=2268"}],"version-history":[{"count":3,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2268\/revisions"}],"predecessor-version":[{"id":133226,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2268\/revisions\/133226"}],"wp:attachment":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/media?parent=2268"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/categories?post=2268"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/tags?post=2268"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/yst_prominent_words?post=2268"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}