Tag Archives: SonataAdminBundle

Symfony2 Tutorial Part 3: Backend with SonataAdminBundle

Updated for symfony2 2.6

Here you can find more infos on the SonataAdminBundle: http://sonata-project.org/bundles/admin

install SonataAdminBundle

You can install the bundle by simply typing the following on your console.

composer.phar require sonata-project/admin-bundle

Hint: You have to be inside your symfony project directory.

The SonataAdminBundle requires a storage Bundle to be installed. In our case we will use SonataDoctrineORMAdminBundle.
Here you will find some more information about SonataDoctrineORMAdminBundle: http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/installation.html
The Installation is very simple.

composer.phar require sonata-project/doctrine-orm-admin-bundle

This will simply add a line in your composer.json file and then update your vendors.

{
    "name": "jorzekowsky/simple_blog",
    "license": "proprietary",
    "type": "project",
    "autoload": {
        "psr-0": {
            "": "src/",
            "SymfonyStandard": "app/"
        }
    },
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*",
        "doctrine/orm": "~2.2,>=2.2.3,<2.5",
        "doctrine/dbal": "<2.5",
        "doctrine/doctrine-bundle": "~1.2",
        "twig/extensions": "~1.0",
        "symfony/assetic-bundle": "~2.3",
        "symfony/swiftmailer-bundle": "~2.3",
        "symfony/monolog-bundle": "~2.4",
        "sensio/distribution-bundle": "~3.0,>=3.0.12",
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2",
        "incenteev/composer-parameter-handler": "~2.0",
        "sonata-project/admin-bundle": "^2.3",
        "sonata-project/doctrine-orm-admin-bundle": "^2.3"
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3"
    },
    "scripts": {
        "post-root-package-install": [
            "SymfonyStandard\\Composer::hookRootPackageInstall"
        ],
        "post-install-cmd": [
            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
        ],
        "post-update-cmd": [
            "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles",
            "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
        ]
    },
    "config": {
        "bin-dir": "bin"
    },
    "extra": {
        "symfony-app-dir": "app",
        "symfony-web-dir": "web",
        "symfony-assets-install": "relative",
        "incenteev-parameters": {
            "file": "app/config/parameters.yml"
        }
    }
}

Hint: You can do this manually. Yu just need to add the higlighted line yourself and then run the following command on your console.

composer.phar update

Syntax is very important inside a json file so don’t forget the comma in line 23.

We will enable them in the next step.
Edit app/AppKernel.php and add the following lines so it looks like this:

<?php // app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Symfony\Bundle\AsseticBundle\AsseticBundle(),
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),

            new Sonata\CoreBundle\SonataCoreBundle(),
            new Sonata\BlockBundle\SonataBlockBundle(),
            new Knp\Bundle\MenuBundle\KnpMenuBundle(),
            new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
            new Sonata\AdminBundle\SonataAdminBundle(),

            new AppBundle\AppBundle(),
        );

        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
            $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
            $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
        }

        return $bundles;
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
    }
}

Now we need to configure some of the dependencies. Just add the following lines at the bottom of your app/config/config.yml file.

# app/config/config.yml
sonata_block:
    default_contexts: [cms]
    blocks:
        # Enable the SonataAdminBundle block
        sonata.admin.block.admin_list:
            contexts:   [admin]
        # Your other blocks

Hint: you can learn more about the SonataBlockBundle here: http://sonata-project.org/bundles/block/master/doc/index.html

Now execute the following commands on your console.

php app/console assets:install web
php app/console cache:clear

Now we have installed SonataAdminBundle. In the next step we will configure it so that we are able to use it for our blog.

Configuration

First we need to define our routes in app/config/routing.yml. It should now look like this.

# app/config/routing.yml
quadspot_blog:
    resource: "@QuadspotBlogBundle/Resources/config/routing.yml"
    prefix:   /

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

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

Next thing we do is create an admin class for our Post entity.
Create a file src/AppBundle/Admin/PostAdmin.php with the following content.

<?php // src/AppBundle/Admin/PostAdmin.php

namespace QAppBundle\Admin;

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

class PostAdmin extends Admin
{
    // Fields to be shown on create/edit forms
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('title', 'text', array('label' => 'Post Title'))
            ->add('body')
        ;
    }

    // Fields to be shown on filter forms
    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('title')
        ;
    }

    // Fields to be shown on lists
    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->addIdentifier('title')
        ;
    }
}

Now we need to define a service for this class. If we used the generate:bundle function to generate our BlogBundle we already have a yml file where we can define our new service.
Open src/Quadspot/BlogBundle/Resources/config/services.yml and add the following lines so it looks like this.

# src/AppBundle/Resources/config/services.yml
services:
    sonata.admin.post:
        class: AppBundle\Admin\PostAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: "Content", label: "Post" }
        arguments:
            - ~
            - AppBundle\Entity\Post
            - ~
        calls:
            - [ setTranslationDomain, [AppBundle]]

Now we need to import this service, just edit the app/config/config.yml file.

# app/config/config.yml
imports:
    # ...
    - { resource: "@AppBundle/Resources/config/services.yml" }
    # ...

Now you can add, edit and delete Posts. Just visit http://symfony.yourhostname/app_dev.php/admin or http://symfony.yourhostname/admin in your production environment.

Hint: If you get a 404 error in your production environment try clearing the cache.

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

Now I have noticed that the labels and descriptions on some of the elements in the admin backend do not look quite right. That is because the translator is not enabled by default.
You can enable it in app/config/config.yml.

# app/config/config.yml
framework:
    # ...
    translator:      { fallback: "%locale%" }
    # ...

Now there will be some error when you try to add a new post or update an existing one. This is because the created_at and updated_at field is required per default and we do not provide them with any values. We want them to be filled automatically. To do that we need to add two more methods and enable lifecyclecallbacks in our post entity in src/AppBundle/Entity/Post.php.

&lt;?php // src/AppBundle/Entity/Post.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Post
 *
 * @ORM\Table()
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Post
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255)
     */
    private $title;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="created_at", type="datetime")
     */
    private $created_at;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="updated_at", type="datetime")
     */
    private $updated_at;

    /**
     * @var string
     *
     * @ORM\Column(name="body", type="text")
     */
    private $body;


    public function __construct()
    {
        $this->created_at = $this->updated_at = new \DateTime("now");
    }

    /** @ORM\PreUpdate */
    public function updated()
    {
        $this->updated_at = new \DateTime("now");
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set title
     *
     * @param string $title
     * @return Post
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title
     *
     * @return string 
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set created_at
     *
     * @param \DateTime $createdAt
     * @return Post
     */
    public function setCreatedAt($createdAt)
    {
        $this->created_at = $createdAt;

        return $this;
    }

    /**
     * Get created_at
     *
     * @return \DateTime 
     */
    public function getCreatedAt()
    {
        return $this->created_at;
    }

    /**
     * Set updated_at
     *
     * @param \DateTime $updatedAt
     * @return Post
     */
    public function setUpdatedAt($updatedAt)
    {
        $this->updated_at = $updatedAt;

        return $this;
    }

    /**
     * Get updated_at
     *
     * @return \DateTime 
     */
    public function getUpdatedAt()
    {
        return $this->updated_at;
    }

    /**
     * Set body
     *
     * @param string $body
     * @return Post
     */
    public function setBody($body)
    {
        $this->body = $body;

        return $this;
    }

    /**
     * Get body
     *
     * @return string 
     */
    public function getBody()
    {
        return $this->body;
    }
}

The __construct() method is only called once when we first create our new post. The updated() method has to be called whenever we change something.

Hint: Look at the doctrine documentation if you want to know more http://doctrine-orm.readthedocs.org/en/latest/

Now you can crate update and delete posts in your blog.

Setting up a Symfony2 Project with FOSUserBundle, SonataUserBundle and SonataAdminBundle

Tested with Symfony Standard 2.0.7 (http://symfony.com/download?v=Symfony_Standard_Vendors_2.0.7.tgz)

This is basically a merge of the installation-guides of FOSUserBundle (https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/index.md), SonataAdminBundle (http://sonata-project.org/bundles/admin/master/doc/reference/installation.html) and SonataUserBundle (http://sonata-project.org/bundles/user/master/doc/reference/installation.html)

  1. Add
    [FOSUserBundle]
        git=git://github.com/FriendsOfSymfony/FOSUserBundle.git
        target=bundles/FOS/UserBundle
    
    [SonatajQueryBundle]
        git=http://github.com/sonata-project/SonatajQueryBundle.git
        target=/bundles/Sonata/jQueryBundle
    
    [SonataAdminBundle]
        git=http://github.com/sonata-project/SonataAdminBundle.git
        target=/bundles/Sonata/AdminBundle
    
    [MenuBundle]
        git=https://github.com/KnpLabs/KnpMenuBundle.git
        target=/bundles/Knp/Bundle/MenuBundle
    
    [KnpMenu]
        git=https://github.com/KnpLabs/KnpMenu.git
        target=/knp/menu
    
    [SonataUserBundle]
        git=git://github.com/sonata-project/SonataUserBundle.git
        target=/bundles/Sonata/UserBundle
    
    [SonataEasyExtendsBundle]
        git=git://github.com/sonata-project/SonataEasyExtendsBundle.git
        target=/bundles/Sonata/EasyExtendsBundle
    
    [SonataDoctrineORMAdminBundle]
        git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git
        target=/bundles/Sonata/DoctrineORMAdminBundle

    to your deps file. (Don’t forget the trailing newline.)

  2. Run
    php bin/vendors install --reinstall
  3. Add the namespaces to app/autoload.php
    // app/autoload.php
    $loader->registerNamespaces(array(
        // ...
        'FOS'              => __DIR__.'/../vendor/bundles',
        'Sonata'           => __DIR__.'/../vendor/bundles',
        'Application'      => __DIR__,
        'Knp'              => array(
                              __DIR__.'/../vendor/bundles',
                              __DIR__.'/../vendor/knp/menu/src',
                              ),
        // ...
    ));
  4. Enable the bundles in app/AppKernel.php
    // app/AppKernel.php
        public function registerBundles()
        {
            $bundles = array(
                // ...
                new FOS\UserBundle\FOSUserBundle(),
                new Sonata\jQueryBundle\SonatajQueryBundle(),
                new Sonata\AdminBundle\SonataAdminBundle(),
                new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
                new Knp\Bundle\MenuBundle\KnpMenuBundle(),
                new Sonata\UserBundle\SonataUserBundle('FOSUserBundle'),
                new Sonata\EasyExtendsBundle\SonataEasyExtendsBundle(),
                // ...
            );
            // ...
        }
  5. Add
    # app/config/config.yml
    fos_user:
        db_driver: orm
        firewall_name: main
        user_class: Application\Sonata\UserBundle\Entity\User

    to app/config/config.yml

  6. Run
    php app/console sonata:easy-extends:generate SonataUserBundle
  7. Add the new Bundle to app/AppKernel.php
    // app/AppKernel.php
        public function registerbundles()
        {
            $bundles = array(
                // Application Bundles
                // ...
                new Application\Sonata\UserBundle\ApplicationSonataUserBundle(),
                // ...
            );
            // ...
        }
  8. Add
    # app/config/routing.yml
    fos_user_security:
        resource: "@FOSUserBundle/Resources/config/routing/security.xml"
    
    fos_user_profile:
        resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
        prefix: /profile
    
    fos_user_register:
        resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
        prefix: /register
    
    fos_user_resetting:
        resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
        prefix: /resetting
    
    fos_user_change_password:
        resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
        prefix: /change-password
    
    admin:
        resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
        prefix: /admin
    
    _sonata_admin:
        resource: .
        type: sonata_admin
        prefix: /admin
    
    soanata_user:
        resource: '@SonataUserBundle/Resources/config/routing/admin_security.xml'
        prefix: /admin

    to app/config/routing.yml

  9. Add the following to app/config/security.yml
    # app/config/security.yml
    security:
        encoders:
            FOS\UserBundle\Model\UserInterface: sha512
    
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
            SONATA:
                - ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT  # if you are not using acl then this line must be uncommented
    
        providers:
            fos_userbundle:
                id: fos_user.user_manager
    
        firewalls:
    
            # -> custom firewall for the admin area of the URL
            admin:
                pattern:      /admin(.*)
                form_login:
                    provider:       fos_userbundle
                    login_path:     /admin/login
                    use_forward:    false
                    check_path:     /admin/login_check
                    failure_path:   null
                logout:
                    path:           /admin/logout
                anonymous:    true
            # -> end custom configuration
    
            # defaut login area for standard users
            main:
                pattern:      .*
                form_login:
                    provider:       fos_userbundle
                    login_path:     /login
                    use_forward:    false
                    check_path:     /login_check
                    failure_path:   null
                logout:       true
                anonymous:    true
    
    # ...
    
        access_control:
            # URL of FOSUserBundle which need to be available to anonymous users
            - { path: ^/_wdt, role: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/_profiler, role: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    
            # -> custom access control for the admin area of the URL
            - { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/admin/login-check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
            # -> end
    
            - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
    
            # Secured part of the site
            # This config requires being logged for the whole site and having the admin role for the admin part.
            # Change these rules to adapt them to your needs
            - { path: ^/admin, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
            - { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
    # ...
  10. Update your DB-Schema. Run
    php app/console doctrine:schema:update --force
  11. Now, install the assets from the different bundles:
    php app/console assets:install web
  12. Clear your cache:
    php app/console cache:clear
  13. You can create an admin-user and a test-user with the following commands.
    php app/console fos:user:create admin admin@example.com password --super-admin
    php app/console fos:user:create testuser test@example.com password
  14. You need to enable your translator in /app/config.yml.
  15. Create app/Application/Sonata/UserBundle/Recources/translations/SonataUserBundle.en.ymlwith the following content:
    # app/Application/Sonata/UserBundle/Recources/translations/SonataUserBundle.en.yml
    form:
      label_username:             Username
      label_email:                Email
      label_plain_password:       Password (Plain)
      label_groups:               Groups
      label_roles:                Roles
      label_locked:               Locked
      label_expired:              Expired
      label_enabled:              Enabled
      label_credentials_expired:  Credentials Expired
      label_name:                 Name
    
    list:
      label_username:             Username
      label_email:                Email
      label_enabled:              Enabled
      label_locked:               Locked
      label_created_at:           Created At
      label_roles:                Roles
      label_name:                 Name
    
    filter:
      label_username:             Username
      label_locked:               Locked
      label_email:                Email
      label_id:                   ID
      label_name:                 Name