Managing one to many entities in sonata admin bundle - symfony

Imagine you are developing a website using symfony2 and it's admin panel using Sonata Admin Bundle and of course imagine you have a class named Product which owns some Images. Image is also a class and you have set in Product a one to many relation-ship to Image class. So every image is owned by a product, so you want to manage Image Admin inside Product Admin classes.
So there are some problems you should to face with them. Would you please to tell how?
1. When you delete a product object, all images related to that product be deleted.
2. When you are in show product page or add new product page the picture of all of images display on page.
(Is there any solution without using sonata media bundle too?)
Thanks
I handle Image upload with Document class:
FstQst\WebBundle\Entity\Document:
type: entity
table: null
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
updated: # changed when files are uploaded, to force preUpdate and postUpdate to fire
type: datetime
nullable: true
format:
type: string
length: 25
nullable: true
lifecycleCallbacks:
prePersist: [ preUpload ]
preUpdate: [ preUpload ]
postPersist: [upload]
postUpdate: [upload]
postRemove: [removeUpload]
I have another class using Document for adding some features to image:
FstQst\WebBundle\Entity\Post:
type: entity
table: null
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
title:
type: string
length: 100
nullable: true
oneToOne:
document:
targetEntity: Document
joinColumn:
name: document_id
referencedColumnName: id
orphanRemoval: true
manyToOne:
site:
targetEntity: VisitablePoint
inversedBy: posts
joinColumn:
name: vPoint_id
referencedColumnName: id
lifecycleCallbacks: { }
And a class named VisitablePoint, which uses Post class:
FstQst\WebBundle\Entity\VisitablePoint:
type: entity
table: visitablePoint
id:
id:
type: integer
id: true
generator:
strategy: AUTO
oneToMany:
posts:
targetEntity: Post
mappedBy: site
orphanRemoval: true
lifecycleCallbacks: { }
I changed my classes names from Post to Image and from VisitablePoint to Product at my old post. Now I want when I go to VisitablePoint admin/show page I see pictures of Post object instead of their title. And of course in admin/edit page too.
And these are Admin Classes:
<?php
namespace FstQst\WebBundle\Admin;
use FstQst\WebBundle\Entity\Document;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class DocumentAdmin extends Admin
{
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add('updated')
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('file', 'file', $this->getFieldOptionForImagePreview())
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('id')
->add('updated')
->add('format')
;
}
public function prePersist($image) {
$this->manageFileUpload($image);
}
public function preUpdate($image) {
$this->manageFileUpload($image);
}
protected function manageFileUpload(Document $image) {
if ($image->getFile()) {
$image->refreshUpdated();
}
}
protected function getFieldOptionForImagePreview($maxSize = 200){
if($this->hasParentFieldDescription()) { // this Admin is embedded
// $getter will be something like 'getlogoImage'
$getter = 'get' . $this->getParentFieldDescription()->getFieldName();
// get hold of the parent object
$parent = $this->getParentFieldDescription()->getAdmin()->getSubject();
if ($parent) {
$document = $parent->$getter();
} else {
$document = null;
}
} else {
$document = $this->getSubject();
}
// use $fileFieldOptions so we can add other options to the field
$fileFieldOptions = array('required' => false);
if ($document && ($webPath = $document->getWebPath())) {
// get the container so the full path to the image can be set
$container = $this->getConfigurationPool()->getContainer();
$fullPath = $container->get('request')->getBasePath().'/'.$webPath;
//$fileFieldOptions['help'] = '<img src="/uploads/documents/10.png" class="admin-preview" style="max-height: 200px; max-width: 200px"/>';
$fileFieldOptions['help'] = <<<START
<img src="$fullPath" style="max-height: {$maxSize}px; max-width: {$maxSize}px"/>
START;
}
return $fileFieldOptions;
}
}
<?php
namespace FstQst\WebBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class VisitablePointAdmin extends Admin
{
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('posts', 'sonata_type_model', array('multiple' => true, 'property' => 'title', 'label' => 'Image', 'required' => false))
;
}
/**
* #param ShowMapper $showMapper
*/
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('posts', 'sonata_type_collection', ['label' => 'Images',
'required' => false, 'cascade_validation' => true,
'by_reference' => false], ['edit' => 'inline', 'inline' => 'table'])
->end()
;
}
}

The One to Many relation
1. When you delete a product object, all images related to that product be deleted.
You can easily manage this kind of deletion with the orphanRemoval="true"
<?php
class Product{
[...]
/**
* #ORM\OneToMany(targetEntity="Event", mappedBy="day", orphanRemoval="true", cascade={"all"})
*/
private $images;
[...]
}
in yml, the configuration looks like:
oneToMany:
images:
targetEntity: Image
orphanRemoval: true
mappedBy: product
2. When you are in show product page or add new product page the picture of all of images display on page.
You have to use a sonata_type_collection in your configureFormFields or your configureShowFields method of your ProductAdmin class:
<?php
class ProductAdmin{
[...]
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->with('tab_images')
->add('images', 'sonata_type_collection', array(
'required' => false,
'cascade_validation' => true,
'by_reference' => false,
), array(
'edit' => 'inline',
'inline' => 'table',
))
->end()
;
}
[...]
}
Then provide a ImageAdmin with all you need to upload files.
You might have to change settings because i took it from a personal project and i don't know if it's totally adapted to your needs.
Show images in Sonata
first configure your ImageAdmin as follow:
class ImageAdmin extends Admin
{
[...]
protected function configureShowFields(ShowMapper $showMapper) {
$showMapper
->add('myImageAttr', 'image', array(
'prefix' => '/',
))
;
}
[...]
protected function configureListFields(ListMapper $listMapper) {
$listMapper
->add('myImageAttr', 'image', array(
'prefix' => '/',
'width' => 100
))
;
}
}
then create template for your list_image and you show_image types:
Resources/views/CRUD/show_image.html.twig
{% extends 'SonataAdminBundle:CRUD:base_show_field.html.twig' %}
{% block field %}
{% if value %}
<img src="{% if field_description.options.prefix %}{{ field_description.options.prefix }}{% endif %}{{ value }}" />
{% endif %}
{% endblock %}
Resources/views/CRUD/list_image.html.twig
{% extends admin.getTemplate('base_list_field') %}
{% block field%}
{% spaceless %}
{% set width = field_description.options.width is defined ? field_description.options.width : 50 %}
{% set height = field_description.options.height is defined ? field_description.options.height : 50 %}
{% if value %}
<img src="{% if field_description.options.prefix is defined %}{{ field_description.options.prefix }}{% endif %}{{ value }}"
style="max-width:{{ width }}px; max-height:{{ height }}px;" />
{% else %}
<div class="no-image" style="width:{{ width }}px; height:{{ height }}px;"></div>
{% endif %}
{% endspaceless %}
{% endblock %}
finally, add this configuration into your app/config/config.yml
sonata_doctrine_orm_admin:
templates:
types:
show:
image: YourBundle:CRUD:show_image.html.twig
list:
image: YourBundle:CRUD:list_image.html.twig
(http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/templates.html)

Related

Create a custom route in a custom page

I'm using Symfony 4.3 and Sonata 3.x version.
I'm trying to create a custom route in a custom Page but I get the error :
An exception has been thrown during the rendering of a template ("unable to find the route `admin.photocall|admin.photocall_gallery.moderate`")
I have an entity X with a OneToMany relation to the Y entity.
Explanation with code :
class XAdmin extends AbstractAdmin
{
[...]
protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
$admin = $this->isChild() ? $this->getParent() : $this;
$id = $admin->getRequest()->get('id');
if ($this->isGranted('LIST')) {
$menu->addChild('Galerie', [
'uri' => $admin->generateUrl('admin.photocall_gallery.list', ['id' => $id])
]);
}
}
}
Then there is my YAdmin :
class YAdmin extends AbstractAdmin
{
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->add('_action', null, [
'actions' => [
'clone' => [
'template' => 'admin/PhotocallListAdmin/__list_action_moderate.html.twig'
]
]
])
;
}
protected function configureRoutes(RouteCollection $collection)
{
if ($this->isChild()) {
$collection->clearExcept(['list', 'moderate']);
$collection->add($collection->getBaseCodeRoute().'.moderate', 'moderate');
return;
}
}
}
So there, I add an action with a template which look like this :
<a class="btn btn-sm" href="{{ admin.generateObjectUrl('moderate', object) }}">
{% if not object.ismoderate %}
Moderate
{% else %}
Undo moderation
{% endif%}
</a>
So the error says that it's unable to find the route admin.photocall|admin.photocall_gallery.moderate. But when I dump the $collection in YAdmin after adding the moderate route, I have two elements :
admin.photocall|admin.photocall_gallery.list (the current page)
admin.photocall|admin.photocall_gallery.moderate
I searched but it looks like that nobody else did this.
Thanks for you help
I write Gaska's comment to close this issue.
I just had to add :
$collection->add('moderate', 'moderate'); and then clear the cache.
Thanks to him

Problems having two different forms in the same twig file

I have been experiencing problems with embedding a controller that creates a form where you can upload files. When the controller is rendered in certain parts of the twig file, I get this error:
An exception has been thrown during the rendering of a template ("Expected argument of type "Symfony\Component\HttpFoundation\File\UploadedFile", "string" given").
This is strange since in other parts of the same twig file, the expected argument is given without problems. The problem seems to be another form in the same twig file that doesn't play nice with my embedded controller form.
The part that seems to cause the problem:
<div id="payment_checkout_form">
{% if cId and shippingRegionId %}
{% set savedPath =path('cart_set_shipping', {'store_id': webstore.id, 'shippingRegion': shippingRegionId,'cId':cId}) %}
{{ form_start(form, {'attr': {'id': 'form_checkout','data-url':savedPath}}) }}
{% else %}
{{ form_start(form, {'attr': {'id': 'form_checkout'}}) }}
{% endif %}
{{ render(url('passport')) }}
Relevent part of my PassportType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file', 'file', array('label' => false) , [
'multiple' => true,
'label' => '',
'attr' => [
'accept' => 'image/*',
'multiple' => 'multiple'
]
]
)
->add('confirm', 'submit');
}
public function configureOptions(OptionsResolver $resolver){
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Passport',
));
}
Relevent part of my Passport entity:
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
/**
* Sets file.
*
* #param Symfony\Component\HttpFoundation\File\UploadedFile $file
*/
public function setFile(UploadedFile $file = null) {
$this->file = $file;
}
Relevent part of my Passport controller
/**
* #Route("/passport", name="passport")
*/
public function createPassportAction(Request $request)
{
$request = $this->get('request_stack')->getMasterRequest();
$passport = new Passport();
$passport->setName('default');
$form = $this->createForm(new PassportType(), $passport);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$files = $request->files->get('passportPhoto');
if (!empty($files)) {
$this->uploadFile($files);
}
}
return $this->render('passport.html.twig', [
'form' => $form->createView(),
'isFormSubmitted' => $form->isSubmitted(),
'passportImages' => $this->getDoctrine()->getRepository('AppBundle\Entity\Passport')->findAll(),
]);
}
{{ render(url('passport')) }} is the embedded controller that renders the file upload form. If I put the{{ render(url('passport')) }} above the form_start of the other form everything works.
Answering my own question:
embedding a form inside another form like I'm trying to do in the question by using render is not possible. I fixed my problem by first removing the render call of my embedded passport form and making my passport type a sub type of the type that is used in the checkout form like this:
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
...
->add('passport', new PassportType(), array(
'required' => true
))
...
}
I still wanted to have the controller of the passport part of my form to be in it's own file. To achieve this I called my passport controller inside of the checkout controller using the forward method like this:
$files = $request->files->get('order')['passport_id'];
$store_id = $request->attributes->get('store_id');
$this->forward('AppBundle\Controller\PassportController::uploadFile',
[ 'files' => $files, 'store_id' => $store_id ]);

Symfony3 - How to render Embedded collection of Forms

I havenĀ“t found the solution to manually render a form which contains a collection.
Here is my code in twig:
<ul id="document-fields-list" data-prototype="{{ form_widget(formulario.documentos.vars.prototype)|e }}">
<div><button class="pull-right" href="#" id="add-another-document">Agregar documento</button></div>
{% for documento in formulario.documentos %}
<li>
{{ form_label(documento) }}
{{ form_widget(documento) }}
Eliminar
</li>
{% endfor %}
</ul>
FormType
In your case we need to create formType for PersonaDocumento. Imagine, that this entity has field documentName:
class PersonaDocumentoType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
* #SuppressWarnings(unused)
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('documentName', TextType::class, [
'label' => false,
'translation_domain' => 'messages'
])
;
}
/**
* #return string
*/
public function getName()
{
return 'app_persona_documento_type';
}
/**
* #return null|string
*/
public function getBlockPrefix()
{
return 'app_persona_documento';
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => PersonaDocumento::class,
'csrf_protection' => true,
'validation' => true,
));
}
}
Form that contain collection
Consider you entity Formulario. It has a OneToMany relation to PersonaDocumento. And Form will be:
class FormularioFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder
->add('documentos', CollectionType::class, [
'entry_type' => PersonaDocumentoType::class,
'entry_options' => [
'label' => false,
],
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false, // Very important thing!
])
;
}
// ...
}
Widget for collection
We have a form (FormularioFormType) that contain collection of small forms with type PersonaDocumentoType.
New widget you can create in file with standard widgets, and name of the file is fields.html.twig : path_to_your_project/src/AppBundle/Resources/views/Form/fields.html.twig.
Name of the block will be app_persona_documento_widget.
Thus, example of fields.html.twig :
{% trans_default_domain 'messages' %}
{% block app_persona_documento_widget %}
{% spaceless %}
<div class="form-group" {{ block('widget_container_attributes') }}>
<div class="col-sm-12">
{{ form_widget(form.name, {'attr' : { 'placeholder' : 'app.form.label.name'|trans , 'class' : 'form-control' }}) }}
</div>
</div>
{% endspaceless %}
{% endblock app_persona_documento_widget %}
Also pay attention that "app_persona_documento_widget" - assembled from the getBlockPrefix() of you PersonaDocumentoType plus string "_widget"
Register new form themes in config.yml
# Twig Configuration
twig:
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
form_themes:
# other form themes
# ...
- 'AppBundle:Form:fields.html.twig'
Render collection in parent form
{{ form_start(formulario_form) }}
<div class="form-group">
<label for="" class="col-sm-2 control-label">
Label
</label>
<div class="col-sm-10 documentos" data-prototype="{{ form_widget(formulario_form.documentos.vars.prototype)|e('html_attr') }}">
{% for document in formulario_form.documentos %}
<div>
{{ form_widget(document) }}
</div>
{% endfor %}
</div>
</div>
<span>
{{ form_errors(formulario_form) }}
</span>
{# Here you can add another fields of form #}
{{ form_end(formulario_form) }}
Of course, you also need buttons: one "Add another document" button and "Remove" buttons for each "Documento" item.
Symfony documentation suggests that we use JavaScript for this purpose.
You can read more here in official docs
Also you can install Ninsuo/symfony-collection - A jQuery plugin that manages adding, deleting and moving elements from a Symfony collection

symfony2: multiple entities one form

I have 2 entities:
ADS\LinkBundle\Entity\Link:
type: entity
table: null
repositoryClass: ADS\LinkBundle\Entity\LinkRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
dateAdded:
type: datetime
expirationDate:
type: datetime
nullable: true
designator:
type: string
length: 255
nullable: false
unique: true
slug:
type: string
length: 255
nullable: true
unique: true
manyToOne:
company:
targetEntity: ADS\UserBundle\Entity\Company
inversedBy: link
joinColumn:
name: company_id
referencedColumnName: id
nullable: true
createdBy:
targetEntity: ADS\UserBundle\Entity\User
inversedBy: link
joinColumn:
name: createdBy_id
referencedColumnName: id
domain:
targetEntity: ADS\DomainBundle\Entity\Domain
inversedBy: link
joinColumn:
name: domain_id
referencedColumnNames: id
oneToMany:
paths:
targetEntity: ADS\LinkBundle\Entity\Path
mappedBy: link
cascade: [persist]
lifecycleCallbacks: { }
and
ADS\LinkBundle\Entity\Path:
type: entity
table: null
repositoryClass: ADS\LinkBundle\Entity\PathRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
pathAddress:
type: string
length: 255
pathWeight:
type: string
length: 255
manyToOne:
link:
targetEntity: ADS\LinkBundle\Entity\Link
inversedBy: paths
joinColumn:
name: link_id
referencedColumnName: id
lifecycleCallbacks: { }
I have everything figured out except for the paths portion of the entity. This is for an A/B split test, so each link can have 2 paths. Each path will consist of a web address, and a number ( 0 - 100 )
Here is my form in it's current state:
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PathType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('pathAddress')
->add('pathWeight')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Path'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
and
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LinkType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('designator')
->add('domain', 'entity', array(
'class' => 'ADS\DomainBundle\Entity\Domain',
'property' => 'domainAddress'
))
->add('paths', 'collection', array('type' => new PathType(), 'allow_add' => true))
->add('Submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Link'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
What I need to figure out, is when creating a link, I need to also be able to create the correct path and weight to go with it. The paths won't be in the database before a link is created.
Here is what I have for my controller:
public function newAction(Request $request) {
$entity = new Link();
$form = $this->createForm(new LinkType(), $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$code = $this->get('ads.default');
$em = $this->getDoctrine()->getManager();
$user = $this->getUser();
$entity->setDateAdded(new \DateTime("now"));
$entity->setCreatedBy($user);
$entity->setSlug($code->generateToken(5));
$entity->setCompany($user->getParentCompany());
$em->persist($entity);
$em->flush();
return new Response(json_encode(array('error' => '0', 'success' => '1')));
}
return new Response(json_encode(array('error' => count($form->getErrors()), 'success' => '0')));
}
return $this->render('ADSLinkBundle:Default:form.html.twig', array(
'entity' => $entity,
'saction' => $this->generateUrl('ads.link.new'),
'form' => $form->createView()
));
}
Thanks to #Onema ( read the comments above ), I've figured this out. By reading the documentation at http://symfony.com/doc/current/cookbook/form/form_collections.html It gave me information I needed to get this done.
First step in doing what I needed to do, was to create a new form type called PathsType.php which houses the fields associated with the Paths Entity
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PathType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('pathAddress')
->add('pathWeight')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Path'));
}
public function getName() { return 'ads_linkbundle_path'; }
}
Then modifying the LinkType.php to utilize this new form
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LinkType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('designator')
->add('domain', 'entity', array(
'class' => 'ADS\DomainBundle\Entity\Domain',
'property' => 'domainAddress'
))
->add('paths', 'collection', array(
'type' => new PathType(),
'allow_add' => true,))
->add('Submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Link'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
The addition of allow_add makes it so that you can add multiple instances of that form.
Within the view, I now utilize the data-prototype attribute. In the documentation, it has the example using a list item - so that's where I started.
<ul class="tags" data-prototype="{{ form_widget(form.paths.vars.prototype)|e }}"></ul>
Then came the jQuery functions ( listed on the documentation link above, simple copy/paste will work )
This got the system working, with 1 small issue and that in my paths entity, I have a relationship to the Link entity but it was not noticing this relationship and had the link_id field as null
To combat this, we edit LinkType.php one more time, and add by_reference = false to the collection definition. We then edit the addPath method inside the entity to look like so:
public function addPath(\ADS\LinkBundle\Entity\Path $paths)
{
$paths->setLink($this);
$this->paths->add($paths);
}
This sets the current link object, as the link the path is associated with.
At this point, the system is working flawlessly. It's creating everything that it needs to, only need to adjust the display a little bit. I personally opted to use a twig macro to modify the html output contained in data-prototype
my macro as it currently sits (incomplete - but working ) which I added to the beginning of my form.html.twig
{% macro path_prototype(paths) %}
<div class="form-group col-md-10">
<div class="col-md-3">
<label class="control-label">Address</label>
</div>
<div class="col-md-9">
{{ form_widget(paths.pathAddress, { 'attr' : { 'class' : 'form-control required' }}) }}
</div>
</div>
{% endmacro %}
In the HTML for the form itself, I removed the list creation, and replaced it with:
<div class="form-group">
{{ form_label(form.paths,'Destination(s)', { 'label_attr' : {'class' : 'col-md-12 control-label align-left text-left' }}) }}
<div class="tags" data-prototype="{{ _self.path_prototype(form.paths.vars.prototype)|e }}">
</div>
</div>
I then modified my javascript to use the div as a starting point instead of the ul in the example.
<script type="text/javascript">
var $collectionHolder;
// setup an "add a tag" link
var $addTagLink = $('Add Another Destination');
var $newLinkLi = $('<div></div>').append($addTagLink);
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
$collectionHolder = $('div.tags');
// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
addTagForm($collectionHolder, $newLinkLi);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTagForm($collectionHolder, $newLinkLi);
});
});
function addTagForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
console.log(index);
if (index == 1) {
console.log('something');
$('a.add_tag_link').remove();
}
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = newForm;
$newLinkLi.before($newFormLi);
}
</script>
Being that these paths are destination addresses for an A/B split test within my marketing app, I opted to limit the paths to 2 per link. And with this, I have successfully setup a form to use a collections type.

Adding action in SonataAdminBundle

I'm trying to add an action in sonata admin bundle. I changed my Admin class with :
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->add('name')
// add custom action links
->add('_action', 'actions', array(
'actions' => array(
'view' => array(),
'calculate' => array('template' => 'myappMyBundle:Admin:list__action_calculate.html.twig'),
'edit' => array(),
)
))
;
}
and
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
if (!$childAdmin && !in_array($action, array('edit'))) {
return;
}
$admin = $this->isChild() ? $this->getParent() : $this;
$id = $admin->getRequest()->get('id');
$menu->addChild('calculate', array('uri' => 'http://google.com?id=' . $id));
}
and put a template called list__action_calculate.html.twig in src/myapp/MyBundle/Resources/views/Admin/ :
{% if admin.isGranted('EDIT', object) and admin.hasRoute('edit') %}
<a href="{{ admin.generateObjectUrl('calculate', object) }}" class="calculate_link" title="{{ 'action_calculate'|trans({}, 'SonataAdminBundle') }}">
<img src="{{ asset('bundles/sonataadmin/famfamfam/page_white_edit.png') }}" alt="{{ 'action_calculate'|trans({}, 'SonataAdminBundle') }}" />
</a>
{% endif %}
But i got this error from symfony :
An exception has been thrown during the rendering of a template
("unable to find the route `mysite.mybundle.admin.myentity.calculate`")
in "SonataAdminBundle:CRUD:list.html.twig"
What have i missed ?
Is there a clue in the documentation than this page of the Doc.
Finally got it !
In the admin class :
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('calculate');
}
# Override to add actions like delete, etc...
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'))
{
// define calculate action
$actions['calculate']= array ('label' => 'Calculate', 'ask_confirmation' => true );
}
return $actions;
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->add('name')
// add custom action links
->add('_action', 'actions', array(
'actions' => array(
'view' => array(),
'calculate' => array('template' => 'chemoinfoEdrugBundle:CRUD:list__action_calculate.html.twig'),
'edit' => array(),
)
))
;
}
and in admin controller :
public function batchActionCalculate(ProxyQueryInterface $selectedModelQuery)
{
...
}
and in /src/mysite/mybundle/Resources/views/CRUD :
{% if admin.isGranted('EDIT', object) and admin.hasRoute('edit') %}
<a href="{{ admin.generateObjectUrl('calculate', object) }}" class="calculate_link" title="{{ 'action_calculate'|trans({}, 'SonataAdminBundle') }}">
<img src="{{ asset('bundles/sonataadmin/famfamfam/calculator.png') }}" alt="{{ 'action_calculate'|trans({}, 'SonataAdminBundle') }}" />
</a>
{% endif %}

Resources