{"id":2203,"date":"2013-08-12T08:14:41","date_gmt":"2013-08-12T08:14:41","guid":{"rendered":"https:\/\/intelligentbee.com\/blog\/?p=2203"},"modified":"2024-05-31T09:07:44","modified_gmt":"2024-05-31T09:07:44","slug":"symfony2-jobeet-day-6-more-with-the-model","status":"publish","type":"post","link":"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-6-more-with-the-model\/","title":{"rendered":"Symfony2 Jobeet Day 6: More with the Model"},"content":{"rendered":"<p><span style=\"font-family: timesnew roman; font-size: 12px;\">* This article is part of the original <a href=\"\/\/symfony.com\/legacy\/doc\/jobeet?orm=Doctrine\" target=\"_blank\" rel=\"noopener\">Jobeet Tutorial<\/a>, created by Fabien Potencier, for Symfony 1.4.<\/span><\/p>\n<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-6-more-with-the-model\/#The_Doctrine_Query_Object\" title=\"The Doctrine Query Object\">The Doctrine Query Object<\/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-6-more-with-the-model\/#Debugging_Doctrine_generated_SQL\" title=\"Debugging Doctrine generated SQL\">Debugging Doctrine generated SQL<\/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-6-more-with-the-model\/#Object_Serialization\" title=\"Object Serialization\">Object Serialization<\/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-6-more-with-the-model\/#More_with_Fixtures\" title=\"More with Fixtures\">More with 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-6-more-with-the-model\/#Refactoring\" title=\"Refactoring\">Refactoring<\/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-6-more-with-the-model\/#Categories_on_the_Homepage\" title=\"Categories on the Homepage\">Categories on the Homepage<\/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-6-more-with-the-model\/#Limit_the_results\" title=\"Limit the results\">Limit the results<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-6-more-with-the-model\/#Custom_Configuration\" title=\"Custom Configuration\">Custom Configuration<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-6-more-with-the-model\/#Dinamic_Fixtures\" title=\"Dinamic Fixtures\">Dinamic Fixtures<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-6-more-with-the-model\/#Secure_the_Job_Page\" title=\"Secure the Job Page\">Secure the Job Page<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"The_Doctrine_Query_Object\"><\/span>The Doctrine Query Object<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>From the second day\u2019s requirements: <em>\u201cOn the homepage, the user sees the latest active jobs\u201d<\/em>. But as of now, all jobs are displayed, whether they are active or not:<span id=\"more-184\"><\/span><\/p>\n<pre class=\"theme:tomorrow-night toolbar:1 lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\nclass JobController extends Controller\r\n{\r\n    public function indexAction()\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $entities = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findAll();\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n            'entities' =&gt; $entities\r\n        ));\r\n\r\n \/\/ ...\r\n}<\/pre>\n<p>An active job is one that was posted less than 30 days ago. The <code>$entities = $em-&gt;getRepository('IbwJobeetBundle')-&gt;findAll()<\/code> method will make a request to the database to get all the jobs. We are not specifying any condition, which means that all the records are retrieved from the database.<\/p>\n<p>Let\u2019s change it to only select active jobs:<\/p>\n<pre class=\"theme:tomorrow-night toolbar:1 lang:default decode:true \">public function indexAction()\r\n{\r\n    $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n    $query = $em-&gt;createQuery(\r\n        'SELECT j FROM IbwJobeetBundle:Job j WHERE j.created_at &gt; :date'\r\n    )-&gt;setParameter('date', date('Y-m-d H:i:s', time() - 86400 * 30));\r\n    $entities = $query-&gt;getResult();\r\n\r\n    return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n        'entities' =&gt; $entities\r\n    ));\r\n}<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Debugging_Doctrine_generated_SQL\"><\/span>Debugging Doctrine generated SQL<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Sometimes, it is of great help to see the SQL generated by Doctrine; for instance, to debug a query that does not work as expected. In the dev environment, thanks to the Symfony Web Debug Toolbar, all the information you need is available within the comfort of your browser (<code>http:\/\/jobeet.local\/app_dev.php<\/code>):<\/p>\n<p><a href=\"\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-6-web-debug-toolbar.png\"><img decoding=\"async\" class=\"alignnone size-full wp-image-50\" src=\"\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-6-web-debug-toolbar.png\" alt=\"Day 6 - web debug toolbar\" width=\"1909\" height=\"1044\" \/><\/a><\/p>\n<h2><span class=\"ez-toc-section\" id=\"Object_Serialization\"><\/span>Object Serialization<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Even if the code above works, it is far from perfect as it does not take into account some requirements from Day 2: <em>\u201cA user can come back to re-activate or extend the validity of the job for an extra 30 days..\u201d<\/em>.<\/p>\n<p>But as the above code only relies on the <code>created_at<\/code> value, and because this column stores the creation date, we cannot satisfy the above requirement.<\/p>\n<p>If you remember the database schema we have described during Day 3, we also have defined an <code>expires_at<\/code> column. Currently, if this value is not set in <code>fixture<\/code> file, it remains always empty. But when a job is created, it can be automatically set to 30 days after the current date.<\/p>\n<p>When you need to do something automatically before a Doctrine object is serialized to the database, you can add a new action to the <code>lifecycle callbacks<\/code> in the file that maps objects to the database, like we did earlier for the <code>created_at<\/code> column:<\/p>\n<pre class=\"theme:tomorrow-night toolbar:1 lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Job.orml.yml\"># ...\r\n    # ...\r\n    lifecycleCallbacks:\r\n        prePersist: [ setCreatedAtValue, setExpiresAtValue ]\r\n        preUpdate: [ setUpdatedAtValue ]<\/pre>\n<p>Now, we have to rebuild the entities classes so Doctrine will add the new function:<\/p>\n<pre class=\"theme:tomorrow-night toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:generate:entities IbwJobeetBundle<\/pre>\n<p>Open the <code>src\/Ibw\/JobeetBundle\/Entity\/Job.php<\/code> file and edit the new added function:<\/p>\n<pre class=\"theme:tomorrow-night toolbar:1 lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">\/\/ ...\r\n\r\nclass Job\r\n{\r\n    \/\/ ... \r\n\r\n    public function setExpiresAtValue()\r\n    {\r\n        if(!$this-&gt;getExpiresAt()) {\r\n            $now = $this-&gt;getCreatedAt() ? $this-&gt;getCreatedAt()-&gt;format('U') : time();\r\n            $this-&gt;expires_at = new DateTime(date('Y-m-d H:i:s', $now + 86400 * 30));\r\n        }\r\n    }\r\n}<\/pre>\n<p>Now, let\u2019s change the action to use the <code>expires_at<\/code> column instead of the <code>created_at<\/code> one to select the active jobs:<\/p>\n<pre class=\"theme:tomorrow-night toolbar:1 lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function indexAction()\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $query = $em-&gt;createQuery(\r\n            'SELECT j FROM IbwJobeetBundle:Job j WHERE j.expires_at &gt; :date'\r\n    )-&gt;setParameter('date', date('Y-m-d H:i:s', time()));\r\n        $entities = $query-&gt;getResult();\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n            'entities' =&gt; $entities\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"More_with_Fixtures\"><\/span>More with Fixtures<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Refreshing the Jobeet homepage in your browser won\u2019t change anything, as the jobs in the database have been posted just a few days ago. Let\u2019s change the fixtures to add a job that is already expired:<\/p>\n<pre class=\"theme:tomorrow-night toolbar:1 lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/DataFixtures\/ORM\/LoadJobData.php\">\/\/ ...\r\n\r\n    public function load(ObjectManager $em)\r\n    {\r\n        $job_expired = new Job();\r\n        $job_expired-&gt;setCategory($em-&gt;merge($this-&gt;getReference('category-programming')));\r\n        $job_expired-&gt;setType('full-time');\r\n        $job_expired-&gt;setCompany('Sensio Labs');\r\n        $job_expired-&gt;setLogo('sensio-labs.gif');\r\n        $job_expired-&gt;setUrl('http:\/\/www.sensiolabs.com\/');\r\n        $job_expired-&gt;setPosition('Web Developer Expired');\r\n        $job_expired-&gt;setLocation('Paris, France');\r\n        $job_expired-&gt;setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');\r\n        $job_expired-&gt;setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');\r\n        $job_expired-&gt;setIsPublic(true);\r\n        $job_expired-&gt;setIsActivated(true);\r\n        $job_expired-&gt;setToken('job_expired');\r\n        $job_expired-&gt;setEmail('job@example.com');\r\n        $job_expired-&gt;setCreatedAt(new DateTime('2005-12-01'));\r\n\r\n        \/\/ ...\r\n\r\n        $em-&gt;persist($job_expired);\r\n        \/\/ ...\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>Reload the fixtures and refresh your browser to ensure that the old job does not show up:<\/p>\n<pre class=\"toolbar:2 lang:default decode:true \">php app\/console doctrine:fixtures:load<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Refactoring\"><\/span>Refactoring<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Although the code we have written works fine, it\u2019s not quite right yet. Can you spot the problem?<\/p>\n<p>The Doctrine query code does not belong to the action (the Controller layer), it belongs to the <code>Model<\/code> layer. In the MVC model, the Model defines all the business logic, and the Controller only calls the Model to retrieve data from it. As the code returns a collection of jobs, let\u2019s move the code to the model. For that we will need to create a custom repository class for <code>Job <\/code>entity and to add the query to that class.<\/p>\n<p>Open <code>\/src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Job.orm.yml<\/code> and add the following to it:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Job.orm.yml\">IbwJobeetBundleEntityJob:\r\n    type: entity\r\n    repositoryClass: IbwJobeetBundleRepositoryJobRepository\r\n    # ...<\/pre>\n<p>Doctrine can generate the repository class for you by running the <code>generate:entities <\/code>command used earlier:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:generate:entities IbwJobeetBundle<\/pre>\n<p>Next, add a new method \u2013 <code>getActiveJobs()<\/code> \u2013 to the newly generated repository class. This method will query for all of the active Job entities sorted by the <code>expires_at<\/code> column (and filtered by category, if it receives the <code>$category_id<\/code> parameter).<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/JobRepository.php\">namespace IbwJobeetBundleRepository;\r\n\r\nuse DoctrineORMEntityRepository;\r\n\r\n\/**\r\n * JobRepository\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 JobRepository extends EntityRepository\r\n{\r\n    public function getActiveJobs($category_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;orderBy('j.expires_at', 'DESC');\r\n\r\n        if($category_id)\r\n        {\r\n            $qb-&gt;andWhere('j.category = :category_id')\r\n                -&gt;setParameter('category_id', $category_id);\r\n        }\r\n\r\n        $query = $qb-&gt;getQuery();\r\n\r\n        return $query-&gt;getResult();\r\n    }\r\n}<\/pre>\n<p>Now the action code can use this new method to retrieve the active jobs.<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function indexAction()\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $entities = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs();\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n            'entities' =&gt; $entities\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>This refactoring has several benefits over the previous code:<\/p>\n<ul>\n<li>The logic to get the active jobs is now in the Model, where it belongs<\/li>\n<li>The code in the controller is thinner and much more readable<\/li>\n<li>The <code>getActiveJobs()<\/code> method is re-usable (for instance in another action)<\/li>\n<li>The model code is now unit testable<\/li>\n<\/ul>\n<div>\n<h2><span class=\"ez-toc-section\" id=\"Categories_on_the_Homepage\"><\/span>Categories on the Homepage<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>According to the second day\u2019s requirements we need to have jobs sorted by categories. Until now, we have not taken the job category into account. From the requirements, the homepage must display jobs by category. First, we need to get all categories with at least one active job.<\/p>\n<p>Create a repository class for the <code>Category<\/code> entity like we did for Job:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Category.orm.yml\">IbwJobeetBundleEntityCategory:\r\n    type: entity\r\n    repositoryClass: IbwJobeetBundleRepositoryCategoryRepository\r\n    #...<\/pre>\n<p>Generate the repository class:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:generate:entities IbwJobeetBundle<\/pre>\n<p>Open the <code>CategoryRepository<\/code> class and add a <code>getWithJobs()<\/code> method:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/CategoryRepository.php\">namespace IbwJobeetBundleRepository;\r\n\r\nuse DoctrineORMEntityRepository;\r\n\r\n\/**\r\n * CategoryRepository\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 CategoryRepository extends EntityRepository\r\n{\r\n    public function getWithJobs()\r\n    {\r\n        $query = $this-&gt;getEntityManager()-&gt;createQuery(\r\n            'SELECT c FROM IbwJobeetBundle:Category c LEFT JOIN c.jobs j WHERE j.expires_at &gt; :date'\r\n        )-&gt;setParameter('date', date('Y-m-d H:i:s', time()));\r\n\r\n        return $query-&gt;getResult();\r\n    }   \r\n}<\/pre>\n<p>Change the <code>index<\/code> action accordingly:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function indexAction()\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $categories = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();\r\n\r\n        foreach($categories as $category) {\r\n            $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId()));\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n            'categories' =&gt; $categories\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>For this to work, we have to add a new property to our <code>Category<\/code> class, the <code>active_jobs<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Category.php\">class Category\r\n{\r\n    \/\/ ...\r\n\r\n    private $active_jobs;\r\n\r\n    \/\/ ...\r\n\r\n    public function setActiveJobs($jobs)\r\n    {\r\n        $this-&gt;active_jobs = $jobs;\r\n    }\r\n\r\n    public function getActiveJobs()\r\n    {\r\n        return $this-&gt;active_jobs;\r\n    }\r\n}<\/pre>\n<p>In the template, we need to iterate through all categories and display the active jobs:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/index.html.twig\">&lt;!-- ... --&gt;\r\n{% block content %}\r\n    &lt;div id=\"jobs\"&gt;\r\n        {% for category in categories %}\r\n            &lt;div&gt;\r\n                &lt;div class=\"category\"&gt;\r\n                    &lt;div class=\"feed\"&gt;\r\n                        &lt;a href=\"\"&gt;Feed&lt;\/a&gt;\r\n                    &lt;\/div&gt;\r\n                    &lt;h1&gt;{{ category.name }}&lt;\/h1&gt;\r\n                &lt;\/div&gt;\r\n                &lt;table class=\"jobs\"&gt;\r\n                    {% for entity in category.activejobs %}\r\n                        &lt;tr class=\"{{ cycle(['even', 'odd'], loop.index) }}\"&gt;\r\n                            &lt;td class=\"location\"&gt;{{ entity.location }}&lt;\/td&gt;\r\n                            &lt;td class=\"position\"&gt;\r\n                                &lt;a href=\"{{ path('ibw_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}\"&gt;\r\n                                    {{ entity.position }}\r\n                                &lt;\/a&gt;\r\n                            &lt;\/td&gt;\r\n                             &lt;td class=\"company\"&gt;{{ entity.company }}&lt;\/td&gt;\r\n                        &lt;\/tr&gt;\r\n                    {% endfor %}\r\n                &lt;\/table&gt;\r\n            &lt;\/div&gt;\r\n        {% endfor %}\r\n    &lt;\/div&gt;\r\n{% endblock %}<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Limit_the_results\"><\/span>Limit the results<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>There is still one requirement to implement for the homepage job list: we have to limit the job list to 10 items. That\u2019s simple enough to add the <code>$max<\/code> parameter to the <code>JobRepository::getActiveJobs()<\/code> method:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/JobRepository.php\">    public function getActiveJobs($category_id = null, $max = 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;orderBy('j.expires_at', 'DESC');\r\n\r\n        if($max) {\r\n            $qb-&gt;setMaxResults($max);\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\r\n        $query = $qb-&gt;getQuery();\r\n\r\n        return $query-&gt;getResult();\r\n    }<\/pre>\n<p>Change the call to <code>getActiveJobs()<\/code> to include the <code>$max<\/code> parameter:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function indexAction()\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $categories = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();\r\n\r\n        foreach($categories as $category)\r\n        {\r\n            $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId(), 10));\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n            'categories' =&gt; $categories\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Custom_Configuration\"><\/span>Custom Configuration<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In the <code>JobController<\/code>, <code>indexAction<\/code> method, we have hardcoded the number of max jobs returned for a category. It would have been better to make the 10 limit configurable. In Symfony, you can define custom parameters for your application in the <code>app\/config\/config.yml<\/code> file, under the <code>parameters<\/code> key (if the <code>parameters<\/code> key doesn\u2019t exist, create it):<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"app\/config\/config.yml\"># ...\r\n\r\nparameters:\r\n    max_jobs_on_homepage: 10<\/pre>\n<p>This can now be accessed from a controller:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function indexAction()\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $categories = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();\r\n\r\n        foreach($categories as $category) {\r\n            $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId(), $this-&gt;container-&gt;getParameter('max_jobs_on_homepage')));\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(\r\n            'categories' =&gt; $categories\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Dinamic_Fixtures\"><\/span>Dinamic Fixtures<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>For now, you won\u2019t see any difference because we have a very small amount of jobs in our database. We need to add a bunch of jobs to the fixture. So, you can copy and paste an existing job ten or twenty times by hand\u2026 but there\u2019s a better way. Duplication is bad, even in fixture files:<\/p>\n<pre class=\"wrap:true lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/DataFixtures\/ORM\/LoadJobData.php\">\/\/ ...\r\n\r\npublic function load(ObjectManager $em)\r\n{\r\n    \/\/ ...\r\n\r\n    for($i = 100; $i &amp;lt;= 130; $i++)\r\n    {\r\n        $job = new Job();\r\n        $job-&amp;gt;setCategory($em-&amp;gt;merge($this-&amp;gt;getReference('category-programming')));\r\n        $job-&amp;gt;setType('full-time');\r\n        $job-&amp;gt;setCompany('Company '.$i);\r\n        $job-&amp;gt;setPosition('Web Developer');\r\n        $job-&amp;gt;setLocation('Paris, France');\r\n        $job-&amp;gt;setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');\r\n        $job-&amp;gt;setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');\r\n        $job-&amp;gt;setIsPublic(true);\r\n        $job-&amp;gt;setIsActivated(true);\r\n        $job-&amp;gt;setToken('job_'.$i);\r\n        $job-&amp;gt;setEmail('job@example.com');\r\n\r\n        $em-&amp;gt;persist($job);\r\n    }\r\n\r\n    \/\/ ... \r\n    $em-&amp;gt;flush();\r\n}\r\n\r\n\/\/ ...<\/pre>\n<p>You can now reload the fixtures with the <code>doctrine:fixtures:load<\/code> task and see if only 10 jobs are displayed on the homepage for the Programming category:<\/p>\n<p><a href=\"\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-6-limited-no-of-jobs.png\"><img decoding=\"async\" class=\"alignnone size-full wp-image-51\" src=\"\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-6-limited-no-of-jobs.png\" alt=\"Day 6 - limited no of jobs\" width=\"1918\" height=\"1042\" \/><\/a><\/p>\n<h2><span class=\"ez-toc-section\" id=\"Secure_the_Job_Page\"><\/span>Secure the Job Page<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When a job expires, even if you know the URL, it must not be possible to access it anymore. Try the URL for the expired job (replace the id with the actual id in your database \u2013 <code>SELECT id, token FROM job WHERE expires_at &lt; NOW()<\/code>):<\/p>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td>\n<div>\n<div><code>\/app_dev.php\/job\/sensio-labs\/paris-france\/ID\/web-developer-expired<\/code><\/div>\n<\/div>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Instead of displaying the job, we need to forward the user to a 404 page. For this we will create a new function in the <code>JobRepository<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/JobRepository.php\">\/\/ ...\r\n\r\n    public function getActiveJob($id)\r\n    {\r\n        $query = $this-&gt;createQueryBuilder('j')\r\n            -&gt;where('j.id = :id')\r\n            -&gt;setParameter('id', $id)\r\n            -&gt;andWhere('j.expires_at &gt; :date')\r\n            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))\r\n            -&gt;setMaxResults(1)\r\n            -&gt;getQuery();\r\n\r\n        try {\r\n            $job = $query-&gt;getSingleResult();\r\n        } catch (DoctrineOrmNoResultException $e) {\r\n            $job = null;\r\n        }\r\n\r\n        return $job;\r\n    }<\/pre>\n<blockquote><p>The <code>getSingleResult()<\/code> method throws a <code>DoctrineORMNoResultException<\/code> exception if no results are returned and a <code>DoctrineORMNonUniqueResultException<\/code> if more than one result is returned. If you use this method, you may need to wrap it in a <code>try-catch block<\/code>and ensure that only one result is returned.<\/p><\/blockquote>\n<p>Now change the <code>showAction()<\/code> from the <code>JobController<\/code> to use the new repository method:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n$entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJob($id);\r\n\r\n\/\/ ...<\/pre>\n<p>Now, if you try to get an expired job, you will be forwarded to a 404 page:<\/p>\n<p><a href=\"\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-6-no-job-found.png\"><img decoding=\"async\" class=\"alignnone size-full wp-image-52\" src=\"\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-6-no-job-found.png\" alt=\"Day 6 - no job found\" width=\"1903\" height=\"1036\" \/><\/a><\/p>\n<h4>That\u2019s all for today! We will see you again tomorrow, when we\u2019ll be playing with the category page.<\/h4>\n<\/div>\n<p><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. The Doctrine Query [&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":[394,1147,1355,1395,1398,1556],"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2203"}],"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=2203"}],"version-history":[{"count":2,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2203\/revisions"}],"predecessor-version":[{"id":133055,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2203\/revisions\/133055"}],"wp:attachment":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/media?parent=2203"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/categories?post=2203"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/tags?post=2203"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/yst_prominent_words?post=2203"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}