Improving the SonataAdminBundle in a generic way
Out-of-the-box, Sonata Admin modules are not capable of dealing with foreign references in ManyToOne / OneToMany and ManyToMany relations. Then we tried to design a simple way to use and deploy a Sonata Admin module (especially its FormMapper), minimizing the things to do, the code to write...
When you want to create a Sonata module for an existing entity, you usually start from the command line executing :
app/console sonata:admin:generate
Then you create/modify/validate your new service in your src/AcmeBundle/Resources/config/admin.yml
. At this point, things are working as a Sonata Admin module "Vanilla".
To be able to use the logic provided by the libre-informatique/core-bundle
, you need to deviate inheritance tree of your Sonata Admin. So you will change your src/AcmeBundle/Admin/MyAdmin.php
that way:
// src/AcmeBundle/Admin/MyAdmin.php
// ...
use Librinfo\CoreBundle\Admin\CoreAdmin;
class MyAdmin extends CoreAdmin
{
// ...
}
Your MyAdmin
is now a Librinfo\CoreBundle\Admin\CoreAdmin
before being a Sonata\AdminBundle\Admin\Admin
(the CoreAdmin
extends it). Things are getting serious, it sounds good.
You have to create a class that :
- extends your
*Admin
(eg.MyAdmin
) - is registered in the
Sonata
service instead of your*Admin
(eg.MyAdmin
) - uses more logic from the
libre-informatique/core-bundle
(the traitLibrinfo\CoreBundle\Admin\Traits\Base
)
Eg.:
// src/AcmeBundle/Admin/MyAdminConcrete.php
namespace AcmeBundle\Admin;
use Librinfo\CoreBundle\Admin\Traits\Base as BaseAdmin;
class MyAdminConcrete extends MyAdmin
{
use BaseAdmin;
}
// src/AcmeBundle/Resources/config/admin.yml
acme.my:
class: AcmeBundle\Admin\MyAdminConcrete
arguments: [~, AcmeBundle\Entity\My, SonataAdminBundle:CRUD]
tags:
- {name: sonata.admin, manager_type: orm}
We have already seen the Librinfo\CoreBundle\Admin\Traits\Base
in the previous example. It is really simple and simply overrides the methods Admin::configureDatagridFilters()
, Admin::configureListFields()
, Admin::configureFormFields()
and Admin::configureShowFields()
to use the logic from Librinfo\CoreBundle\Admin\CoreAdmin
.
This, then, allows you not to write a line of PHP to define complex Sonata Admin forms and complete modules. You will find all the needed details in the README.md file of the libre-informatique/core-bundle
.
Any other trait uses this trait. So if you want to specialize your current Sonata Admin, you will not have to use the Base
trait anymore (it prevents most of the possible conflicts, avoiding the need of resolutions).
The Librinfo\CoreBundle\Admin\Traits\EmbeddedAdmin
trait is to be used when your CoreAdmin
is embedded within a sonata_type_collection
form type (at least in its FormMapper/ShowMapper mode).
Embedded
is the exact mirror of Embedding
(which is being treated in the next title) and aims to be used as a twin of Embedding
.
This is done in YAML using :
parameters:
librinfo:
// ...
AcmeBundle\Entity\My:
// ...
Sonata\AdminBundle\Form\FormMapper:
add:
MyTab:
MyGroup:
children:
type: sonata_type_collection
by_reference: false # required
type_options:
required: false
btn_add: false
required: false
label: false
_options:
edit: inline # required
#inline: table
allow_delete: true
// ...
- Simply use it in your
CoreAdmin
:
// src/AcmeBundle/Admin/ChildAdminConcrete.php
namespace AcmeBundle\Admin;
use Librinfo\CoreBundle\Admin\Traits\Embedded;
class ChildAdminConcrete extends ChildAdmin
{
use Embedded;
}
The libre-informatique/core-bundle
will take care of everything for you excepting :
- Add some logic in your parent entity :
// src/AcmeBundle/Entity/My.php
namespace AcmeBundle\Entity;
class My
{
// ...
/*
* @var Collection
*/
private $children;
/**
* @param Child $children
* @return self
*/
public function addChild(Child $child)
{
$rc = new \ReflectionClass($this);
$child->setMy($this);
$this->children->add($child);
return $this;
}
/**
* @param Child $children
* @return self
*/
public function removeChild(Child $child)
{
$this->children->removeElement($child);
return $this;
}
/**
* @return Collection
*/
public function getChildren()
{
return $this->children;
}
// ...
}
Eventually, if many entities are using this embedded Admin (meaning that many entities have children), you can think of writing a trait with this logic, which will allow you to write things about this trait in your librinfo.yml
... preventing many descriptions of the same FormMapper
.
The Librinfo\CoreBundle\Admin\Traits\HandlesRelationsAdmin
trait is to be used when your CoreAdmin
embeds other CoreAdmin
using sonata_type_collection
form types (at least in its FormMapper/ShowMapper mode) or when it has many-to-many related collections.
In fact, Embedding
is the exact mirror of Embedded
and aims to be used as a twin of Embedded
.
It subscribes all the sonata_type_collection
to the Librinfo\CoreBundle\Admin\Trait\CollectionsManager::managedCollections
, avoiding the registration of collections in the librinfo.yml
definition.
It also finds all the many-to-many relationships in your form form. It takes care or deleting linked entities when the Admin entity is on the inverse side of the many-to-many relationship.
- Create a trait file in your
MyBundle/Admin/Traits/
directory - Write the needed PHP code to implement your logic, for instance in the
CoreAdmin::configureFormFields()
method - If some logic needs to be executed within the
CoreAdmin::prePersist()
orCoreAdmin::preUpdate()
calls, create methods that fit the convention:[MyTrait]::pre[Persist|Update][MyTrait]()
(replace[text]
to fit your needs). - Optionally, your trait can use
Librinfo\CoreBundle\Admin\Traits\Base
or others, if this is correct to do so. - Use your trait (maybe in addition with others) in your
*AdminConcrete
class.
Some traits are here only to make the Librinfo\CoreBundle\Admin\CoreAdmin
more readable, and consistant.
The Librinfo\CoreBundle\Admin\Traits\Mapper
trait embeds all the logic that parses the librinfo.yml
files and generate a matching Sonata\AdminBundle\Admin\Admin
without writing a line of PHP.
The Librinfo\CoreBundle\Admin\Traits\CollectionsManager
trait treats the collections that would be left untouched after a change in an embedded form (a sonata_type_collection
form type). It uses definitions found in the librinfo.yml
files.
eg.:
# app/config/config.yml
parameters:
librinfo:
AcmeBundle\Admin\MyAdmin:
managedCollections: [children]
The Librinfo\CoreBundle\Admin\Traits\ManyToManyManager
trait removes the many-to-many links that would be left untouched after a change in the related collection widget.
It is not necessary to use it when the Admin entity is on the owning side of the many-to-many relationship.
It uses definitions found in the librinfo.yml
files with the managedManyToMany keyword.
eg.:
# app/config/config.yml
parameters:
librinfo:
AcmeBundle\Admin\Worker:
managedManyToMany: [task, tool]
If you want your Admin to handle those many-to-many relationships automatically, use the HandlesRelationsAdmin
trait.
The Librinfo\CoreBundle\Admin\Traits\PreEvents
trait embeds the Sonata\AdminBundle\Admin\Admin::preUpdate($object)
and Sonata\AdminBundle\Admin\Admin::prePersist($object)
methods. It comes with the ability to define new "behaviors" in traits. When called, those methods try to execute every methods componed as:
[MyTrait]::[prePersist|preUpdate][MyTrait]($object)