add entity inside other with many to one relation in sonata - symfony

I'm using Symfony and sonata bundle, and I have 2 Entities, related with a ManyToOne/OneToMany relation as follows:
One Category can have many SubCategory entities. For that, in Sonata's FormMapper, when I add a new category I want to add a button to display a popup to to create more than one SubCategory .. so how can I override the Twig of Sonata to do that?
CategoryAdmin
class CategoryAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->add('subcats', 'entity', array(
'class'=> 'ProductBundle\Entity\SubCategory',
'multiple' => true,
))
;
}
}

You can use your one template by adding:
$formMapper
->add('name')
->add('subcats', 'entity', array(
'class'=> 'ProductBundle\Entity\SubCategory',
'multiple' => true,
'attr' => array('template'=> 'your\path\to\twig')
))
;
and this twig should extends from base_edit_form.html.twig
{% extends 'SonataAdminBundle:CRUD:base_edit_form.html.twig' %}
{% block field %}
<div>
// put your code here
</div>
{% endblock %}
Or you have an other solution that can fix your problem, you can use the Sonata_Type_Model
->add('subcats','sonata_type_model', array(
'multiple' => true,
'by_reference' => false
))
This solution will give you that you like, a button to add and remove to create your SubCategory

Related

How to get a custom value for a field type in sonata?

With Sonata, when I create a contract with a choiceType, the user can choose contract1 or contract2 and in my database I would get "451" for contract1 and "678" for contract2.
In my Field List all my data are displayed but for my contract I've got either "451" or "678" and I would like instead of those numbers, contract1 or contract2.
This is my field for creating the contract :
$mapper
->add('contract', ChoiceType::class, [
'choices' => [
'contract1' => '451',
'contract2' => '678',
],
])
And in my code for the field, I don't know how to tell it if 451 then 'contract1'. I started like that :
->add('contract', null, [
'label' => 'Contract',
])
Any idea ?
You can use the form entity type which would solve your problem:
$builder->add('contract', EntityType::class, [
// looks for choices from this entity
'class' => Contract::class,
// uses the Contrzct.name property as the visible option string
'choice_label' => 'name',
// Query builder to select your to specific contract
'query_builder' => function (ContractRepositoty $contractRepository) {
return $contractRepository->createQueryBuilder('support_time_slot')
->where('contract.id in :ids')
->setParameter('ids', [461,678])
->orderBy('contract.name');
},
// used to render a select box, check boxes or radios
'multiple' => true,
'expanded' => true,
]);
I found a solution. I created a specific template and in it I translated the value I wanted:
->add('contract', null, [
'label' => 'Contract',
'template' => 'AdminBundle:ContractAdmin:list__operation.html.twig'
])
And my Twig :
{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}
{% block field %}
{% if value %}
{{ ('contract.operation.'~value~'.value')|trans }}
{% endif %}
{% endblock %}

SonataAdmin sonata_type_model with lot of rows throw OutOfMemoryException

I have a simple one to many relation Listing -> Booking with many thousands listings.
When i add the following SonataAdmin class :
class BookingAdmin extends Admin
{
...
$formMapper
->add(
'listing',
null,
array(
'disabled' => true,
)
),
...
An OutOfMemoryException is thrown due to the lot of numbers of listings.
I would like to know how to avoid this error by displaying the listing title in the form without using the choice list.
You could use a 'sonata_type_model_autocomplete' form type for these cases (Ref.):
class BookingAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// the dropdown autocomplete list will show only Booking
// entities that contain specified text in "title" attribute
$formMapper->add('listing', 'sonata_type_model_autocomplete', array(
'property' => 'title'
));
}
}
This one avoids to query all rows to populate the widget.
I found an another solution than Yonel one.
In this solution we only get the current Listing of the persisted Booking entity in the choice widget. It is only useful if the Listing must not be changed.
class BookingAdmin extends Admin
{
...
protected function configureFormFields(FormMapper $formMapper)
{
$listing= $this->getSubject();
$formMapper
->add(
'listing',
'sonata_type_model',
array(
'query' => $this->modelManager
->getEntityManager('Bundle:Listing')
->getRepository('Bundle:Listing')
->find(
$listing->getId()
),
'disabled' => true,
)
);
...

Use sonata_type_collection inside custom type

What I want to do is add sonata_type_collection to my custom formType.
Normal way is add sonata_collection_type to $formMaper inside AdminClass like:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('elements, 'sonata_type_collection', array(
'some_options' => 'options'
))
}
It work perfect, but i have my custom form type, and when i defined it like:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formMapper->add('elements, 'sonata_type_collection', array(
'some_options' => 'options'
))
}
It doesn't work (it appear only label of filed). Problem is wrong template, so I tried to set formAdminTemplate
I made it by set template in view
{% form_theme formElement 'SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig' %}
Problem is sonata_admin variable inside this 'formTheme'. This variable doesn't exist in my form.
Of course my form type is related to admin class but i don't know how could I I tell symfony about this relation
You need an admin class for your collection child :
$formMapper->add('customizations', 'sonata_type_collection',
array(
'required' => true,
'type_options' => array('delete' => true),
'by_reference' => false,
'mapped' => true
),
array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'targetEntity' => '/path/to/Entity/Customization',
'admin_code' => 'my.service.customization_admin'
)
);
I find solution. Instead using my custom type, I defined form using admin class. I need this form outside admin so it was little difficult.
First of all in my controller i get admin class from service. Inside admin class I override 3 methods which are use to create form
public function getFormBuilder()
public function defineFormBuilder(FormBuilder $formBuilder)
public function buildForm()
then i had to save my entity by sonata admin way. using create method instead handleRequest.

How to loop through form fields in Sensio Generator Bundle

I have overridden the Sensio Generator Bundle for CRUD in order to better suit my needs.
What I would like to do is to be able to loop through the entity fields.
It is done by default in show.html.twig but not in new and edit views.
When I implement the same logic in new.html.twig.twig it doesn't work though it does for edit.html.twig.twig.
{#app/Resources/SensioGeneratorBundle/skeleton/crud/views/new.html.twig.twig#}
{% for field, metadata in fields %}
{% if field not in 'id' %}
{{ '{{ form_row(edit_form.' ~ field ~ ')}}' }}
{% endif %}
{% endfor %}
When running the generator, the error is: Variable "fields" does not exist in "crud/views/new.html.twig.twig" at line 9
Ok, in fact it is an issue in Sensio Generator Bundle.
In the file: sensio\generator-bundle\Sensio\Bundle\GeneratorBundle\Generator\DoctrineCrudGenerator.php the generateNewView function is missing a paramter. It is not passing the fields as opposed to generateShowView.
Here is the comparison:
protected function generateNewView($dir)
{
$this->renderFile('crud/views/new.html.twig.twig', $dir.'/new.html.twig', array(
'bundle' => $this->bundle->getName(),
'entity' => $this->entity,
'route_prefix' => $this->routePrefix,
'route_name_prefix' => $this->routeNamePrefix,
'actions' => $this->actions,
));
}
versus
protected function generateShowView($dir)
{
$this->renderFile('crud/views/show.html.twig.twig', $dir.'/show.html.twig', array(
'bundle' => $this->bundle->getName(),
'entity' => $this->entity,
'fields' => $this->metadata->fieldMappings,
'actions' => $this->actions,
'route_prefix' => $this->routePrefix,
'route_name_prefix' => $this->routeNamePrefix,
));
}
I'll try to post this as an improvement.

Symfony2 : Radio buttons in a collection

In my application, I created a form using the collection field type :
$builder->add('tags', 'collection', array(
'type' => new TagType(),
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
With some JQuery, this code works correctly, but now I would like to select one of this dynamic tag to make it "the main tag".
In my Tag entity, I added a boolean attribute which define if the tag is the main or not :
/**
* #ORM\Column(name="main", type="boolean")
*/
private $main;
But in my view, each row now contains a checkbox. So I can select more than one main tag. How to transform this checkbox in radio button please ?
You're not tackling the problem from the right angle. If there should be a main tag, then this property should not be added in the Tag entity itself, but in the entity that contains it!
I'm speaking of the data_class entity related to the form having the tags attribute. This is the entity that should have a mainTag property.
If defined properly, this new mainTag attribute will not be a boolean, for it will contain a Tag instance, and thus will not be associated to a checkbox entry.
So, the way I see it, you should have a mainTag property containing your instance and a tags property that conatins all other tags.
The problem with that is that your collection field will no longer contain the main tag. You should thus also create a special getter getAllTags that will merge your main tag with all others, and change your collection definition to:
$builder->add('allTags', 'collection', array(
'type' => new TagType(),
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
Now, how do we add the radio boxes, you may ask? For this, you will have to generate a new field:
$builder->add('mainTag', 'radio', array(
'type' => 'choice',
'multiple' => false,
'expanded' => true,
'property_path' => 'mainTag.id', // Necessary, for 'choice' does not support data_classes
));
These are the basics however, it only grows more complex from here. The real problem here is how your form is displayed. In a same field, you mix the usual display of a collection and the display of a choice field of the parent form of that collection. This will force you to use form theming.
To allow some room to reusability, you need to create a custom field. The associated data_class:
class TagSelection
{
private mainTag;
private $tags;
public function getAllTags()
{
return array_merge(array($this->getMainTag()), $this->getTags());
}
public function setAllTags($tags)
{
// If the main tag is not null, search and remove it before calling setTags($tags)
}
// Getters, setters
}
The form type:
class TagSelectionType extends AbstractType
{
protected buildForm( ... )
{
$builder->add('allTags', 'collection', array(
'type' => new TagType(),
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
// Since we cannot know which tags are available before binding or setting data, a listener must be used
$formFactory = $builder->getFormFactory();
$listener = function(FormEvent $event) use ($formFactory) {
$data = $event->getForm()->getData();
// Get all tags id currently in the data
$choices = ...;
// Careful, in PRE_BIND this is an array of scalars while in PRE_SET_DATA it is an array of Tag instances
$field = $this->factory->createNamed('mainTag', 'radio', null, array(
'type' => 'choice',
'multiple' => false,
'expanded' => true,
'choices' => $choices,
'property_path' => 'mainTag.id',
));
$event->getForm()->add($field);
}
$builder->addEventListener(FormEvent::PRE_SET_DATA, $listener);
$builder->addEventListener(FormEvent::PRE_BIND, $listener);
}
public function getName()
{
return 'tag_selection';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TagSelection', // Adapt depending on class name
// 'prototype' => true,
));
}
}
Finally, in the form theme template:
{% block tag_selection_widget %}
{% spaceless %}
{# {% set attr = attr|default({})|merge({'data-prototype': form_widget(prototype)}) %} #}
<ul {{ block('widget_attributes') }}>
{% for child in form.allTags %}
<li>{{ form_widget(form.mainTag[child.name]) }} {{ form_widget(child) }}</li>
{% endfor %}
</ul>
{% endspaceless %}
{% endblock tag_selection_widget %}
Lastly, we need to include that in your parent entity, the one that originally contained tags:
class entity
{
// Doctrine definition and whatnot
private $tags;
// Doctrine definition and whatnot
private $mainTag;
...
public setAllTags($tagSelection)
{
$this->setMainTag($tagSelection->getMainTag());
$this->setTags($tagSelection->getTags());
}
public getAllTags()
{
$ret = new TagSelection();
$ret->setMainTag($this->getMainTag());
$ret->setTags($this->getTags());
return $ret;
}
...
}
And in your original form:
$builder->add('allTags', new TagSelection(), array(
'label' => false,
));
I recognize the solution I propose is verbose, however it seems to me to be the most efficient. What you are trying to do cannot be done easily in Symfony.
You can also note that there is an odd "prototype" option in the comment. I just wanted to underline a very useful property of "collection" in your case: the prototype option contains a blank item of your collection, with placeholders to replace. This allow to quickly add new items in a collection field using javascript, more info here.
This is not the right solution, but since you are using jQuery to add/remove...
TagType
->add('main', 'radio', [
'attr' => [
'class' => 'toggle'
],
'required' => false
])
jQuery
div.on('change', 'input.toggle', function() {
div
.find('input.toggle')
.not(this)
.removeAttr('checked');
});
http://jsfiddle.net/coma/CnvMk/
And use a callback constraint to ensure that there is only one main tag.
First thing you should wary about - its that in your scheme if tag become main for one entity it will be main for all entities because the tag store attribute and few entities can be tagged with one tag.
So simplest decision here is to create new property main_tag near tags in your entity, create hidden field main_tag(with id to Tag Data transformer) in your form and populate and change this field with jQuery(for example set it on tag click or clear on main tag delete)
Maybe there is something to do with the multiple form option, but it might require a little tweaking on your collection form and tag entity.

Resources