How to get the defined field in sonataadmin query builder? - symfony

There is the following code
$form->with('Item')->add('parent', null, array(
'label' => 'Category',
'required' => true,
'query_builder' =>
function($er) use ($id) {
$qb = $er->createQueryBuilder('p');
if ($id){
$qb->where('p.id <> :id')
->setParameter('id', $id);
}
$qb->orderBy('p.root, p.lft', 'ASC');
return $qb;
}
.........
Result is the entity-objects collection which is given to the string (__toString method).
It return name-field.
But I need get the another field - url.
How to get url value instead the name in the select-list form?
The query_builder type return object => how to change this form that it works likewise query_builder?

I didn't work with SonataAdminBundle forms, but I think that it works absolutely like symfony forms. All that you need here is to add 'class' and 'property' values to your options list:
$form->with('Item')->add('parent', null, array(
'class' => 'Acme\DemoBundle\Entity\Category',
'property' => 'url',
'label' => 'Category',
'required' => true,
'query_builder' =>
function($er) use ($id) {
$qb = $er->createQueryBuilder('p');
if ($id){
$qb->where('p.id <> :id')
->setParameter('id', $id);
}
$qb->orderBy('p.root, p.lft', 'ASC');
return $qb;
}
property - is the name of the field in your entity that will represent your Entity's value instead of calling __toString().
But also... If you need always represent your Entity as URL you can simply override __toString() method in the Entity class to something like that:
public function __toString() {
return $this->url;
}

In my case, ->orderBy() used in query_builder worked, but was overwritten for unknown reasons "somewhere" later. Fun fact: when I used extended => true, everything was rendered like sorted in the query_builder. But by using extended => false my <option>s are re-sorted before select2 touched it.
As a workaround I did this:
config/packages/twig.yaml
twig:
paths:
'%kernel.project_dir%/templates': '%kernel.project_dir%/templates'
# This is to prevent a infinite loop when extending the parent template
'vendor/sonata-project/admin-bundle/src/Resources/views': SonataAdminBundleOriginal
And then I done the sorting I wanted again in twig for the project entity:
templates/bundles/SonataAdminBundle/Form/form_admin_fields.html.twig:
{% extends '#SonataAdminBundleOriginal/Form/form_admin_fields.html.twig' %}
{%- block choice_widget_options -%}
{% if name == 'project' %}
{% set options = options|sort((a, b) => a.data.numberSortable <=> b.data.numberSortable)|reverse %}
{% endif %}
{{ parent() }}
{%- endblock choice_widget_options -%}

Just in case if someone needs to "filter" an entity by a FK (foreign key) just like i needed (and searched for 3 days), here is the solution:
In the __construct you can set/find what you need. In my case i work this "sessions" (year). So i find the session->status=true:
$this->sessionActive = $this->sessionRepository->findOneBy(['status'=>true]);
Then in the QueryBuilder:
protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
{
$rootAlias = current($query->getRootAliases());
$query
->select($rootAlias)
->where($rootAlias.'.session = :id')
->addOrderBy($rootAlias.'.dateStart', 'ASC')
->setParameter('id', $this->sessionActive->getId());
return $query;
}
Note: I have always one Session->status set to "true". If you might have null values don't forget to make a condition so that it doesn't give you an error!
Regards.

Related

Add placeholder or empty value to Entity Type with multiple = true in Symfony

I want to do this:
$builder->add('participants', EntityType::class, array(
'label' => 'Teilnehmer',
'class' => SchoolUser::class,
'multiple' => true,
'choices' => $this->getParticipantsOfEntry($builder),
'empty_value' => 'All',
'empty_data' => null,
'preferred_choices' => array(null)
));
But I get no selected 'All' - field at all. This should not be hard, I wonder where is my mistake?
'placeholder' = 'All',
did also not work for me.
How can I do this?
I know the question was 4-months ago but I ran into a similar problem so thought I would share my solution in case it helps others.
First in the Form object:
class MealFormType extends AbstractType {
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder->add( 'courses', EntityType::class, array(
'class' => MealCourse::class,
'multiple' => true,
) );
$builder->addEventListener( FormEvents::PRE_SUBMIT, function( FormEvent $event ) {
// Remove added entries here... maybe something like
$data = $event->getData();
if( ($key = array_search( 'all', $data['courses'] ) ) !== false ) {
unset( $data['courses'][$key] );
$event->setData( $data );
}
} );
}
public function configureOptions( OptionsResolver $resolver ) {
$resolver->setDefaults( ['data_class' => Meal::class] );
}
public function finishView( FormView $view, FormInterface $form, array $options) {
$newChoice = new ChoiceView( null, 'all', 'I want them all!' );
array_unshift( $view->children['courses']->vars['choices'], $newChoice );
}
}
This creates the form with multiple meal courses that can be selected, such as appetizer, soup, salad, entree, coffee, desert, ... using the buildForm() method. Also in this method an event listener for the PRE_SUBMIT event is added whose job it will be to remove any choices we have added since they are probably not real (if they were, we wouldn't have to add them in this painful way).
It sets the data_class as usual in configureOptions().
Lastly it provides the finishView() method that creates a new choice with value of 'all' that will display as 'I want them all' in the select, then adds it at the beginning for the 'choices' array for the 'courses' form entry (this would be $view->children['courses']->vars['choices'][] = $newChoice; to put it at the end).
Ok, this is fine but it really doesn't do anything more than having an additional option to click or unclick. To use this we need some Javascript to manage the form. I just stuck this Javascript in the bottom of my Twig file and use jQuery.
The Twig file:
{% block body %}
{{ form_start(mealForm) }}
{{ form_row(mealForm.courses,
{'attr': {'class': 'js-meal_course-select'}}) }}
<button type="submit">Save</button>
{{ form_end(mealForm) }}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
var mealCourseAllSelected = false;
jQuery(document).ready( function() {
// This will be called whenever an entry is selected or deselected
$('.js-meal_course-select').change( function() {
var selectedSet = $(this).val();
if( mealCourseAllSelected &&
(selectedSet.length != $(this).get(0).options.length - 1 ||
selectedSet.includes( 'all' )) ) {
var opts = $(this).get(0).options;
var len = opts.length;
for( var i = 0; i < len; ++i ) {
if( opts[i].value == 'all' ) opts[i].selected = false;
}
mealCourseAllSelected = false;
} else if( selectedSet.includes( "all" ) ||
selectedSet.length == $(this).get(0).options.length - 1 ) {
$('.js-meal_course-select option').prop( 'selected', true );
mealCourseAllSelected = true;
}
}
);
</script>
{% endblock %}
The JavaScript responds to changes to what is selected. If the select option with the value of 'all' is in the selected set, then all the select options are marked as selected, and a flag is set to say so. This is also done if everything is selected except the 'all' entry because that is kind of the definition of all.
If the 'all are selected' flag is set and there is a change, then that change must be deselecting at least one, unless that one is the select option with the value of 'all', it deselects the selection option with the value of 'all' and clears the 'all are selected' flag. If all are selected and the user tries to unselect just the select option with the value of 'all', it is just reselected.

Get parameter from a search form without any entity attached

I am creating simple search form that will query and render data using Doctrine LIKE expression.I am confused how to get the parameter like this
$name = 'San Francisco China';
In the controller i created a simple form
public function searchAction(Request $request)
{
$data = array();
$form= $this->createFormBuilder($data)
->add('name_city', 'text', array(
'label' => 'Search Here',
'error_bubbling' => true,
))
->add('search', 'submit')
->getForm();
if ($request->isMethod('POST')) {
$form->handleRequest($request);
$data = $form->getData();
}
$name = $request->request->get('name_city');//this confused me
//$name = 'Beijing Angeles';//this will work
$em = $this->getDoctrine()->getManager();
$city = $em->getRepository('Bundle:City')->searchCity($name);
return $this->render('Bundle:City:list.html.twig', array(
'city' => $city,
'form' => $form->createView(),
));
}
In this case, the $name variable is passed to the searchCity method as the argument
public function searchCity($name)
{
return $this
->createQueryBuilder('c')
->select('c')
->where('c.name LIKE :name_city')
->setParameter('name_city', '%'.$name.'%')
// ->orderBy('v.dateCreated', 'DESC')
->getQuery()
->getResult()
;
}
//list.twig
{% extends '::base.html.twig' %}
{% block body %}
{{ form(form)}}
{% for city in city %}
{{ city }}
{% endfor %}
{% endblock %}
This will not work since all city name are displayed during page load. If i manually add a value to the $name, e.g $name = 'Beijing Washington', search works.Whats the correct way?
As for the setting the default name_city, you could do this:
$data = array(
'name_city' => 'Beijing Angeles'
);
$form= $this->createFormBuilder($data)
->add('name_city', 'text', array(
'label' => 'Search Here',
'error_bubbling' => true,
))
->add('search', 'submit')
->getForm();
// The rest of your code
Then, get the city name like:
$form->handleRequest($request);
$data = $form->getData();
$name = $data['name_city'];
You might notice that I removed the POST check. That's because search forms are traditionally set to method="get", because of history/bookmarks. So, in your example, you would never check for POST but handlerRequest() each and every time.
Hope this helps...
Feel free to correct me if I misunderstood the intention here ;)

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.

How to pass another entity info into form in Symfony2?

What is best practice to include another info in form from another entity?
I have Student Entity, Group Entity, StudentsGroup and Attendance Entity.
Student have id, code, and name.
Group have id and name.
StudentsGroup have id, group_id, and student_id.
Attendance have id, students_group_id, date and status.
Group can have many Student which saved in StudentsGroup. Why I make StudentsGroup? Because actually 1 Group can have some sub groups like SubjectsGroup etc. And Attendance save student information by StudentsGroup Id which same student can have different students_group_id.
Now, the problem is : How to show Student information in collection form of attendance?
All Entity relationship is declared as object, so actually we can access em freely from any entity. But I don't know how to do that in form. Here my form :
<?php
/* Collection Form */
$tanggal = new \DateTime($request->request->get('sifo_adminbundle_studentsgrouping')['tanggal']);
$attendances = new StudentsGrouping();
foreach ($entities as $temp) {
$entity = new Attendance();
$entity = $em->getRepository('SifoAdminBundle:Attendance')->findOneBy(array('studentsGrouping' => $temp, 'date' => $tanggal));
if ($entity){
$attendances->getAttendances()->add($entity);
}
}
$form = $this->createCollectionForm($attendances, $id, $tanggal);
return $this->render('SifoAdminBundle:DftAbsensi:manage.html.twig', array(
'form' => $form->createView(),
));
This is how I render it in twig :
{{ form_start(form_collection) }}
{{ form_row(form_collection.tanggal) }}
{% for attendance in form_collection.attendances %}
{{ form_row(attendance.status) }}
{% endfor %}
{{ form_end(form_collection) }}
## Concept ##
I'm thinking about creating entity and pass it into form like this :
foreach ($entities as $temp) {
$entity = new Student();
$entity = $em->getRepository('SifoAdminBundle:Student')->find($temp->getId());
if ($entity){
$entities[i] = $entity;
}
$i++
}
and then in twig show it like this :
{{ form_start(form_collection) }}
{{ form_row(form_collection.tanggal) }}
{% for key, attendance in form_collection.attendances %}
{{ entities[key].code }}
{{ entities[key].name }}
{{ form_row(attendance.status) }}
{% endfor %}
{{ form_end(form_collection) }}
But I feel not comfort with this. Am I really need to make new entity just for showing name and code from Student Entity? Is there a best practice to do this?
You can use entity field.
$builder->add('users', 'entity', array(
'class' => 'AcmeHelloBundle:User',
'multiple' => true, /* you can choose more than one */
'mapped' => false, /* if you are using the form with an entity */
'query_builder' => function(EntityRepository $er) {
/* use query builder to get correct results */
return $er->createQueryBuilder('u')->orderBy('u.username', 'ASC');
},
));
There are two important keys in above.
'multiple' => true ----- you can able to choose more than one
'mapped' => false ----- if you are using form with an entity, your form will automatically looks for a connection between these entities and if can not found, throws exception. to avoid of that problem you should set this option to false
http://symfony.com/doc/current/reference/forms/types/entity.html

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