{"id":2234,"date":"2013-08-16T13:38:30","date_gmt":"2013-08-16T13:38:30","guid":{"rendered":"https:\/\/intelligentbee.com\/blog\/?p=2234"},"modified":"2024-09-30T07:48:30","modified_gmt":"2024-09-30T07:48:30","slug":"symfony2-jobeet-day-10-the-forms","status":"publish","type":"post","link":"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-10-the-forms\/","title":{"rendered":"Symfony2 Jobeet Day 10: The Forms"},"content":{"rendered":"<p><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><\/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-10-the-forms\/#Forms_in_jobeet\" title=\"Forms in jobeet\">Forms in jobeet<\/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-10-the-forms\/#Customizing_the_Job_Form\" title=\"Customizing the Job Form\">Customizing the Job Form<\/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-10-the-forms\/#The_Form_Action\" title=\"The Form Action\">The Form Action<\/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-10-the-forms\/#PROTECTING_THE_JOB_FORM_WITH_A_TOKEN\" title=\"PROTECTING THE JOB FORM WITH A TOKEN\">PROTECTING THE JOB FORM WITH A TOKEN<\/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-10-the-forms\/#The_Preview_Page\" title=\"The Preview Page\">The Preview Page<\/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-10-the-forms\/#Job_Activation_and_Publication\" title=\"Job Activation and Publication\">Job Activation and Publication<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"Forms_in_jobeet\"><\/span>Forms in jobeet<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Any website has forms, from the simple contact form to the complex ones with lots of fields. Writing forms is also one of the most complex and tedious task for a web developer: 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, repopulate fields in case of errors and much more \u2026<span id=\"more-187\"><\/span><\/p>\n<p>In Day 3 of this tutorial we used the <code>doctrine:generate:crud<\/code> command to generate a simple CRUD controller for our <code>Job<\/code> entity. This also generated a <code>Job form<\/code> that you can find in <code>\/src\/Ibw\/JobeetBundle\/Form\/JobType.php<\/code> file.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Customizing_the_Job_Form\"><\/span>Customizing the Job Form<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The <code>Job<\/code> form is a perfect example to learn form customization. Let\u2019s see how to customize it, step by step.<\/p>\n<p>First, change the <code>Post a Job<\/code> link in the <code>layout<\/code> to be able to check changes directly in your browser:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/layout.html.twig\">&lt;a href=\"{{ path('ibw_job_new') }}\"&gt;Post a Job&lt;\/a&gt;<\/pre>\n<p>Then, change the <code>ibw_job_show<\/code> route parameters in <code>createAction<\/code> of the <code>JobController<\/code> to match the new route we created in day 5 of this tutorial:<\/p>\n<pre class=\"lang:php decode:true\" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\npublic function createAction(Request $request)\r\n{\r\n    $entity  = new Job();\r\n    $form = $this-&gt;createForm(new JobType(), $entity);\r\n    $form-&gt;bind($request);\r\n\r\n    if ($form-&gt;isValid()) {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $em-&gt;persist($entity);\r\n        $em-&gt;flush();\r\n\r\n        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_show', array(\r\n            'company' =&gt; $entity-&gt;getCompanySlug(),\r\n            'location' =&gt; $entity-&gt;getLocationSlug(),\r\n            'id' =&gt; $entity-&gt;getId(),\r\n            'position' =&gt; $entity-&gt;getPositionSlug()\r\n        )));\r\n    }\r\n\r\n    return $this-&gt;render('IbwJobeetBundle:Job:new.html.twig', array(\r\n        'entity' =&gt; $entity,\r\n        'form'   =&gt; $form-&gt;createView(),\r\n    ));\r\n}\r\n\r\n\/\/ ...<\/pre>\n<p>By default, the Doctrine generated form displays fields for all the table columns. But for the <code>Job<\/code> form, some of them must not be editable by the end user. Edit the <code>Job<\/code> form as you see below:<\/p>\n<pre class=\"lang:php decode:true\" title=\"src\/Ibw\/JobeetBundle\/Form\/JobType.php\">namespace IbwJobeetBundleForm;\r\n\r\nuse SymfonyComponentFormAbstractType;\r\nuse SymfonyComponentFormFormBuilderInterface;\r\nuse SymfonyComponentOptionsResolverOptionsResolverInterface;\r\n\r\nclass JobType extends AbstractType\r\n{\r\n    public function buildForm(FormBuilderInterface $builder, array $options)\r\n    {\r\n        $builder\r\n            -&gt;add('type')\r\n            -&gt;add('category')\r\n            -&gt;add('company')\r\n            -&gt;add('logo')\r\n            -&gt;add('url')\r\n            -&gt;add('position')\r\n            -&gt;add('location')\r\n            -&gt;add('description')\r\n            -&gt;add('how_to_apply')\r\n            -&gt;add('token')\r\n            -&gt;add('is_public')\r\n            -&gt;add('email')\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; 'IbwJobeetBundleEntityJob'\r\n        ));\r\n    }\r\n\r\n    public function getName()\r\n    {\r\n        return 'job';\r\n    }\r\n}<\/pre>\n<p>The form configuration must sometimes be more precise than what can be introspected from the database schema. For example, the <code>email<\/code> column is a <code>varchar<\/code> in the schema, but we need this column to be validated as an email. In Symfony2, validation is applied to the underlying object (e.g. Job). In other words, the question isn\u2019t whether the <code>form <\/code>is valid, but whether or not the <code>Job<\/code> object is valid after the form has applied the submitted data to it. To do this, create a new <code>validation.yml<\/code> file in the <code>Resources\/config<\/code> directory of our bundle:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/validation.yml\">IbwJobeetBundleEntityJob:\r\n    properties:\r\n        email:\r\n            - NotBlank: ~\r\n            - Email: ~<\/pre>\n<p>Even if the <code>type<\/code> column is also a <code>varchar<\/code> in the schema, we want its value to be restricted to a list of choices: <strong>full time<\/strong>, <strong>part time<\/strong> or <strong>freelance<\/strong>.<\/p>\n<pre class=\"lang:php decode:true\" title=\"src\/Ibw\/JobeetBundle\/Form\/JobType.php\">\/\/ ...\r\nuse IbwJobeetBundleEntityJob;\r\n\r\nclass JobType extends AbstractType\r\n{\r\n    public function buildForm(FormBuilderInterface $builder, array $options)\r\n    {\r\n        $builder\r\n            -&gt;add('type', 'choice', array('choices' =&gt; Job::getTypes(), 'expanded' =&gt; true))\r\n            \/\/ ...\r\n    }\r\n\r\n    \/\/ ...\r\n\r\n}<\/pre>\n<p>For this to work, add the following methods in the <code>Job<\/code> entity:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">     \/\/ ...\r\n\r\n    public static function getTypes()\r\n    {\r\n        return array('full-time' =&gt; 'Full time', 'part-time' =&gt; 'Part time', 'freelance' =&gt; 'Freelance');\r\n    }\r\n\r\n    public static function getTypeValues()\r\n    {\r\n        return array_keys(self::getTypes());\r\n    }\r\n\r\n    \/\/ ...<\/pre>\n<p>The <code>getTypes()<\/code> method is used in the form to get the possible types for a Job and <code>getTypeValues()<\/code> will be used in the validation to get the valid values for the type field.<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/validation.yml\">IbwJobeetBundleEntityJob:\r\n    properties:\r\n        type:\r\n            - NotBlank: ~\r\n            - Choice: { callback: getTypeValues }\r\n        email:\r\n            - NotBlank: ~\r\n            - Email: ~<\/pre>\n<p>For each field, symfony automatically generates a <code>label<\/code> (which will be used in the rendered <code><label><\/label><\/code>tag). This can be changed with the <code>label<\/code> option:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Form\/JobType.php\">    public function buildForm(FormBuilderInterface $builder, array $options)\r\n    {\r\n        $builder\r\n            \/\/ ...\r\n            -&gt;add('logo', null, array('label' =&gt; 'Company logo'))\r\n            \/\/ ...\r\n            -&gt;add('how_to_apply', null, array('label' =&gt; 'How to apply?'))\r\n            \/\/ ...\r\n            -&gt;add('is_public', null, array('label' =&gt; 'Public?'))\r\n            \/\/ ...\r\n    }<\/pre>\n<p>You should also add validation constraints for the rest of the fields:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/validation.yml\">IbwJobeetBundleEntityJob:\r\n    properties:\r\n        category:\r\n            - NotBlank: ~\r\n        type:\r\n            - NotBlank: ~\r\n            - Choice: {callback: getTypeValues}\r\n        company:\r\n            - NotBlank: ~\r\n        position:\r\n            - NotBlank: ~\r\n        location:\r\n            - NotBlank: ~\r\n        description:\r\n            - NotBlank: ~\r\n        how_to_apply:\r\n            - NotBlank: ~\r\n        token:\r\n            - NotBlank: ~\r\n        email:\r\n            - NotBlank: ~\r\n            - Email: ~\r\n        url:\r\n            - Url: ~<\/pre>\n<p>The constraint applied to url field enforces the URL format to be like this: <code>http:\/\/www.sitename.domain<\/code> or <code>https:\/\/www.sitename.domain<\/code>.<\/p>\n<p>After modifying <code>validation.yml<\/code>, you need to clear the cache.<\/p>\n<h3>Handling File Uploads in Symfony2<\/h3>\n<p>To handle the actual file upload in the form, we will use a <code>virtual file<\/code> field. For this, we will add a new file property to the <code>Job<\/code> entity:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">    \/\/ ...\r\n\r\n    public $file;\r\n\r\n    \/\/ ...<\/pre>\n<p>Now we need to replace the logo with the file widget and change it to a file input tag:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Form\/JobType.php\">\/\/ ...\r\n\r\n    public function buildForm(FormBuilderInterface $builder, array $options)\r\n    {\r\n        $builder\r\n            \/\/ ...\r\n            -&gt;add('file', 'file', array('label' =&gt; 'Company logo', 'required' =&gt; false))\r\n            \/\/ ...\r\n    }\r\n\/\/ ...<\/pre>\n<p>To make sure the uploaded file is a valid image, we will use the Image validation constraint:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/validation.yml\">IbwJobeetBundleEntityJob:\r\n    properties:\r\n        # ...\r\n        file:\r\n            - Image: ~<\/pre>\n<p>When the form is submitted, the file field will be an instance of <code>UploadedFile<\/code>. It can be used to move the file to a permanent location. After this, we will set the job <code>logo<\/code> property to the uploaded file name.<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function createAction(Request $request)\r\n    {\r\n        \/\/ ...\r\n\r\n        if ($form-&gt;isValid()) {\r\n            $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n            $entity-&gt;file-&gt;move(__DIR__.'\/..\/..\/..\/..\/web\/uploads\/jobs', $entity-&gt;file-&gt;getClientOriginalName());\r\n            $entity-&gt;setLogo($entity-&gt;file-&gt;getClientOriginalName());\r\n\r\n            $em-&gt;persist($entity);\r\n            $em-&gt;flush();\r\n\r\n            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_show', array(\r\n                'company' =&gt; $entity-&gt;getCompanySlug(),\r\n                'location' =&gt; $entity-&gt;getLocationSlug(),\r\n                'id' =&gt; $entity-&gt;getId(),\r\n                'position' =&gt; $entity-&gt;getPositionSlug()\r\n            )));\r\n        }\r\n        \/\/ ...\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>You need to create the logo directory (<code>web\/uploads\/jobs\/<\/code>) and check that it is writable by the web server.<br \/>\nEven if this implementation works, a better way is to handle the file upload using the Doctrine <code>Job<\/code> entity.<\/p>\n<p>First, add the following to the <code>Job<\/code> entity:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">class Job\r\n{\r\n    \/\/ ... \r\n    protected function getUploadDir()\r\n    {\r\n        return 'uploads\/jobs';\r\n    }\r\n\r\n    protected function getUploadRootDir()\r\n    {\r\n        return __DIR__.'\/..\/..\/..\/..\/web\/'.$this-&gt;getUploadDir();\r\n    }\r\n\r\n    public function getWebPath()\r\n    {\r\n        return null === $this-&gt;logo ? null : $this-&gt;getUploadDir().'\/'.$this-&gt;logo;\r\n    }\r\n\r\n    public function getAbsolutePath()\r\n    {\r\n        return null === $this-&gt;logo ? null : $this-&gt;getUploadRootDir().'\/'.$this-&gt;logo;\r\n    }\r\n}<\/pre>\n<p>The <code>logo<\/code> property stores the relative path to the file and is persisted to the database. The <code>getAbsolutePath()<\/code> is a convenience method that returns the absolute path to the file while the <code>getWebPath()<\/code> is a convenience method that returns the web path, which can be used in a template to link to the uploaded file.<\/p>\n<p>We will make the implementation so that the database operation and the moving of the file are atomic: if there is a problem persisting the entity or if the file cannot be saved, then nothing will happen. To do this, we need to move the file right as Doctrine persists the entity to the database. This can be accomplished by hooking into the <code>Job<\/code> entity <code>lifecycle callback<\/code>. Like we did in day 3 of the Jobeet tutorial, we will edit the <code>Job.orm.yml<\/code> file and add the <code>preUpload<\/code>, <code>upload<\/code> and <code>removeUpload<\/code> callbacks in it:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Job.orm.yml\">IbwJobeetBundleEntityJob:\r\n    # ...\r\n\r\n    lifecycleCallbacks:\r\n        prePersist: [ preUpload, setCreatedAtValue, setExpiresAtValue ]\r\n        preUpdate: [ preUpload, setUpdatedAtValue ]\r\n        postPersist: [ upload ]\r\n        postUpdate: [ upload ]\r\n        postRemove: [ removeUpload ]<\/pre>\n<p>Now run the <code>generate:entities<\/code> doctrine command to add these new methods to the <code>Job <\/code>entity:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:generate:entities IbwJobeetBundle<\/pre>\n<p>Edit the <code>Job<\/code> entity and change the added methods to the following:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">class Job\r\n{\r\n    \/\/ ...\r\n\r\n    \/**\r\n     * @ORMPrePersist\r\n     *\/\r\n    public function preUpload()\r\n    {\r\n         if (null !== $this-&gt;file) {\r\n             $this-&gt;logo = uniqid().'.'.$this-&gt;file-&gt;guessExtension();\r\n         }\r\n    }\r\n\r\n    \/**\r\n     * @ORMPostPersist\r\n     *\/\r\n    public function upload()\r\n    {\r\n        if (null === $this-&gt;file) {\r\n            return;\r\n        }\r\n\r\n        \/\/ If there is an error when moving the file, an exception will\r\n        \/\/ be automatically thrown by move(). This will properly prevent\r\n        \/\/ the entity from being persisted to the database on error\r\n        $this-&gt;file-&gt;move($this-&gt;getUploadRootDir(), $this-&gt;logo);\r\n\r\n        unset($this-&gt;file);\r\n    }\r\n\r\n    \/**\r\n     * @ORMPostRemove\r\n     *\/\r\n    public function removeUpload()\r\n    {\r\n        if(file_exists($file)) {\r\n            if ($file = $this-&gt;getAbsolutePath()) {\r\n                unlink($file);\r\n            }\r\n        }    \r\n    }\r\n}<\/pre>\n<p>The class now does everything we need: it generates a unique filename before persisting, moves the file after persisting, and removes the file if the entity is ever deleted. Now that the moving of the file is handled atomically by the entity, we should remove the code we added earlier in the controller to handle the upload:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function createAction(Request $request)\r\n    {\r\n        $entity  = new Job();\r\n        $form = $this-&gt;createForm(new JobType(), $entity);\r\n        $form-&gt;bind($request);\r\n\r\n        if ($form-&gt;isValid()) {\r\n            $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n            $em-&gt;persist($entity);\r\n            $em-&gt;flush();\r\n\r\n            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_show', array(\r\n                'company' =&gt; $entity-&gt;getCompanySlug(),\r\n                'location' =&gt; $entity-&gt;getLocationSlug(),\r\n                'id' =&gt; $entity-&gt;getId(),\r\n                'position' =&gt; $entity-&gt;getPositionSlug()\r\n            )));\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:new.html.twig', array(\r\n            'entity' =&gt; $entity,\r\n            'form'   =&gt; $form-&gt;createView(),\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<h3>The Form Template<\/h3>\n<p>Now that the form class has been customized, we need to display it. Open the <code>new.html.twig <\/code>template and edit it:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibe\/JobeetBundle\/Resources\/views\/Job\/new.html.twig\">{% extends 'IbwJobeetBundle::layout.html.twig' %}\r\n\r\n{% form_theme form _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;Job creation&lt;\/h1&gt;\r\n    &lt;form action=\"{{ path('ibw_job_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=\"Preview your job\" \/&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.category) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.category) }}\r\n                        {{ form_widget(form.category) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.type) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.type) }}\r\n                        {{ form_widget(form.type) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.company) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.company) }}\r\n                        {{ form_widget(form.company) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.file) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.file) }}\r\n                        {{ form_widget(form.file) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&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.position) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.position) }}\r\n                        {{ form_widget(form.position) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.location) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.location) }}\r\n                        {{ form_widget(form.location) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.description) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.description) }}\r\n                        {{ form_widget(form.description) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.how_to_apply) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.how_to_apply) }}\r\n                        {{ form_widget(form.how_to_apply) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.token) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.token) }}\r\n                        {{ form_widget(form.token) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(form.is_public) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(form.is_public) }}\r\n                        {{ form_widget(form.is_public) }}\r\n                        &lt;br \/&gt; Whether the job can also be published on affiliate websites or not.\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;\/tbody&gt;\r\n        &lt;\/table&gt;\r\n    {{ form_end(form) }}\r\n{% endblock %}<\/pre>\n<p>We could render the form by just using the following line of code, but as we need more customization, we choose to render each form field by hand.<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">{{ form(form) }}<\/pre>\n<p>By printing <code>form(form)<\/code>, each field in the form is rendered, along with a label and error message (if there is one). As easy as this is, it\u2019s not very flexible (yet). Usually, you\u2019ll want to render each form field individually so you can control how the form looks.<\/p>\n<p>We also used a technique named <a href=\"http:\/\/symfony.com\/doc\/current\/book\/forms.html#form-theming\" target=\"_blank\" rel=\"noopener\">form theming<\/a> to customize how the <a href=\"http:\/\/symfony.com\/doc\/current\/cookbook\/form\/form_customization.html#customizing-error-output\" target=\"_blank\" rel=\"noopener\">form errors<\/a> will be rendered. You can read more about this in the official Symfony2 <a href=\"http:\/\/symfony.com\/doc\/current\/book\/index.html\" target=\"_blank\" rel=\"noopener\">documentation<\/a>.<\/p>\n<p>Do the same thing with the <code>edit.html.twig<\/code> template:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/edit.html.twig\">{% extends 'IbwJobeetBundle::layout.html.twig' %}\r\n\r\n{% form_theme edit_form _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;Job edit&lt;\/h1&gt;\r\n    &lt;form action=\"{{ path('ibw_job_update', { 'id': entity.id }) }}\" method=\"post\" {{ form_enctype(edit_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=\"Preview your job\" \/&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(edit_form.category) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.category) }}\r\n                        {{ form_widget(edit_form.category) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.type) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.type) }}\r\n                        {{ form_widget(edit_form.type) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.company) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.company) }}\r\n                        {{ form_widget(edit_form.company) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.file) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.file) }}\r\n                        {{ form(edit_form.file) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.url) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.url) }}\r\n                        {{ form_widget(edit_form.url) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.position) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.position) }}\r\n                        {{ form_widget(edit_form.position) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.location) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.location) }}\r\n                        {{ form_widget(edit_form.location) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.description) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.description) }}\r\n                        {{ form_widget(edit_form.description) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.how_to_apply) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.how_to_apply) }}\r\n                        {{ form_widget(edit_form.how_to_apply) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.token) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.token) }}\r\n                        {{ form_widget(edit_form.token) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.is_public) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.is_public) }}\r\n                        {{ form_widget(edit_form.is_public) }}\r\n                        &lt;br \/&gt; Whether the job can also be published on affiliate websites or not.\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n                &lt;tr&gt;\r\n                    &lt;th&gt;{{ form_label(edit_form.email) }}&lt;\/th&gt;\r\n                    &lt;td&gt;\r\n                        {{ form_errors(edit_form.email) }}\r\n                        {{ form_widget(edit_form.email) }}\r\n                    &lt;\/td&gt;\r\n                &lt;\/tr&gt;\r\n            &lt;\/tbody&gt;\r\n        &lt;\/table&gt;\r\n    {{ form_end(edit_form) }}\r\n{% endblock %}<\/pre>\n<p>&nbsp;<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Form_Action\"><\/span>The Form Action<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We now have a form class and a template that renders it. Now, it\u2019s time to actually make it work with some actions. The job form is managed by four methods in the <code>JobController:<\/code><\/p>\n<ul>\n<li><code>newAction<\/code>: Displays a blank form to create a new job<\/li>\n<li><code>createAction<\/code>: Processes the form (validation, form repopulation) and creates a new job with the user submitted values<\/li>\n<li><code>editAction<\/code>: Displays a form to edit an existing job<\/li>\n<li><code>updateAction<\/code>: Processes the form (validation, form repopulation) and updates an existing job with the user submitted values<\/li>\n<\/ul>\n<p>When you browse to the <code>\/job\/new page<\/code>, a form instance for a new job object is created by calling the <code>createForm()<\/code> method and passed to the template (<code>newAction<\/code>).<\/p>\n<p>When the user submits the form (<code>createAction<\/code>), the form is bound (<code>bind($request)<\/code> method) with the user submitted values and the validation is triggered.<\/p>\n<p>Once the form is bound, it is possible to check its validity using the <code>isValid()<\/code> method: if the form is valid (returns true), the job is saved to the database (<code>$em-&gt;persist($entity)<\/code>), and the user is redirected to the job preview page; if not, the <code>new.html.twig<\/code> template is displayed again with the user submitted values and the associated error messages.<\/p>\n<p>The modification of an existing job is quite similar. The only difference between the <code>new<\/code> and the <code>edit<\/code> action is that the job object to be modified is passed as the second argument of the <code>createForm<\/code> method. This object will be used for default widget values in the template.<\/p>\n<p>You can also define default values for the <code>creation<\/code> form. For this we will pass a pre-modified <code>Job<\/code> object to the <code>createForm()<\/code> method to set the type default value to <code>full-time<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">    \/\/ ...\r\n\r\n    public function newAction()\r\n    {\r\n        $entity = new Job();\r\n        $entity-&gt;setType('full-time');\r\n        $form = $this-&gt;createForm(new JobType(), $entity);\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:new.html.twig', array(\r\n            'entity' =&gt; $entity,\r\n            'form'   =&gt; $form-&gt;createView()\r\n        ));\r\n    }\r\n\r\n    \/\/ ...<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"PROTECTING_THE_JOB_FORM_WITH_A_TOKEN\"><\/span>PROTECTING THE JOB FORM WITH A TOKEN<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Everything must work fine by now. As of now, the user must enter the token for the job. But the job token must be generated automatically when a new job is created, as we don\u2019t want to rely on the user to provide a unique token. Add the <code>setTokenValue<\/code> method to the <code>prePersist lifecycleCallbacks<\/code> for the <code>Job<\/code> entity:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/doctrine\/Job.orm.yml\"># ...\r\n\r\n  lifecycleCallbacks:\r\n     prePersist: [ setTokenValue, preUpload, setCreatedAtValue, setExpiresAtValue ]\r\n     # ...<\/pre>\n<p>Regenerate the <code>doctrine entities<\/code> to apply this modification:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console doctrine:generate:entities IbwJobeetBundle<\/pre>\n<p>Edit the <code>setTokenValue()<\/code> method of the <code>Job<\/code> entity to add the logic that generates the token before a new job is saved:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">    \/\/ ...\r\n\r\n    public function setTokenValue()\r\n    {\r\n        if(!$this-&gt;getToken()) {\r\n            $this-&gt;token = sha1($this-&gt;getEmail().rand(11111, 99999));\r\n        }\r\n    }\r\n\r\n    \/\/ ...<\/pre>\n<p>You can now remove the <code>token<\/code> field from the form:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Form\/JobType.php\">\/\/ ...\r\n\r\n    public function buildForm(FormBuilderInterface $builder, array $options)\r\n    {\r\n        $builder\r\n            -&gt;add('category')\r\n            -&gt;add('type', 'choice', array('choices' =&gt; Job::getTypes(), 'expanded' =&gt; true))\r\n            -&gt;add('company')\r\n            -&gt;add('file', 'file', array('label' =&gt; 'Company logo', 'required' =&gt; false))\r\n            -&gt;add('url')\r\n            -&gt;add('position')\r\n            -&gt;add('location')\r\n            -&gt;add('description')\r\n            -&gt;add('how_to_apply', null, array('label' =&gt; 'How to apply?'))\r\n            -&gt;add('is_public', null, array('label' =&gt; 'Public?'))\r\n            -&gt;add('email')\r\n        ;\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>Remove it from the <code>new.html.twig<\/code> and <code>edit.html.twig<\/code> templates also:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/new.html.twig\">&lt;!-- ... --&gt;\r\n&lt;tr&gt;\r\n    &lt;th&gt;{{ form_label(form.token) }}&lt;\/th&gt;\r\n    &lt;td&gt;\r\n        {{ form_errors(form.token) }}\r\n        {{ form_widget(form.token) }}\r\n    &lt;\/td&gt;\r\n&lt;\/tr&gt;\r\n&lt;!-- ... --&gt;<\/pre>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/edit.html.twig\">&lt;!-- ... --&gt;\r\n&lt;tr&gt;\r\n    &lt;th&gt;{{ form_label(edit_form.token) }}&lt;\/th&gt;\r\n    &lt;td&gt;\r\n        {{ form_errors(edit_form.token) }}\r\n        {{ form(edit_form.token) }}\r\n    &lt;\/td&gt;\r\n&lt;\/tr&gt;\r\n&lt;!-- ... --&gt;<\/pre>\n<p>And from the <code>validation.yml<\/code> file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/validation.yml\"># ...\r\n    # ...\r\n    token:\r\n        - NotBlank: ~<\/pre>\n<p>If you remember the user stories from day 2, a job can be edited only if the user knows the associated token. Right now, it is pretty easy to edit or delete any job, just by guessing the URL. That\u2019s because the edit URL is like <code>\/job\/ID\/edit<\/code>, where <code>ID<\/code> is the primary key of the job.<\/p>\n<p>Let\u2019s change the routes so you can edit or delete a job only if you now the secret token:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing\/job.yml\"># ...\r\n\r\nibw_job_edit:\r\n    pattern:  \/{token}\/edit\r\n    defaults: { _controller: \"IbwJobeetBundle:Job:edit\" }\r\n\r\nibw_job_update:\r\n    pattern:  \/{token}\/update\r\n    defaults: { _controller: \"IbwJobeetBundle:Job:update\" }\r\n    requirements: { _method: post|put }\r\n\r\nibw_job_delete:\r\n    pattern:  \/{token}\/delete\r\n    defaults: { _controller: \"IbwJobeetBundle:Job:delete\" }\r\n    requirements: { _method: post|delete }<\/pre>\n<p>Now edit the <code>JobController<\/code> to use the token instead of the id:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\nclass JobController extends Controller\r\n{\r\n    \/\/ ...\r\n\r\n    public function editAction($token)\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);\r\n\r\n        if (!$entity) {\r\n            throw $this-&gt;createNotFoundException('Unable to find Job entity.');\r\n        }\r\n\r\n        $editForm = $this-&gt;createForm(new JobType(), $entity);\r\n        $deleteForm = $this-&gt;createDeleteForm($token);\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:edit.html.twig', array(\r\n            'entity'      =&gt; $entity,\r\n            'edit_form'   =&gt; $editForm-&gt;createView(),\r\n            'delete_form' =&gt; $deleteForm-&gt;createView(),\r\n        ));\r\n    }   \r\n\r\n    public function updateAction(Request $request, $token)\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);\r\n\r\n        if (!$entity) {\r\n            throw $this-&gt;createNotFoundException('Unable to find Job entity.');\r\n        }\r\n\r\n        $editForm   = $this-&gt;createForm(new JobType(), $entity);\r\n        $deleteForm = $this-&gt;createDeleteForm($token);\r\n\r\n        $editForm-&gt;bind($request);\r\n\r\n        if ($editForm-&gt;isValid()) {\r\n            $em-&gt;persist($entity);\r\n            $em-&gt;flush();\r\n\r\n            return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_edit', array('token' =&gt; $token)));\r\n        }\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:edit.html.twig', array(\r\n            'entity'      =&gt; $entity,\r\n            'edit_form'   =&gt; $editForm-&gt;createView(),\r\n            'delete_form' =&gt; $deleteForm-&gt;createView(),\r\n        ));\r\n    }\r\n\r\n    public function deleteAction(Request $request, $token)\r\n    {\r\n        $form = $this-&gt;createDeleteForm($token);\r\n        $form-&gt;bind($request);\r\n\r\n        if ($form-&gt;isValid()) {\r\n            $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n            $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);\r\n\r\n            if (!$entity) {\r\n                throw $this-&gt;createNotFoundException('Unable to find Job entity.');\r\n            }\r\n\r\n            $em-&gt;remove($entity);\r\n            $em-&gt;flush();\r\n        }\r\n\r\n        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job'));\r\n    }\r\n\r\n    \/**\r\n     * Creates a form to delete a Job entity by id.\r\n     *\r\n     * @param mixed $id The entity id\r\n     *\r\n     * @return SymfonyComponentFormForm The form\r\n     *\/\r\n    private function createDeleteForm($token)\r\n    {\r\n        return $this-&gt;createFormBuilder(array('token' =&gt; $token))\r\n            -&gt;add('token', 'hidden')\r\n            -&gt;getForm()\r\n        ;\r\n    }\r\n}<\/pre>\n<p>In the job show template <code>show.html.twig<\/code>, change the <code>ibw_job_edit<\/code> route parameter:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/show.html.twig\">&lt;a href=\"{{ path('ibw_job_edit', {'token': entity.token}) }}\"&gt;<\/pre>\n<p>Do the same for <code>ibw_job_update<\/code> route in <code>edit.html.twig<\/code> job template:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/edit.html.twig\">&lt;form action=\"{{ path('ibw_job_update', {'token': entity.token}) }}\" method=\"post\" {{ form_enctype(edit_form) }}&gt;<\/pre>\n<p>Now, all routes related to the jobs, except the <code>job_show_user<\/code> one, embed the token. For instance, the route to edit a job is now of the following pattern:<br \/>\n<code>http:\/\/jobeet.local\/job\/TOKEN\/edit<\/code><\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Preview_Page\"><\/span>The Preview Page<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The preview page is the same as the job page display. The only difference is that the job preview page will be accessed using the job token instead of the job id:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing\/job.yml\"># ...\r\n\r\nibw_job_show:\r\n    pattern:  \/{company}\/{location}\/{id}\/{position}\r\n    defaults: { _controller: \"IbwJobeetBundle:Job:show\" }\r\n    requirements:\r\n        id:  d+\r\n\r\nibw_job_preview:\r\n    pattern:  \/{company}\/{location}\/{token}\/{position}\r\n    defaults: { _controller: \"IbwJobeetBundle:Job:preview\" }\r\n    requirements:\r\n        token:  w+\r\n\r\n# ...<\/pre>\n<p>The <code>preview<\/code> action (here the difference from the <code>show<\/code> action is that the job is retrieved from the database using the provided token instead of the id):<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\n    public function previewAction($token)\r\n    {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n\r\n        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);\r\n\r\n        if (!$entity) {\r\n            throw $this-&gt;createNotFoundException('Unable to find Job entity.');\r\n        }\r\n\r\n        $deleteForm = $this-&gt;createDeleteForm($entity-&gt;getId());\r\n\r\n        return $this-&gt;render('IbwJobeetBundle:Job:show.html.twig', array(\r\n            'entity'      =&gt; $entity,\r\n            'delete_form' =&gt; $deleteForm-&gt;createView(),\r\n        ));\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>If the user comes in with the tokenized URL, we will add an admin bar at the top. At the beginning of the <code>show.html.twig<\/code> template, include a template to host the admin bar and remove the <code>edit<\/code> link at the bottom:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/show.html.twig\">&lt;!-- ... --&gt;\r\n\r\n{% block content %}\r\n    {% if app.request.get('token') %}\r\n        {% include 'IbwJobeetBundle:Job:admin.html.twig' with {'job': entity} %}\r\n    {% endif %}\r\n\r\n &lt;!-- ... --&gt;\r\n\r\n{% endblock %}<\/pre>\n<p>Then, create the <code>admin.html.twig<\/code> template:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/admin.html.twig\">&lt;div id=\"job_actions\"&gt;\r\n    &lt;h3&gt;Admin&lt;\/h3&gt;\r\n    &lt;ul&gt;\r\n        {% if not job.isActivated %}\r\n            &lt;li&gt;&lt;a href=\"{{ path('ibw_job_edit', { 'token': job.token }) }}\"&gt;Edit&lt;\/a&gt;&lt;\/li&gt;\r\n            &lt;li&gt;&lt;a href=\"{{ path('ibw_job_edit', { 'token': job.token }) }}\"&gt;Publish&lt;\/a&gt;&lt;\/li&gt;\r\n        {% endif %}\r\n        &lt;li&gt;\r\n            &lt;form action=\"{{ path('ibw_job_delete', { 'token': job.token }) }}\" method=\"post\"&gt;\r\n                {{ form_widget(delete_form) }}\r\n                &lt;button type=\"submit\" onclick=\"if(!confirm('Are you sure?')) { return false; }\"&gt;Delete&lt;\/button&gt;\r\n            &lt;\/form&gt;\r\n        &lt;\/li&gt;\r\n        {% if job.isActivated %}\r\n            &lt;li {% if job.expiresSoon %} class=\"expires_soon\" {% endif %}&gt;\r\n                {% if job.isExpired %}\r\n                    Expired\r\n                {% else %}\r\n                    Expires in &lt;strong&gt;{{ job.getDaysBeforeExpires }}&lt;\/strong&gt; days\r\n                {% endif %}\r\n\r\n                {% if job.expiresSoon %}\r\n                    - &lt;a href=\"\"&gt;Extend&lt;\/a&gt; for another 30 days\r\n                {% endif %}\r\n            &lt;\/li&gt;\r\n        {% else %}\r\n            &lt;li&gt;\r\n                [Bookmark this &lt;a href=\"{{ url('ibw_job_preview', { 'token': job.token, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}\"&gt;URL&lt;\/a&gt; to manage this job in the future.]\r\n            &lt;\/li&gt;\r\n        {% endif %}\r\n    &lt;\/ul&gt;\r\n&lt;\/div&gt;<\/pre>\n<p>There is a lot of code, but most of the code is simple to understand.<\/p>\n<p>To make the template more readable, we have added a bunch of shortcut methods in the <code>Job<\/code> entity class:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">    \/\/ ...\r\n\r\n    public function isExpired()\r\n    {\r\n        return $this-&gt;getDaysBeforeExpires() &lt; 0;\r\n    }\r\n\r\n    public function expiresSoon()\r\n    {\r\n        return $this-&gt;getDaysBeforeExpires() &lt; 5;    \r\n    }\r\n\r\n    public function getDaysBeforeExpires()\r\n    {\r\n        return ceil(($this-&gt;getExpiresAt()-&gt;format('U') - time()) \/ 86400);\r\n    }\r\n\r\n    \/\/ ...<\/pre>\n<p>The admin bar displays the different actions depending on the job status:<\/p>\n<p><img decoding=\"async\" class=\"alignnone size-medium wp-image-2240\" src=\"https:\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-10-admin-bar-300x101.png\" alt=\"\" width=\"300\" height=\"101\" srcset=\"https:\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-10-admin-bar-300x101.png 300w, https:\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-10-admin-bar-768x258.png 768w, https:\/\/intelligentbee.com\/blog\/wp-content\/uploads\/2017\/03\/Day-10-admin-bar.png 858w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/p>\n<p>&nbsp;<\/p>\n<p>We will now redirect the create and update actions of the <code>JobController<\/code> to the new preview page:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">public function createAction(Request $request)\r\n{\r\n    \/\/ ...\r\n    if ($form-&gt;isValid()) {\r\n        \/\/ ... \r\n        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(\r\n            'company' =&gt; $entity-&gt;getCompanySlug(),\r\n            'location' =&gt; $entity-&gt;getLocationSlug(),\r\n            'token' =&gt; $entity-&gt;getToken(),\r\n            'position' =&gt; $entity-&gt;getPositionSlug()\r\n        )));\r\n    }\r\n    \/\/ ...\r\n}\r\n\r\npublic function updateAction(Request $request, $token)\r\n{\r\n    \/\/ ...\r\n    if ($editForm-&gt;isValid()) {\r\n        \/\/ ... \r\n\r\n        return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(\r\n            'company' =&gt; $entity-&gt;getCompanySlug(),\r\n            'location' =&gt; $entity-&gt;getLocationSlug(),\r\n            'token' =&gt; $entity-&gt;getToken(), \r\n            'position' =&gt; $entity-&gt;getPositionSlug()\r\n        )));\r\n    }\r\n    \/\/ ...\r\n}<\/pre>\n<p>As we said before, you can edit a job only if you know the job token and you\u2019re the admin of the site. At the moment, when you access a job page, you will see the <code>Edit<\/code> link and that\u2019s bad. Let\u2019s remove it from the <code>show.html.twig<\/code> file:<\/p>\n<pre class=\"toolbar:2 nums:false lang:xhtml decode:true \">&lt;div style=\"padding: 20px 0\"&gt;\r\n    &lt;a href=\"{{ path('ibw_job_edit', { 'token': entity.token }) }}\"&gt;\r\n        Edit\r\n    &lt;\/a&gt;\r\n&lt;\/div&gt;<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Job_Activation_and_Publication\"><\/span>Job Activation and Publication<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In the previous section, there is a link to publish the job. The link needs to be changed to point to a new publish action. For this we will create new route:<\/p>\n<pre class=\"lang:yaml decode:true\" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/routing\/job.yml\"># ...\r\n\r\nibw_job_publish:\r\n    pattern:  \/{token}\/publish\r\n    defaults: { _controller: \"IbwJobeetBundle:Job:publish\" }\r\n    requirements: { _method: post }<\/pre>\n<p>We can now change the link of the <code>Publish<\/code> link (we will use a form here, like when deleting a job, so we will have a POST request):<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/Job\/admin.html.twig\">&lt;!-- ... --&gt;\r\n\r\n{% if not job.isActivated %}\r\n    &lt;li&gt;&lt;a href=\"{{ path('ibw_job_edit', { 'token': job.token }) }}\"&gt;Edit&lt;\/a&gt;&lt;\/li&gt;\r\n    &lt;li&gt;\r\n        &lt;form action=\"{{ path('ibw_job_publish', { 'token': job.token }) }}\" method=\"post\"&gt;\r\n            {{ form_widget(publish_form) }}\r\n            &lt;button type=\"submit\"&gt;Publish&lt;\/button&gt;\r\n        &lt;\/form&gt;\r\n    &lt;\/li&gt;\r\n{% endif %}\r\n\r\n&lt;!-- ... --&gt;<\/pre>\n<p>The last step is to create the <code>publish<\/code> action, the <code>publish<\/code> form and to edit the <code>preview<\/code> action to send the <code>publish<\/code> form to the template:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobController.php\">\/\/ ...\r\n\r\npublic function previewAction($token)\r\n{\r\n    \/\/ ...\r\n\r\n    $deleteForm = $this-&gt;createDeleteForm($entity-&gt;getToken());\r\n    $publishForm = $this-&gt;createPublishForm($entity-&gt;getToken());\r\n\r\n    return $this-&gt;render('IbwJobeetBundle:Job:show.html.twig', array(\r\n        'entity'      =&gt; $entity,\r\n        'delete_form' =&gt; $deleteForm-&gt;createView(),\r\n        'publish_form' =&gt; $publishForm-&gt;createView(),\r\n    ));\r\n}\r\n\r\npublic function publishAction(Request $request, $token)\r\n{\r\n    $form = $this-&gt;createPublishForm($token);\r\n    $form-&gt;bind($request);\r\n\r\n    if ($form-&gt;isValid()) {\r\n        $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n        $entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findOneByToken($token);\r\n\r\n        if (!$entity) {\r\n            throw $this-&gt;createNotFoundException('Unable to find Job entity.');\r\n        }\r\n\r\n        $entity-&gt;publish();\r\n        $em-&gt;persist($entity);\r\n        $em-&gt;flush();\r\n\r\n        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('notice', 'Your job is now online for 30 days.');\r\n    }\r\n\r\n    return $this-&gt;redirect($this-&gt;generateUrl('ibw_job_preview', array(\r\n        'company' =&gt; $entity-&gt;getCompanySlug(),\r\n        'location' =&gt; $entity-&gt;getLocationSlug(),\r\n        'token' =&gt; $entity-&gt;getToken(),\r\n        'position' =&gt; $entity-&gt;getPositionSlug()\r\n    )));\r\n}\r\n\r\nprivate function createPublishForm($token)\r\n{\r\n    return $this-&gt;createFormBuilder(array('token' =&gt; $token))\r\n        -&gt;add('token', 'hidden')\r\n        -&gt;getForm()\r\n    ;\r\n}\r\n\r\n\/\/ ...<\/pre>\n<p>The <code>publishAction()<\/code> method uses a new <code>publish()<\/code> method that can be defined as follows:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Entity\/Job.php\">\/\/ ...\r\n\r\npublic function publish()\r\n{\r\n    $this-&gt;setIsActivated(true);\r\n}\r\n\r\n\/\/ ...<\/pre>\n<p>You can now test the new <code>publish<\/code> feature in your browser.<\/p>\n<p>But we still have something to fix. The non-activated jobs must not be accessible, which means that they must not show up on the Jobeet homepage, and must not be accessible by their URL. We need to edit the <code>JobRepository<\/code> methods to add this requirement:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/JobRepository.php\">namespace IbwJobeetBundleRepository;\r\nuse DoctrineORMEntityRepository;\r\n\r\nclass JobRepository extends EntityRepository\r\n{\r\n    public function getActiveJobs($category_id = null, $max = null, $offset = 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\r\n        $query = $qb-&gt;getQuery();\r\n\r\n        return $query-&gt;getResult();\r\n    }\r\n\r\n    public function countActiveJobs($category_id = null)\r\n    {\r\n        $qb = $this-&gt;createQueryBuilder('j')\r\n            -&gt;select('count(j.id)')\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\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;getSingleScalarResult();\r\n    }\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;andWhere('j.is_activated = :activated')\r\n            -&gt;setParameter('activated', 1)\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    }\r\n}<\/pre>\n<p>The same for <code>CategoryRepository getWithJobs()<\/code> method:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Repository\/CategoryRepository.php\">namespace IbwJobeetBundleRepository;\r\nuse DoctrineORMEntityRepository;\r\n\r\nclass CategoryRepository extends EntityRepository\r\n{\r\n    public function getWithJobs()\r\n    {\r\n        $query = $this-&gt;getEntityManager()\r\n            -&gt;createQuery('SELECT c FROM IbwJobeetBundle:Category c LEFT JOIN c.jobs j WHERE j.expires_at &gt; :date AND j.is_activated = :activated')\r\n            -&gt;setParameter('date', date('Y-m-d H:i:s', time()))\r\n            -&gt;setParameter('activated', 1);\r\n\r\n        return $query-&gt;getResult();\r\n    }\r\n}<\/pre>\n<h4>That\u2019s all. You can test it now in your browser. All non-activated jobs have disappeared from the homepage; even if you know their URLs, they are not accessible anymore. They are, however, accessible if one knows the job\u2019s token URL. In that case, the job preview will show up with the admin bar.<\/h4>\n<p><a href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/\" rel=\"license\"><img decoding=\"async\" style=\"border-width: 0;\" src=\"https:\/\/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. Forms in jobeet [&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":[1375,1385,1383,1382,1381,1380,1379,1378,1377,1376,1013,1374,1373,1372,1371,1370,1369,1356,1355],"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2234"}],"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=2234"}],"version-history":[{"count":7,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2234\/revisions"}],"predecessor-version":[{"id":133236,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2234\/revisions\/133236"}],"wp:attachment":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/media?parent=2234"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/categories?post=2234"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/tags?post=2234"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/yst_prominent_words?post=2234"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}