Sonata admin
How to make a full-featured backend without any line on client-side(html/css/js)
What is sonata admin?
"A set of several rich open source bundles
based on Symfony2"
Why sonata?
- You can create a rich, consistent and configurable interface for models data manipulation
- You have a high-featured backoffice out of the box
- Sonata is built using the best practices of Symfony2 and on base of Symfony2
- You still have high flexibility for customization
- You do not need to reinvent a wheel each time when writing backoffice
Sonata Main bundles
- SonataCoreBundle(Classes which are reused in project itself, like variuous form types, doctrine basic entity manager, serialization stuff, various helpers)
- SonataAdminBundle(Admin generator itself)
- SonataDoctrineORMAdminBundle
- SonataDoctrineMongoDBAdminBundle(early stage)
- SonataDoctrinePhpcrAdminBundle(early stage)
- SonataPropelAdminBundle(early stage)
sonata featured bundles
SonataPageBundle(CMS)
SonataMediaBundle(media library based on a dedicated abstract provider which handles different type of media: files, videos or images)
SonataNewsBundle(blog platform based on Doctrine2 and Symfony2)
SonataUserBundle(FOS/User integration)
SonataBlockBundle(block element rendering and caching using different cache storages (memcached/apc/esi/ssi/mongo))
Let's start
{
"require": {
"sonata-project/admin-bundle": "~2.2@dev",
"sonata-project/block-bundle": "~2.2@dev",
"sonata-project/doctrine-orm-admin-bundle": "~2.2@dev",
"sonata-project/core-bundle": "~2.2@dev"
}
}
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
//...
new Sonata\CoreBundle\SonataCoreBundle(),
new Sonata\BlockBundle\SonataBlockBundle(),
new Sonata\jQueryBundle\SonatajQueryBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
new Sonata\AdminBundle\SonataAdminBundle(),
//...
}
}
Configuration
#config.yml
sonata_block:
default_contexts: [cms]
blocks:
sonata.admin.block.admin_list:
contexts: [admin]
#routing.yml
admin:
resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
prefix: /admin
_sonata_admin:
resource: .
type: sonata_admin
prefix: /admin
That's it
We have almost nothing
but we added almost nothing
Let's create models
class Country
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=2)
*/
private $isoCode;
/**
* @ORM\Column(type="string", length=25)
*/
private $name;
}
Let's create models(part 2)
class Index
{
/**
* @var string name
* @ORM\Column(type="string")
*/
private $name;
/**
* @var Country
* @ORM\ManyToOne(targetEntity="App\Entity\Country")
*/
private $country;
}
let's create models(part 3)
class Value
{
const VOLATILITY_LOW = 1;
/**
* @ORM\Column(type="datetime")
*/
private $createdAt;
/**
* @ORM\Column(type="integer")
*/
private $volatility = self::VOLATILITY_LOW;
/**
* @ORM\Column(type="float")
*/
private $fixed;
/**
* @ORM\Column(type="float")
*/
private $prognosed;
/**
* @ORM\Column(type="float")
*/
private $actual;
}
OK, we have models
We even have fake data.
How to work with it?
admin class
- Meta description of CRUD operations
- No code generation
- Based on Symfony services + Sonata Admin services and itself is a service.
the simplest admin ever
namespace App\Admin;
use Sonata\AdminBundle\Admin\Admin;
class CountryAdmin extends Admin
{
}
#src/App/Resources/config/services.yml
parameters:
app.service.admin.country.class: App\Admin\CountryAdmin
services:
app.service.admin.country:
class: %app.service.admin.country.class%
tags:
- { name: sonata.admin, manager_type: orm, group: "Content", label: "Countries" }
arguments:
- ~
- App\Entity\Country
- ~
Results
Menu
List
form configuration
<?php
namespace App\Admin;
use Sonata\AdminBundle\Form\FormMapper;
class CountryAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('translations', 'a2lix_translations')
->add('isoCode');
}
}
result
under the hood
admin class deps overview
- ConfigurationPool: configuration pool where all Admin class instances are stored
- ModelManager: service which handles specific code relating to your persistence layer (e.g. Doctrine ORM)
- FormContractor: builds the forms for the edit/create views using the Symfony FormBuilder
- ShowBuilder: builds the show fields
- ListBuilder: builds the list fields
- DatagridBuilder: builds the filter fields
- Request: the http request received
- RouteBuilder: allows you to add routes for new actions and remove routes for default actions
- RouterGenerator: generates the different urls
- SecurityHandler: handles permissions for model instances and actions
- Validator: handles model validation
- Translator: generates translations
- LabelTranslatorStrategy: a strategy to use when generating labels
- MenuFactory: generates the side menu, depending on the current action
Admin class configuration overview
app.service.admin.country:
class: %app.service.admin.country.class%
tags:
- { name: sonata.admin, manager_type: orm|doctrine_mongodb|doctrine_phpcr, group: "Content", label: "Countries" }
arguments:
- ~ #code
- App\Entity\Index #model- ~ #related CRUD controller
calls:
- [ setLabelTranslatorStrategy, ["@sonata.admin.label.strategy.underscore|noop|native|form_component|bc"]]
- [ setTranslationDomain, "some_domain"]
- [ setRouteBuilder, [ @sonata.admin.route.path_info|default_generator|query_string ]]
list configuration
<?php
namespace App\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
class CountryAdmin extends Admin{
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->add('name')
->add('isoCode')
->add('_action', 'actions', [
'actions' => [
'show' => [],
'edit' => [],
'delete' => [],
]
]
);
}
}
result
additional form types example
<?php
use Sonata\AdminBundle\Form\FormMapper;
class IndexAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->add(
'country',
'sonata_type_model',
[
'property' => 'name',
'required' => true
]
)
->add('enabled', null, ['required' => false]);
}
result
form types
- Admin Type - embedding a form from another Admin class
- Collection Type - one-to-many associations
- Model Type - similar to Entity Type
- Model Reference Type - references to a model, handles entity id
- Immutable array type - form type per array element
Collection type(one to many)
sonata_type_collection
By reference = false
Sonata options:
- edit: standard:inline
- inline:table|list
- sortable by field name if specified
result
groups and help
use Sonata\AdminBundle\Form\FormMapper;
class ValueAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('Meta')
->add('date', 'datetime')
->add(
'volatility',
'choice',
[
'choices' => Value::getVolatilityValues()
]
)
->add('index')
->end()
->with('Data')
->add('prognosed')
->add('actual')
->end()
->setHelps(
[
'prognosed' => 'Fixed value will be calculated automatically based on diff between actual and prognosed values ',
]
);
}
result
quick models status change
use Sonata\AdminBundle\Datagrid\ListMapper;
class IndexAdmin extends Admin
{
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->add('name')
->add('enabled', null, ['editable' => true])
->add(
'_action',
'actions',
[
'actions' => [
'show' => [],
'edit' => [],
'delete' => [],
]
]
);
}
}
result
final result
customization example
what is not good yet
High cost of deep customization
Documentation is messy and not always clear
Unclear strategy of releases
Rather difficult for newcomers
when to use
- small projects? - overkill
- medium/pet projects - rather useful
- large, custom projects - might not be flexible enough
a bit of stats
> 345000 installs on Packagist
> 900 stargazers
> 500 forks
~ 3000 commits
resources
Sonata:
https://github.com/sonata-project
Sample app
https://github.com/akovalyov/SonataSimpleIndexes
contacts
- Accounts: @KNPLabs, @KNPLabsRu
- Hashtag: #SymfonyPizza
andrew.kovalyov@knplabs.com