Symfony - manyToMany not getting saved - php

The Scenario
I have two Entities linked together with manyToMany relation.
Entities
User
Interest
So on a user's profile form there is a field called Interests which is rendered using select2.
Now a user can select as many Interests as they want and upon saving doctrine is doing the nice job of saving the selected Interests in the linked table. When I reload the profile page I can see that interests that I already selected.
The Problem
Although the form field is linked with an Interest entity
$form->add('interest', EntityType::class, array(
'class' => 'AppBundle\Entity\Interest',
'multiple' => true,
'expanded' => false,
'by_reference' => false)
A user can also add interests of their own which do not exist in Interest table with the help of Tagging Support on front end and on backend to save this information I have Form Event Subscriber in place that checks if any of the Interests submitted by the user does not exist in Interest table add them and it is here where I get the following exception
Message
This value is not valid.
Origin
interest
Cause
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).children[interest] = [0 => 1, 1 => 4, 2 => 7, 3 => www]
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "interest": Could not find all matching choices for the given values
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Could not find all matching choices for the given values
Here is the Event Subscriber code
namespace AppBundle\Form\EventListener;
use AppBundle\Entity\Interest;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
class AddProfileFieldSubscriber implements EventSubscriberInterface
{
protected $authorizationChecker;
protected $em;
function __construct(AuthorizationChecker $authorizationChecker, EntityManager $em)
{
$this->authorizationChecker = $authorizationChecker;
$this->em = $em;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that you want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(
FormEvents::PRE_SUBMIT => 'onPreSubmit'
);
}
/**
* #param FormEvent $event
*/
public function onPreSubmit(FormEvent $event){
$interestTags = $event->getData();
$interestTags = $interestTags['interest'];
foreach($interestTags as $interestTag){
$interest = $this->em->getRepository('AppBundle:Interest')->findOneBy(array('id' => $interestTag));
if(!$interest){
$newInterest = new Interest();
$newInterest->setName($interestTag);
$this->em->persist($newInterest);
$this->em->flush();
}
}
}
}
The Attempt
I updated the form code to as following by adding choice_value
$form->add('interest', EntityType::class, array(
'class' => 'AppBundle\Entity\Interest',
'multiple' => true,
'expanded' => false,
'by_reference' => false,
'choice_value' => 'name'
)
);
and I changed the query inside the Event Subscriber from
$interest = $this->em->getRepository('AppBundle:Interest')->findOneBy(array('id' => $interestTag));
to
$interest = $this->em->getRepository('AppBundle:Interest')->findOneBy(array('name' => $interestTag));
This worked perfectly at first but when I reload the profile page my interest field appears empty
The reason why it appears empty is because (my assumption) of id="select2-user_interest-result-5z18-Education" I think that needs to look something like id="select2-user_interest-result-5z18-66"
<ul class="select2-results__options" role="tree" aria-multiselectable="true" id="select2-user_interest-results"
aria-expanded="true" aria-hidden="false">
<li class="select2-results__option" id="select2-user_interest-result-5z18-Education" role="treeitem"
aria-selected="false">Education
</li>
<li class="select2-results__option" id="select2-user_interest-result-rdka-History" role="treeitem"
aria-selected="false">History
</li>
<li class="select2-results__option select2-results__option--highlighted"
id="select2-user_interest-result-lfq4-Architecture" role="treeitem" aria-selected="false">Architecture
</li>
<li class="select2-results__option" id="select2-user_interest-result-qqiq-Entrepreneurship" role="treeitem"
aria-selected="false">Entrepreneurship
</li>
<li class="select2-results__option" id="select2-user_interest-result-qutx-Technology" role="treeitem"
aria-selected="false">Technology
</li>
<li class="select2-results__option" id="select2-user_interest-result-sfx4-Engineering" role="treeitem"
aria-selected="false">Engineering
</li>
I crossed check the data in Interest table and I can see the new interests added by the user which did not exist before, so its working but on front end its not being displayed. Out of curiosity i removed the choice_value and then reloaded the profile page and I could see the new interests
I will really appreciate if anything can push me in right direction and let me know what am I missing and how can i get this to work.

You can't do that as EntityType extend ChoiceType which doesn't allow adding new values in the choice list.
You should use CollectionType like in the cookbook http://symfony.com/doc/current/cookbook/form/form_collections.html
To make a proper select in your code, you'll also have to insert in the view, along with your form, the full list of all interests.
Then your view should looks like :
<select name='{{ form.interest.vars.full_name }}' id="{{ form.interest.vars.id }}" class='select2'>
{% for interestList as interestItem %}
<option value='{{ interestItem.id }}' {% if some_logic_to_check_whether_the_item_is_selected %}selected='selected'{% endif %} >{{ interestItem.name }}</option>
{% endfor %}
</select>

Related

Symfony EntityType - How to use custom layout?

In my Symfony 3.x project, I have 3 entities:
Project
Property
Category
Assumptions:
Each Project has multiple Properties.
Each Property has one Category.
Each Property might have parent Property.
I would like to render Symfony form for Project entity. I'm using EntityType field for properties. However, instead of displaying them in one, long list, I would like to divide them in columns, with Categories as headers.
Regular way of displaying EntityType field:
What I would like to get:
How do I do that? - Without using dirty hacks in entities or views.
So the only way I found it working was:
In Repository class (pulling list of all properties with child properties and categories):
$this->getEntityManager()
->createQueryBuilder()
->select('t, category, children')
->join('t.category', 'category')
->leftJoin('t.children', 'children')
->where('t.parent IS NULL')
->orderBy('category.sortOrder', 'ASC')
->addOrderBy('t.sortOrder', 'ASC')
->addOrderBy('t.name', 'ASC');
$entities = $query->getResult();
$options = [];
/** #var Property $entity */
foreach ($entities as $entity) {
$options[$entity->getCategory()->getName()][] = $entity;
}
In Entity class (pulling the list of IDs of selected properties, to preselect checkboxes in the view file):
public function getPropertyIds() {
$properties = $this->getProperties();
$propertyIds = [];
foreach ($properties as $property) {
$propertyIds[] = $property->getId();
}
return $propertyIds;
}
Edition form class, so the data can be validated:
$builder
->add(
'properties',
EntityType::class,
[
'label' => 'Properties',
'class' => Property::class,
'choice_label' => 'name',
'placeholder' => '',
'expanded' => true,
'multiple' => true,
'required' => false,
]
);
And finally, the view file:
{% for categoryName, items in properties %}
<h2>{{ categoryName }}</h2>
<ul>
{% for property in items %}
<li>
<input type="checkbox"
name="{{ form.properties.vars.full_name }}[]"
value="{{ property.id }}"
id="{{ form.properties.vars.id }}_{{ property.id }}">
<label for="{{ form.properties.vars.id }}_{{ property.id }}">
{{ property.name }}
</label>
</li>
{% endfor %}
</ul>
{% endfor %}
{% do form.properties.setRendered %}
(I omitted the "checked" and "children" part in the view)
However this solution is not ideal in my point of view. I would rather to get rid of manually generating <input...> in the view - I would rather want to use some helper functions.
Anyway, this is some kind of low-level solution to my problem. Hope that helps.

Nested collections in forms

I tried to follow this answer to handle nested collections in forms.
I have an Application's Form with a collection of LienAppliServ's Form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('servLiens', 'collection', array(
'label' => ' ',
'type' => new LienAppliServType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' =>false,
'prototype' => true,
))
//...
In my LienAppliServ's Form, I have another collection of PortLienAppliServ's Form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ports', 'collection', array(
'type' => new PortLienAppliServType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' =>false
))
//...
And the form of PortLienAppliServ is :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('numPort')
->add('type')
;
}
Now, I'd like to handle add/delete for each collection...
As I said, I tried to follow this answer
In order to do that, I tried :
{% block body -%}
{{ form_start(form) }}
<ul id="col-servliens" data-prototype="{{ form_widget(form.servLiens.vars.prototype)|e }}">
{# iterate over each existing tag and render its only field: name #}
{% for servLiens in form.servLiens %}
<li>{{ form_row(servLiens) }} </li>
<ul id="col-ports" data-prototype="{{ form_widget(ports.vars.prototype)|e }}">
{%for ports in servLiens.ports %}
<li>{{ form_row(ports) }}</li>
{% endfor %}
{% endfor %}
</ul>
{{ form_end(form) }}
{% endblock %}
{% block app_js %}
//Same as the other question
<script>
function FormCollection(div_id)
{
// keep reference to self in all child functions
var self=this;
self.construct = function () {
// set some shortcuts
self.div = $('#'+div_id);
self.div.data('index', self.div.find(':input').length);
// add delete link to existing children
self.div.children().each(function() {
self.addDeleteLink($(this));
});
// add click event to the Add new button
self.div.next().on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
self.addNew();
});
};
/**
* onClick event handler -- adds a new input
*/
self.addNew = function () {
// Get the data-prototype explained earlier
var prototype = self.div.data('prototype');
// get the new index
var index = self.div.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
self.div.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
self.div.append($(newForm));
// add a delete link to the new form
self.addDeleteLink( $(self.div.children(':last-child')[0]) );
// not a very nice intergration.. but when creating stuff that has help icons,
// the popovers will not automatically be instantiated
//initHelpPopovers();
return $(newForm);
};
/**
* add Delete icon after input
* #param Element row
*/
self.addDeleteLink = function (row) {
var $removeFormA = $('<i class="entypo-trash"></i>');
$(row).find('select').after($removeFormA);
row.append($removeFormA);
$removeFormA.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// remove the li for the tag form
row.remove();
});
};
self.construct();
}
</script>
<script>
$(document).ready(function() {
new FormCollection('col-servliens');
new FormCollection('col-ports');
});
</script>
And I get
Variable "ports" does not exist.
I really need some help.. Collections are actually my nightmare...
Thanks !
Let me start with apologies, because my answer in the post you mention was clearly wrong :) At least the example code was incorrect. I wonder if the original poster ever found the right solution.
The error is easy enough to spot. In the Twig you use the variable ports, but how should Twig know where it comes from?
{% for servLiens in form.servLiens %}
<li>{{ form_row(servLiens) }} </li>
<ul id="col-ports" data-prototype="{{ form_widget(ports.vars.prototype)|e }}">
{%for ports in servLiens.ports %}
<li>{{ form_row(ports) }}</li>
{% endfor %}
{% endfor %}
Using {{ form_row(servLiens) }} will actually already create the collection classes, so the whole <ul> inside it is not needed. In fact Symfony is smart enough that you don't even need the first <ul>. It will find any child forms (ex. collections) and create all the HTML needed.
So the only thing you need to have in your code is:
{{ form_row(form.servLiens) }}
If you would look at the HTML generated from that, you will see the data-prototype and the children. It will automatically have an id attribute that you can use.
Problem is the javascript part. You are using a collection within a collection. The javascript FormCollection class (which I wrote) cannot handle this as simply as you've tried it. It will handle the top collection fine, but it will not automatically instantiate collections on load/create/delete. Remember that each servLiens collection has a collection of ports. Therefore already at loading you can't simply load the col-ports as you do now, but would need to load each port collection. Then you'd need to instantiate a new FormCollection on the port collection every time you add a servLiens row.
I don't have time to create your code for you, but I hope my explanation helps you to find a solution.
Looks like you use symfony 2.6 or older.
I would start by saying you should'nt do "'type' => new PortLienAppliServType()" but pass the form type name. Symfony will instantiate it and it could be the problem.
Everything is described in extend here: https://symfony.com/doc/2.6/reference/forms/types/collection.html#basic-usage
Honestly, Collections are not that difficult you just need to understand how it works then Symfony will manage everything for you.

Symfony 2 Catchable Fatal Error: Object of class UserCategory could not be converted to string

I try to put in place a system of categories . I love that my users can tag with categories . So I put a Array Collection and a " ManyToMany " but it does not work and whatever I do I get this error " Catchable Fatal Error : Object of class Shootngo \ CoreBundle \ Entity \ UserCategory couldn't be converted to string".
If someone could help me, i think i will not have hairs if i continue to search a solution...
$userCategories = new ArrayCollection();
foreach ($form->getCategories()->getLibCategory() as $category) {
$userCategories->add($category);
}
$user->addCategory($userCategories);
My FormType :
->add('category', CollectionType::class, array(
'entry_type' => UserCategoryType::class,
'allow_add' => true,
))
My View :
<div class="form-group">
<label class="col-sm-4 control-label">Catégories<span class="text-danger">*</span></label>
<div class="col-sm-6">
{{ form_errors(form.category) }}
<ul id="category-list" data-prototype="{{ form_widget(form.category.vars.prototype)|e }}">
{% for cat in form.category %}
{{ form_errors(cat) }}
<div class="form-group">
{{ form_widget(cat,{'attr' : {'class' : 'form-control', 'placeholder' : "", 'data-parsley-required' : 'data-parsley-required'}}) }}
</div>
{% endfor %}
</ul>
Add another category
</div>
thank in advance !
Christophe
you need to add a method to
Shootngo\CoreBundle\Entity\UserCategory
entity class...
public function __toString()
{
return 'My string version of UserCategory'; // if you have a name property you can do $this->getName();
}
that way when the select options are being generated php automatically uses the __toString() method to convert the Entity Object into text...
That error always throws up when entity do not has __toString() method. Symfony component Form often use this method to generate values for <select>.
I always had this error when I used EntityTypeField and forgot to defined attribute choice_label in options. Maybe in your CategoryUserType you use that form type and forget to defined choice label.
Well i have found solution with your two answer thanks :)
FormType :
->add('category', 'entity', array(
'class' => "CoreBundle:Category",
'property' => "libCategory",
'multiple' => true,
'expanded' => true
))
Object User :
/**
* #ORM\ManyToMany(targetEntity="Shootngo\CoreBundle\Entity\Category", cascade={"persist"})
* #ORM\JoinTable(name="sng_member_categories")
*/
private $category;
With the ArrayCollection() on attribute $category defined in the constructor.
And that's works :D

Symfony2 - Entity Collection Choice Restriction

I have the following setup:
Entity: Customer
Entity: Account
Entity: Message
Now imagine the following problem:
The account 'Mark' is in charge of two customers, 'Ben' and 'Lili'.
The account 'Tim' is in charge of two other customers, 'Tom' and 'Ronny'.
The account 'Ben' now wants to send a message to his customers. In a form he can choose the customers he would like to send the message to. Those will be saved as an ArrayCollection in the Message entity (in relation with entity Customer).
However, later on account 'Tim' can view this message and also send it to his customers the same way - by adding his customers to the list of recepients.
Problem is: When 'Tim' adds his recepients, he should not see the recepients of 'Ben' as this is none of his concern.
Visual explanation: http://jsfiddle.net/q0nn62o5/
My solution so far:
I created a custom FormType called 'AccountCustomerType'. This FormType is an entity which includes the customers of one particular account as choices:
$builder
->add('customer', 'entity', array(
'class' => 'AppBundle:Customer',
'choices' => $this->customers,
));
This FormType is used in the main form as a collection:
$form->add('recepients', 'collection', array(
'type' => new AccountCustomerType($customers),
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'by_reference' => false,
));
Printing form...:
<div class="recepients" data-prototype="{{ form_widget(form.recepients.vars.prototype)|e }}">
{% for customer in form.recepients %}
<div>
{{ form_widget(customer) }}
</div>
{% endfor %}
</div>
One problem left:
I can now choose from the customers that one account is in charge of. However, the recepients I am not in charge of are still shown as blank select fields. How can I hide these? I don't want to duplicate messages to seperate recepients as there are a couple more features connected to this.
You can filter a collection in a form by restricting the query results.
E.g. something like:
$accountData = $this->getEntityManager()
->createQueryBuilder()->select('a, c')
->from('YourAccountBundle:Account', 'a')
->join('a.customers', 'c') // assuming there is a relationship like this
->where('a = :yourAccountManager')
->setParameter('yourAccountManager', $accountEntity)
->getQuery()->getResult();
Then use $accountData in your parent form.
This will restrict the Customer entities shown in the form to only the ones linked to $accountEntity.
Note this needs to be the first fetch of this relation in your page load, if you lazy load it with doctrine then it'll return all Customer entities regardless of filtering.

Sonata Admin: Add custom triggers/actions to list/edit action

I'm using SonataAdminBundle for managing entities in my application. The admins of the site can add videos, and some of them first need to be approved by their speakers. There is an authorization system working already - I have working code which will generate a special link and notify the speaker, who can approve or disapprove the video, and notify back the admins automatically.
I'd like to customize my admin section, so there will be a button ask for authorization next to the videos. I'm okay having it either in the list action ( /admin/acme/videos/list ) or in the edit action somewhere in the right-nav ( /admin/acme/videos/x/edit/ )
What's the best approach to do this? The documentation says very little about blocks customization, but I found this example which may be the thing I'm looking for, but I couldn't figure out how to use it.
One option is to use the preUpdate hook, and add a checkbox to the edit action, but a button would be much nicer.
To add an action for edit form
Add to your admin class:
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('My action', array('uri' => 'http://google.com?id=' . $id));
}
It will create left side menu for actions like /admin/acme/videos/x/edit/. Having id for current item allows you to build any custom URL.
To add an action for list:
In your admin file add
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('_action', 'actions', array(
'actions' => array(
'act' => array('template' => 'AcmeBundle:Video:my_temp.html.twig'),
)
))
;
}
It will add a column with links, then you need to create a template for your column, something like
<a href="{{ admin.generateObjectUrl('delete', object) }}" class="delete_link" title="{% trans from 'SonataAdminBundle' %}action_delete{% endtrans %}">
<img src="{{ asset('bundles/sonataadmin/famfamfam/delete.png') }}" alt="{% trans from 'SonataAdminBundle' %}action_delete{% endtrans %}" />
</a>
All examples are taken from link that you provided. Hope it helps

Categories