Symfony2 Jobeet Day 12: The Admin Bundle

With the addition we made in day 11 on Jobeet, the application is now fully usable by job seekers and job posters. It’s time to talk a bit about the admin 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.

Installation of the Admin Bundle

Open your deps file and add these lines:

[SonataCacheBundle]
    git=http://github.com/sonata-project/SonataCacheBundle.git
    target=/bundles/Sonata/CacheBundle
    version=origin/2.0

[SonataBlockBundle]
    git=http://github.com/sonata-project/SonataBlockBundle.git
    target=/bundles/Sonata/BlockBundle
    version=origin/2.0

[SonatajQueryBundle]
    git=http://github.com/sonata-project/SonatajQueryBundle.git
    target=/bundles/Sonata/jQueryBundle

[KnpMenu]
    git=https://github.com/KnpLabs/KnpMenu.git
    version=v1.1.2

[KnpMenuBundle]
    git=https://github.com/KnpLabs/KnpMenuBundle.git
    target=bundles/Knp/Bundle/MenuBundle
    version=v1.1.0

[Exporter]
    git=http://github.com/sonata-project/exporter.git
    target=/exporter

[SonataDoctrineORMAdminBundle]
    git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git
    target=/bundles/Sonata/DoctrineORMAdminBundle
    version=origin/2.0

[SonataAdminBundle]
    git=git://github.com/sonata-project/SonataAdminBundle.git
    target=/bundles/Sonata/AdminBundle
    version=origin/2.0

Run the vendors script to download bundles:

php bin/vendors install --reinstall

Enable the bundles in your autoload.php:

// app/autoload.php

$loader->registerNamespaces(array(
    // ...
    'Sonata' => __DIR__.'/../vendor/bundles',
    'Exporter' => __DIR__.'/../vendor/exporter/lib',
    'Knp\Bundle' => __DIR__.'/../vendor/bundles',
    'Knp\Menu' => __DIR__.'/../vendor/KnpMenu/src',
    // ...
));

and AppKernel.php:

// app/AppKernel.php

public function registerBundles()
{
    return array(
        // ...
        new Sonata\AdminBundle\SonataAdminBundle(),
        new Sonata\BlockBundle\SonataBlockBundle(),
        new Sonata\CacheBundle\SonataCacheBundle(),
        new Sonata\jQueryBundle\SonatajQueryBundle(),
        new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
        new Knp\Bundle\MenuBundle\KnpMenuBundle(),
        // ...
    );
}

You will also need to alter your app/config/config.yml file. Add the following at the end:

# app/config/config.yml
sonata_admin:
    title: Jobeet Admin

sonata_block:
    default_contexts: [cms]
    blocks:
        sonata.admin.block.admin_list:
            contexts:   [admin]

        sonata.block.service.text:
        sonata.block.service.action:
        sonata.block.service.rss:

Also, look for the translator key and uncomment if it is commented:

framework:
    translator:      { fallback: %locale% }

Now, install the assets from the bundles:

php app/console assets:install web

Do not forget to delete your cache:

php app/console cache:clear --env=dev
php app/console cache:clear --env=prod

Now import the admin routes into the application’s routing file:

# app/config/routing.yml

admin:
    resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
    prefix: /admin

_sonata_admin:
    resource: .
    type: sonata_admin
    prefix: /admin

# ...

You should now be able to access the admin dashboard using the following url:

http://jobeet.local/app_dev.php/admin/dashboard

The CRUD Controller

The CRUD controller 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’s requirements. The controller uses the Admin class to construct the different actions. Inside the controller, the Admin object is accessible through the configuration property.

Now lets create a controller for each entity. First for the Category:

// src/Ens/JobeetBundle/Controller/CategoryAdminController.php
namespace Ens\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;

class CategoryAdminController extends Controller
{

}

And now for the Job:

// src/Ens/JobeetBundle/Controller/JobAdminController.php
namespace Ens\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;

class JobAdminController extends Controller
{

}

Creating the Admin class

The Admin class represents the mapping of your model and administration sections (forms, list, show). The easiest way to create an admin class for your model is to extend the Sonata\AdminBundle\Admin\Admin class. We will create the admin classes in the admin folder of our bundle. For categories:

// src/Ens/JobeetBundle/Admin/CategoryAdmin.php

namespace Ens\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;

class CategoryAdmin extends Admin
{
}

And for jobs:

// src/Ens/JobeetBundle/Admin/JobAdmin.php

namespace Ens\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;

class JobAdmin extends Admin
{
}

Now we need to add each admin class in the services.yml configuration file:

# src/Ens/JobeetBundle/Resources/config/services.yml

services:
    ens.jobeet.admin.category:
        class: Ens\JobeetBundle\Admin\CategoryAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Categories }
        arguments: [null, Ens\JobeetBundle\Entity\Category, EnsJobeetBundle:CategoryAdmin]

    ens.jobeet.admin.job:
        class: Ens\JobeetBundle\Admin\JobAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: jobeet, label: Jobs }
        arguments: [null, Ens\JobeetBundle\Entity\Job, EnsJobeetBundle:JobAdmin]

At this point we can see in the dashboard the Jobeet group and inside it the Job and Category modules, with their respective add and list links.

Configuration of Admin classes

At this point, if we follow any link nothing will happen. That’s because we haven’t configure the fields that belong to the list and the form. Let’s do a basic configuration, first for the categories:

// src Ens/JobeetBundle/Admin/CategoryAdmin.php
namespace Ens\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;

class CategoryAdmin extends Admin
{
    // setup the default sort column and order
    protected $datagridValues = array(
        '_sort_order' => 'ASC',
        '_sort_by' => 'name'
    );

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('name')
            ->add('slug')
        ;
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('name')
        ;
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->addIdentifier('name')
            ->add('slug')
        ;
    }
}

And now for the jobs:

// src Ens/JobeetBundle/Admin/JobAdmin.php
namespace Ens\JobeetBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Validator\ErrorElement;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Ens\JobeetBundle\Entity\Job;

class JobAdmin extends Admin
{
    // setup the defaut sort column and order
    protected $datagridValues = array(
        '_sort_order' => 'DESC',
        '_sort_by' => 'created_at'
    );

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('category')
            ->add('type', 'choice', array('choices' => Job::getTypes(), 'expanded' => true))
            ->add('company')
            ->add('file', 'file', array('label' => 'Company logo', 'required' => false))
            ->add('url')
            ->add('position')
            ->add('location')
            ->add('description')
            ->add('how_to_apply')
            ->add('is_public')
            ->add('email')
            ->add('is_activated')
        ;
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('category')
            ->add('company')
            ->add('position')
            ->add('description')
            ->add('is_activated')
            ->add('is_public')
            ->add('email')
            ->add('expires_at')
        ;
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->addIdentifier('company')
            ->add('position')
            ->add('location')
            ->add('url')
            ->add('is_activated')
            ->add('email')
            ->add('category')
            ->add('expires_at')
            ->add('_action', 'actions', array(
                'actions' => array(
                    'view' => array(),
                    'edit' => array(),
                    'delete' => array(),
                )
            ))
        ;
    }

    protected function configureShowField(ShowMapper $showMapper)
    {
        $showMapper
            ->add('category')
            ->add('type')
            ->add('company')
            ->add('webPath', 'string', array('template' => 'EnsJobeetBundle:JobAdmin:list_image.html.twig'))
            ->add('url')
            ->add('position')
            ->add('location')
            ->add('description')
            ->add('how_to_apply')
            ->add('is_public')
            ->add('is_activated')
            ->add('token')
            ->add('email')
            ->add('expires_at')
        ;
    }
}

For the show action we used a custom template to show the logo of the company:

<!-- src/Ens/JobeetBundle/Resources/views/JobAdmin/list_image.html.twig -->

<tr>
    <th>Logo</th>
    <td><img src="{{ asset(object.webPath) }}" /></td>
</tr>

With this, we created a basic administration module with CRUD operations for our jobs and categories. Some of the features you will find when using it are:

  • The list of objects is paginated
  • The list is sortable
  • The list can be filtered
  • Objects can be created, edited, and deleted
  • Selected objects can be deleted in a batch
  • The form validation is enabled
  • Flash messages give immediate feedback to the user

Batch actions

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 delete action allows you to remove several entries at once.

To add a new batch action we have to override the getBatchActions from the Admin class. We will define here a new extend action:

// src/Ens/JobeetBundle/Admin/JobAdmin.php
// ...

public function getBatchActions()
{
    // retrieve the default (currently only the delete action) actions
    $actions = parent::getBatchActions();

    // check user permissions
    if($this->hasRoute('edit') && $this->isGranted('EDIT') && $this->hasRoute('delete') && $this->isGranted('DELETE')) {
        $actions['extend'] = array(
            'label'            => 'Extend',
            'ask_confirmation' => true // If true, a confirmation will be asked before performing the action
        );

    }

    return $actions;
}

The method batchActionExtend form the JobAdminController 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.

// src/Ens/JobeetBundle/Controller/JobAdminController.php
namespace Ens\JobeetBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

class JobAdminController extends Controller
{
    public function batchActionExtend(ProxyQueryInterface $selectedModelQuery)
    {
        if ($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false)
        {
            throw new AccessDeniedException();
        }

        $request = $this->get('request');
        $modelManager = $this->admin->getModelManager();

        $selectedModels = $selectedModelQuery->execute();

        try {
            foreach ($selectedModels as $selectedModel) {
                $selectedModel->extend();
                $modelManager->update($selectedModel);
            }
        } catch (\Exception $e) {
            $this->get('session')->setFlash('sonata_flash_error', $e->getMessage());

            return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
        }

        $this->get('session')->setFlash('sonata_flash_success',  sprintf('The selected jobs validity has been extended until %s.', date('m/d/Y', time() + 86400 * 30)));

        return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
    }
}

Let’s 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’t need to select any jobs from the list because the logic of the action will search for the matching records and delete them.

// src/Ens/JobeetBundle/Admin/JobAdmin.php
// ...

public function getBatchActions()
{
    // retrieve the default (currently only the delete action) actions
    $actions = parent::getBatchActions();

    // check user permissions
    if($this->hasRoute('edit') && $this->isGranted('EDIT') && $this->hasRoute('delete') && $this->isGranted('DELETE')){
        $actions['extend'] = array(
            'label'            => 'Extend',
            'ask_confirmation' => true // If true, a confirmation will be asked before performing the action
        );

        $actions['deleteNeverActivated'] = array(
            'label'            => 'Delete never activated jobs',
            'ask_confirmation' => true // If true, a confirmation will be asked before performing the action
        );
    }

    return $actions;
}

In addition to create the batchActionDeleteNeverActivated action, we will create a new method in our JobAdminController, batchActionDeleteNeverActivatedIsRelevant, 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 JobRepository::cleanup() method.

// src/Ens/JobeetBundle/Controller/JobAdminController.php
// ...

public function batchActionDeleteNeverActivatedIsRelevant()
{
    return true;
}

public function batchActionDeleteNeverActivated()
{
    if ($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) {
        throw new AccessDeniedException();
    }

    $em = $this->getDoctrine()->getEntityManager();
    $nb = $em->getRepository('EnsJobeetBundle:Job')->cleanup(60);

    if ($nb) {
        $this->get('session')->setFlash('sonata_flash_success',  sprintf('%d never activated jobs have been deleted successfully.', $nb));
    } else {
        $this->get('session')->setFlash('sonata_flash_info',  'No job to delete.');
    }

    return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
}

That’s 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.

Symfony2 Jobeet Day 13: Security

Symfony2 Jobeet Day 11: Testing your Forms

20 thoughts on “Symfony2 Jobeet Day 12: The Admin Bundle”

  1. Thank you very much for your excellent work.
    After “Now we need to add each admin class in the services.yml configuration file:”

    “At this point we can see in the dashboard the Jobeet group and inside it the Job and Category modules, with their respective add and list links.”

    But I have a message :
    InvalidArgumentException: There is no extension able to load the configuration for “ens.jobeet.admin.job” (in O:\www\jobeet\src\Ens\JobeetBundle\DependencyInjection/../Resources/config\services.yml). Looked for namespace “ens.jobeet.admin.job”, found none

    Probably I do something wrong. Have you a idea ?

      1. My services.yml. Same probleme with category.

        parameters:
        # ens_jobeet.example.class: Ens\JobeetBundle\Example

        services:
        # ens_jobeet.example:
        # class: %ens_jobeet.example.class%
        # arguments: [@service_id, “plain_value”, %parameter%]

        ens.jobeet.admin.category:
        class: Ens\JobeetBundle\Admin\Category
        tags:
        – { name: sonata.admin, manager_type: orm, group: jobeet, label: Categories }
        arguments: [null, Ens\JobeetBundle\Entity\Category, EnsJobeetBundle:CategoryAdmin]

        ens.jobeet.admin.job:
        class: Ens\JobeetBundle\Admin\JobAdmin
        tags:
        – { name: sonata.admin, manager_type: orm, group: jobeet, label: Jobs }
        arguments: [null, Ens\JobeetBundle\Entity\Job, EnsJobeetBundle:JobAdmin]

        1. I find the probleme. Missing spaces before ens.jobeet…
          and Admin at the end of line :
          class: Ens\JobeetBundle\Admin\CategoryXXXXX

          It’s OK now 🙂

  2. Awesome work, thanks! Keep on going with the tutorial, it seems to be the only complete Symfony 2 step-by-step tutorial out there (I mean, once it’s finished 😉 ).

  3. Is there a way to show image thumbnail in edit mode? Because I’m literally banging my head against the wall, trying to solve that issue. I’ve overriden ‘file’ filed type, registered it as a service, created form theme, merged it with current form theme for admin class and still no luck… I’ve got no errors, but template seems to be ignored.

    1. A very good question, I will try to find a solution for this. In the meantime, if anybody knows how this should be done, please let us know 🙂

    2. I found one solution. Copy the base_edit_form.html.twig template from vendor/bundles/Sonata/AdminBundle/Resources/views/CRUD/ to app/Resources/SonataAdminBundle/views/CRUD/ (you will have to create this directory). Now you can edit the template and customize it as you need and the Symfony2 framework will use it instead of the default one from the SonataAdminBundle. This technique is described here: http://symfony.com/doc/current/book/templating.html#overriding-bundle-templates

      Here is how I customized it to show the uploaded image for a job:

      <!-- app/Resources/SonataAdminBundle/views/CRUD/base_edit_form.html.twig -->
      <!-- ... -->
          <div class="sonata-ba-collapsed-fields">
              {% for field_name in form_group.fields %}
                  {% if admin.formfielddescriptions[field_name] is defined %}
                      {% if field_name == 'file' %}
                          <img src="{{ asset(object.webPath) }}" />
                      {% endif %}
                      {{ form_row(form[field_name])}}
                  {% endif %}
              {% endfor %}
          </div>
      <!-- ... -->
      

      Don’t forget to clear the cache for this modification to work.

      1. Well, that’s extremely useful, thank You very much. I couldn’t find out which template to override and which property to use.

        Of course nothing is perfect in this world, and for some reason I can’t make any file upload bundle work, but that’s another story…

  4. why I have these two errors:
    [1/2] ErrorException: Runtime Notice: Declaration of Ens\JobeetBundle\Admin\JobAdmin::configureShowField() should be compatible with that of Sonata\AdminBundle\Admin\Admin::configureShowField() in C:\wamp\www\Symfony\src\Ens\JobeetBundle\Admin\JobAdmin.php line 112
    [2/2] FileLoaderLoadException: Cannot import resource “C:/wamp/www/Symfony/app/config\.” from “C:/wamp/www/Symfony/app/config\routing.yml”.
    ——————
    my file C :/ wamp / www / Symfony / app / config /routing.yml

    EnsJobeetBundle:
    resource: “@EnsJobeetBundle/Resources/config/routing.yml”
    prefix: /

    admin:
    resource: ‘@SonataAdminBundle/Resources/config/routing/sonata_admin.xml’
    prefix: /admin

    _sonata_admin:
    resource: .
    type: sonata_admin
    prefix: /admin

  5. I did this chapter but i don’t run!In don’t known what to do! please everybody help me about one detail Back-end! thanks…

  6. Hi
    I installed and configured Sonata Admin Bundle and so far everything is working fine…
    Now I am wondering how can I allow to upload more than one image?
    I would appreciate a solution very much 🙂

  7. j’ai un problème avec installation “sonata-project/doctrine-orm-admin-bundle” sous symfony 2.2 avec composer.phar
    aider moi svp

    1. I had problems with installing sonata too. Here is how I solved the problem:
      First I installed (updated) the latest Symfony version (2.2.*).
      Than i updated composer.json file to install other bundles. For each bundle I entered “dev-master” instead of typing its current version except for the these bundles: knplabs/knp-menu and knplabs/knp-menu-bundle. For these two bundles I entered older versions: “1.1.x-dev”, so for these two bundles your composer.json should look like this:

      “knplabs/knp-menu”: “1.1.x-dev”,
      “knplabs/knp-menu-bundle”: “1.1.x-dev”,

      Hope this will solve the problem

  8. Hi,

    i didn’t found “list_image.html.twig” File. The Ordner “JobAdmin/” under src/Ens/JobeetBundle/Resources/views/ didn’t even exist.

    Did i miss something? Where, when and how it be generated?

    Greets,

    Ipan

    PS: Great tutorial tough. Although it didn’t based on the new Symfony 2.2.*, but it really help me a lot. And thx fouy works 😉

  9. Dear friends,
    I am getting the following ERROR while trying to add ‘Category’ in the /admin/dashboard
    An exception has been thrown during the rendering of a template (“Catchable Fatal Error: Method Ens\JobeetBundle\Entity\Category::__toString() must return a string value in /var/www/jobeet/app/cache/dev/classes.php line 8568”) in “SonataAdminBundle:CRUD:edit.html.twig
    Pl help. Thanks.

  10. Another point is also there, inside CategoryAdminController,
    there is nothing inside the class?
    // src/Ens/JobeetBundle/Controller/CategoryAdminController.php
    namespace Ens\JobeetBundle\Controller;

    use Sonata\AdminBundle\Controller\CRUDController as Controller;

    class CategoryAdminController extends Controller
    {

    }
    How is this? Not able to get it!!!

Leave a Reply

Your email address will not be published. Required fields are marked *