{"id":2254,"date":"2013-08-18T09:38:42","date_gmt":"2013-08-18T09:38:42","guid":{"rendered":"https:\/\/intelligentbee.com\/blog\/?p=2254"},"modified":"2024-09-30T07:44:57","modified_gmt":"2024-09-30T07:44:57","slug":"symfony2-jobeet-day-12-sonata-admin-bundle","status":"publish","type":"post","link":"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-12-sonata-admin-bundle\/","title":{"rendered":"Symfony2 Jobeet Day 12: Sonata Admin Bundle"},"content":{"rendered":"<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_68_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title \" >Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/intelligentbee.com\/blog\/symfony2-jobeet-day-12-sonata-admin-bundle\/#_This_article_is_part_of_the_original_Jobeet_Tutorial_created_by_Fabien_Potencier_for_Symfony_14_Sonata_admin_bundle_in_jobeet\" title=\"* This article is part of the original Jobeet Tutorial, created by Fabien Potencier, for Symfony 1.4. \nSonata admin bundle in jobeet\">* This article is part of the original Jobeet Tutorial, created by Fabien Potencier, for Symfony 1.4. \nSonata admin bundle 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-12-sonata-admin-bundle\/#Installation_of_the_Sonata_Admin_Bundle\" title=\"Installation of the Sonata Admin Bundle\">Installation of the Sonata Admin Bundle<\/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-12-sonata-admin-bundle\/#The_CRUD_Controller\" title=\"The CRUD Controller\">The CRUD Controller<\/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-12-sonata-admin-bundle\/#Creating_the_Admin_class\" title=\"Creating the Admin class\">Creating the Admin class<\/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-12-sonata-admin-bundle\/#Configuration_of_Admin_classes\" title=\"Configuration of Admin classes\">Configuration of Admin classes<\/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-12-sonata-admin-bundle\/#Batch_Actions\" title=\"Batch Actions\">Batch Actions<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"_This_article_is_part_of_the_original_Jobeet_Tutorial_created_by_Fabien_Potencier_for_Symfony_14_Sonata_admin_bundle_in_jobeet\"><\/span><span style=\"font-family: timesnew roman; font-size: 12px;\">* This article is part of the original <a href=\"http:\/\/symfony.com\/legacy\/doc\/jobeet?orm=Doctrine\" target=\"_blank\" rel=\"noopener\">Jobeet Tutorial<\/a>, created by Fabien Potencier, for Symfony 1.4.<\/span><br \/>\nSonata admin bundle in jobeet<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>With the addition we made in Day 11 on Jobeet, the application is now fully usable by job seekers and job posters. It\u2019s time to talk a bit about the <strong>admin<\/strong> section of our application. Today, thanks to the Sonata Admin Bundle, we will develop a complete admin interface for Jobeet in less than an hour.<\/p>\n<p>&nbsp;<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Installation_of_the_Sonata_Admin_Bundle\"><\/span>Installation of the Sonata Admin Bundle<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Start by downloading <code>SonataAdminBundle<\/code> and its dependencies to the <code>vendor<\/code> directory:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php composer.phar require sonata-project\/admin-bundle<\/pre>\n<p>To install the latest version of the SonataAdminBundle and its dependencies, give <code>*<\/code> as input.<\/p>\n<pre class=\"toolbar:2 lang:default decode:true \">ibw@ubuntu:\/var\/www\/jobeet$ php composer.phar require sonata-project\/admin-bundle\r\nPlease provide a version constraint for the sonata-project\/admin-bundle requirement: *<\/pre>\n<p>We will also need to install the <code>SonataDoctrineORMADminBundle:<\/code><\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php composer.phar require sonata-project\/doctrine-orm-admin-bundle<\/pre>\n<p>Now, we need to declare these new bundles and dependencies, so go to your <code>AppKernel.php <\/code>file and add the following code:<\/p>\n<pre class=\"lang:php decode:true \" title=\"app\/AppKernel.php\">\/\/ ...\r\n    public function registerBundles()\r\n    {\r\n        $bundles = array(\r\n            \/\/ ...\r\n            new SonataAdminBundleSonataAdminBundle(),\r\n            new SonataBlockBundleSonataBlockBundle(),\r\n            new SonatajQueryBundleSonatajQueryBundle(),\r\n            new SonataDoctrineORMAdminBundleSonataDoctrineORMAdminBundle(),\r\n            new KnpBundleMenuBundleKnpMenuBundle(),\r\n        );\r\n    }\r\n\r\n\/\/ ...<\/pre>\n<p>You will need to alter your <code>config<\/code> file as well. Add the following at the end:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"app\/config\/config.yml\"># ...\r\nsonata_admin:\r\n    title: Jobeet Admin\r\n\r\nsonata_block:\r\n    default_contexts: [cms]\r\n    blocks:\r\n        sonata.admin.block.admin_list:\r\n            contexts:   [admin]\r\n\r\n        sonata.block.service.text:\r\n        sonata.block.service.action:\r\n        sonata.block.service.rss:<\/pre>\n<p>Also, look for the <code>translator<\/code> key and uncomment if it is commented:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"app\/config\/config.yml\"># ...\r\nframework:\r\n    # ...\r\n    translator: { fallback: %locale%}\r\n    # ...\r\n#...<\/pre>\n<p>For your application to work, you need to import the <code>admin routes<\/code> into the application\u2019s routing file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"app\/config\/routing.yml\">admin:\r\n    resource: '@SonataAdminBundle\/Resources\/config\/routing\/sonata_admin.xml'\r\n    prefix: \/admin\r\n\r\n_sonata_admin:\r\n    resource: .\r\n    type: sonata_admin\r\n    prefix: \/admin\r\n\r\n# ...<\/pre>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console assets:install web --symlink<\/pre>\n<p>Do not forget to delete your cache:<\/p>\n<pre class=\"toolbar:2 nums:false lang:default decode:true \">php app\/console cache:clear --env=dev\r\nphp app\/console cache:clear --env=prod<\/pre>\n<p>You should now be able to access the admin dashboard using the following url: http:\/\/jobeet.local\/app_dev.php\/admin\/dashboard<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_CRUD_Controller\"><\/span>The CRUD Controller<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The <code>CRUD controller<\/code> contains the basic CRUD actions. It is related to one Admin class by mapping the controller name to the correct Admin instance. Any or all actions can be overwritten to suit the project\u2019s requirements. The controller uses the Admin class to construct the different actions. Inside the controller, the Admin object is accessible through the configuration property.<\/p>\n<p>Now let\u2019s create a controller for each entity. First, for the <code>Category<\/code> entity:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/CategoryAdminController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SonataAdminBundleControllerCRUDController as Controller;\r\n\r\nclass CategoryAdminController extends Controller\r\n{\r\n    \/\/ Your code will be here\r\n}<\/pre>\n<p>And now for the <code>Job<\/code>:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobAdminController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SonataAdminBundleControllerCRUDController as Controller;\r\n\r\nclass JobAdminController extends Controller\r\n{\r\n    \/\/ Your code will be here\r\n}<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Creating_the_Admin_class\"><\/span>Creating the Admin class<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The <code>Admin<\/code> class represents the mapping of your model and administration sections (forms, list, show). The easiest way to create an <code>admin<\/code> class for your model is to extend the <code>SonataAdminBundleAdminAdmin<\/code> class. We will create the <code>Admin<\/code> classes in the <code>Admin<\/code> folder of our bundle. Start by creating the <code>Admin<\/code> directory and then, the <code>Admin<\/code> class for categories:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/CategoryAdmin.php\">namespace IbwJobeetBundleAdmin;\r\n\r\nuse SonataAdminBundleAdminAdmin;\r\nuse SonataAdminBundleDatagridListMapper;\r\nuse SonataAdminBundleDatagridDatagridMapper;\r\nuse SonataAdminBundleValidatorErrorElement;\r\nuse SonataAdminBundleFormFormMapper;\r\n\r\nclass CategoryAdmin extends Admin\r\n{\r\n    \/\/ Your code will be here\r\n}<\/pre>\n<p>And for jobs:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/JobAdmin.php\">namespace IbwJobeetBundleAdmin;\r\n\r\nuse SonataAdminBundleAdminAdmin;\r\nuse SonataAdminBundleDatagridListMapper;\r\nuse SonataAdminBundleDatagridDatagridMapper;\r\nuse SonataAdminBundleValidatorErrorElement;\r\nuse SonataAdminBundleFormFormMapper;\r\nuse SonataAdminBundleShowShowMapper;\r\nuse IbwJobeetBundleEntityJob;\r\n\r\nclass JobAdmin extends Admin\r\n{\r\n    \/\/ Your code will be here\r\n}<\/pre>\n<p>Now we need to add each admin class in the <code>services.yml<\/code> configuration file:<\/p>\n<pre class=\"lang:yaml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/config\/services.yml\">services:\r\n    ibw.jobeet.admin.category:\r\n        class: IbwJobeetBundleAdminCategoryAdmin\r\n        tags:\r\n            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Categories }\r\n        arguments:\r\n            - ~\r\n            - IbwJobeetBundleEntityCategory\r\n            - 'IbwJobeetBundle:CategoryAdmin'\r\n\r\n    ibw.jobeet.admin.job:\r\n        class: IbwJobeetBundleAdminJobAdmin\r\n        tags:\r\n            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Jobs }\r\n        arguments:\r\n            - ~\r\n            - IbwJobeetBundleEntityJob\r\n            - 'IbwJobeetBundle:JobAdmin'<\/pre>\n<p>At this point, we can see in the dashboard the Jobeet group and, inside it, the <code>Job<\/code> and <code>Category<\/code> modules, with their respective <code>add<\/code> and <code>list<\/code> links.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Configuration_of_Admin_classes\"><\/span>Configuration of Admin classes<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>If you follow any link right now, nothing will happen. That\u2019s because we haven\u2019t configure the fields that belong to the list and the form. Let\u2019s do a basic configuration, first for the categories:<\/p>\n<pre class=\"lang:php decode:true\" title=\"src\/Ibw\/JobeetBundle\/Admin\/CategoryAdmin.php\">namespace IbwJobeetBundleAdmin;\r\n\r\nuse SonataAdminBundleAdminAdmin;\r\nuse SonataAdminBundleDatagridListMapper;\r\nuse SonataAdminBundleDatagridDatagridMapper;\r\nuse SonataAdminBundleValidatorErrorElement;\r\nuse SonataAdminBundleFormFormMapper;\r\n\r\nclass CategoryAdmin extends Admin\r\n{\r\n    \/\/ setup the default sort column and order\r\n    protected $datagridValues = array(\r\n        '_sort_order' =&gt; 'ASC',\r\n        '_sort_by' =&gt; 'name'\r\n    );\r\n\r\n    protected function configureFormFields(FormMapper $formMapper)\r\n    {\r\n        $formMapper\r\n            -&gt;add('name')\r\n            -&gt;add('slug')\r\n        ;\r\n    }\r\n\r\n    protected function configureDatagridFilters(DatagridMapper $datagridMapper)\r\n    {\r\n        $datagridMapper\r\n            -&gt;add('name')\r\n        ;\r\n    }\r\n\r\n    protected function configureListFields(ListMapper $listMapper)\r\n    {\r\n        $listMapper\r\n            -&gt;addIdentifier('name')\r\n            -&gt;add('slug')\r\n        ;\r\n    }\r\n}<\/pre>\n<p>And now for jobs:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/JobAdmin.php\">namespace IbwJobeetBundleAdmin;\r\n\r\nuse SonataAdminBundleAdminAdmin;\r\nuse SonataAdminBundleDatagridListMapper;\r\nuse SonataAdminBundleDatagridDatagridMapper;\r\nuse SonataAdminBundleValidatorErrorElement;\r\nuse SonataAdminBundleFormFormMapper;\r\nuse SonataAdminBundleShowShowMapper;\r\nuse IbwJobeetBundleEntityJob;\r\n\r\nclass JobAdmin extends Admin\r\n{\r\n    \/\/ setup the defaut sort column and order\r\n    protected $datagridValues = array(\r\n        '_sort_order' =&gt; 'DESC',\r\n        '_sort_by' =&gt; 'created_at'\r\n    );\r\n\r\n    protected function configureFormFields(FormMapper $formMapper)\r\n    {\r\n        $formMapper\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')\r\n            -&gt;add('is_public')\r\n            -&gt;add('email')\r\n            -&gt;add('is_activated')\r\n        ;\r\n    }\r\n\r\n    protected function configureDatagridFilters(DatagridMapper $datagridMapper)\r\n    {\r\n        $datagridMapper\r\n            -&gt;add('category')\r\n            -&gt;add('company')\r\n            -&gt;add('position')\r\n            -&gt;add('description')\r\n            -&gt;add('is_activated')\r\n            -&gt;add('is_public')\r\n            -&gt;add('email')\r\n            -&gt;add('expires_at')\r\n        ;\r\n    }\r\n\r\n    protected function configureListFields(ListMapper $listMapper)\r\n    {\r\n        $listMapper\r\n            -&gt;addIdentifier('company')\r\n            -&gt;add('position')\r\n            -&gt;add('location')\r\n            -&gt;add('url')\r\n            -&gt;add('is_activated')\r\n            -&gt;add('email')\r\n            -&gt;add('category')\r\n            -&gt;add('expires_at')\r\n            -&gt;add('_action', 'actions', array(\r\n                'actions' =&gt; array(\r\n                    'view' =&gt; array(),\r\n                    'edit' =&gt; array(),\r\n                    'delete' =&gt; array(),\r\n                )\r\n            ))\r\n        ;\r\n    }\r\n\r\n    protected function configureShowField(ShowMapper $showMapper)\r\n    {\r\n        $showMapper\r\n            -&gt;add('category')\r\n            -&gt;add('type')\r\n            -&gt;add('company')\r\n            -&gt;add('webPath', 'string', array('template' =&gt; 'IbwJobeetBundle:JobAdmin:list_image.html.twig'))\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('is_public')\r\n            -&gt;add('is_activated')\r\n            -&gt;add('token')\r\n            -&gt;add('email')\r\n            -&gt;add('expires_at')\r\n        ;\r\n    }\r\n}<\/pre>\n<p>For the <code>show<\/code> action we used a custom template to show the <code>logo<\/code> of the company:<\/p>\n<pre class=\"lang:xhtml decode:true \" title=\"src\/Ibw\/JobeetBundle\/Resources\/views\/JobAdmin\/list_image.html.twig\">&lt;tr&gt;\r\n    &lt;th&gt;Logo&lt;\/th&gt;\r\n    &lt;td&gt;&lt;img src=\"{{ asset(object.webPath) }}\" \/&gt;&lt;\/td&gt;\r\n&lt;\/tr&gt;<\/pre>\n<p>With this, we created a basic administration module with operations for our jobs and categories. Some of the features you will find when using it are:<\/p>\n<ul>\n<li>The list of objects is paginated<\/li>\n<li>The list is sortable<\/li>\n<li>The list can be filtered<\/li>\n<li>Objects can be created, edited, and deleted<\/li>\n<li>Selected objects can be deleted in a batch<\/li>\n<li>The form validation is enabled<\/li>\n<li>Flash messages give immediate feedback to the user<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"Batch_Actions\"><\/span>Batch Actions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Batch actions are actions triggered on a set of selected models (all of them or only a specific subset). You can easily add some custom batch action in the list view. By default, the <code>delete<\/code> action allows you to remove several entries at once.<\/p>\n<p>To add a new batch action we have to override the <code>getBatchActions<\/code> from the Admin class. We will define here a new <code>extend<\/code> action:<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/JobAdmin.php\">\/\/ ...\r\n\r\npublic function getBatchActions()\r\n{\r\n    \/\/ retrieve the default (currently only the delete action) actions\r\n    $actions = parent::getBatchActions();\r\n\r\n    \/\/ check user permissions\r\n    if($this-&gt;hasRoute('edit') &amp;&amp; $this-&gt;isGranted('EDIT') &amp;&amp; $this-&gt;hasRoute('delete') &amp;&amp; $this-&gt;isGranted('DELETE')) {\r\n        $actions['extend'] = array(\r\n            'label'            =&gt; 'Extend',\r\n            'ask_confirmation' =&gt; true \/\/ If true, a confirmation will be asked before performing the action\r\n        );\r\n\r\n    }\r\n\r\n    return $actions;\r\n}<\/pre>\n<p>The method <code>batchActionExtend<\/code> form the <code>JobAdminController<\/code> will be executed to achieve the core logic. The selected models are passed to the method through a query argument retrieving them. If for some reason it makes sense to perform your batch action without the default selection method (for example you defined another way, at template level, to select model at a lower granularity), the passed query is null.<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobAdminController.php\">namespace IbwJobeetBundleController;\r\n\r\nuse SonataAdminBundleControllerCRUDController as Controller;\r\nuse SonataDoctrineORMAdminBundleDatagridProxyQuery as ProxyQueryInterface;\r\nuse SymfonyComponentHttpFoundationRedirectResponse;\r\n\r\nclass JobAdminController extends Controller\r\n{\r\n    public function batchActionExtend(ProxyQueryInterface $selectedModelQuery)\r\n    {\r\n        if ($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {\r\n            throw new AccessDeniedException();\r\n        }\r\n\r\n        $modelManager = $this-&gt;admin-&gt;getModelManager();\r\n\r\n        $selectedModels = $selectedModelQuery-&gt;execute();\r\n\r\n        try {\r\n            foreach ($selectedModels as $selectedModel) {\r\n                $selectedModel-&gt;extend();\r\n                $modelManager-&gt;update($selectedModel);\r\n            }\r\n        } catch (Exception $e) {\r\n            $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_error', $e-&gt;getMessage());\r\n\r\n            return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n        }\r\n\r\n        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('The selected jobs validity has been extended until %s.', date('m\/d\/Y', time() + 86400 * 30)));\r\n\r\n        return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n    }\r\n}<\/pre>\n<p>Let\u2019s add a new batch action that will delete all jobs that have not been activated by the poster for more than 60 days. For this action we don\u2019t need to select any jobs from the list because the logic of the action will search for the matching records and delete them.<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Admin\/JobAdmin.php\">\/\/ ...\r\n\r\npublic function getBatchActions()\r\n{\r\n    \/\/ retrieve the default (currently only the delete action) actions\r\n    $actions = parent::getBatchActions();\r\n\r\n    \/\/ check user permissions\r\n    if($this-&gt;hasRoute('edit') &amp;&amp; $this-&gt;isGranted('EDIT') &amp;&amp; $this-&gt;hasRoute('delete') &amp;&amp; $this-&gt;isGranted('DELETE')){\r\n        $actions['extend'] = array(\r\n            'label'            =&gt; 'Extend',\r\n            'ask_confirmation' =&gt; true \/\/ If true, a confirmation will be asked before performing the action\r\n        );\r\n\r\n        $actions['deleteNeverActivated'] = array(\r\n            'label'            =&gt; 'Delete never activated jobs',\r\n            'ask_confirmation' =&gt; true \/\/ If true, a confirmation will be asked before performing the action\r\n        );\r\n    }\r\n\r\n    return $actions;\r\n}<\/pre>\n<p>In addition to create the <code>batchActionDeleteNeverActivated<\/code> action, we will create a new method in our <code>JobAdminController<\/code>, <code>batchActionDeleteNeverActivatedIsRelevant<\/code>, that gets executed before any confirmation, to make sure there is actually something to confirm (in our case it will always return true because the selection of the jobs to be deleted is handled by the logic found in the <code>JobRepository::cleanup()<\/code> method.<\/p>\n<pre class=\"lang:php decode:true \" title=\"src\/Ibw\/JobeetBundle\/Controller\/JobAdminController.php\">\/\/ ...\r\n\r\npublic function batchActionDeleteNeverActivatedIsRelevant()\r\n{\r\n    return true;\r\n}\r\n\r\npublic function batchActionDeleteNeverActivated()\r\n{\r\n    if ($this-&gt;admin-&gt;isGranted('EDIT') === false || $this-&gt;admin-&gt;isGranted('DELETE') === false) {\r\n        throw new AccessDeniedException();\r\n    }\r\n\r\n    $em = $this-&gt;getDoctrine()-&gt;getManager();\r\n    $nb = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;cleanup(60);\r\n\r\n    if ($nb) {\r\n        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_success',  sprintf('%d never activated jobs have been deleted successfully.', $nb));\r\n    } else {\r\n        $this-&gt;get('session')-&gt;getFlashBag()-&gt;add('sonata_flash_info',  'No job to delete.');\r\n    }\r\n\r\n    return new RedirectResponse($this-&gt;admin-&gt;generateUrl('list',$this-&gt;admin-&gt;getFilterParameters()));\r\n}<\/pre>\n<h4>That\u2019s all for today! Tomorrow, we will see how to secure the admin section with a username and a password. This will be the occasion to talk about the symfony2 security.<\/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. Sonata admin bundle [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[82],"tags":[],"yst_prominent_words":[798,1133,1395,1398,1410,1556,1948],"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2254"}],"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=2254"}],"version-history":[{"count":3,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2254\/revisions"}],"predecessor-version":[{"id":133232,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/posts\/2254\/revisions\/133232"}],"wp:attachment":[{"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/media?parent=2254"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/categories?post=2254"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/tags?post=2254"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/intelligentbee.com\/blog\/wp-json\/wp\/v2\/yst_prominent_words?post=2254"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}