Symfony form & entity & choice constraint confusion - php

I've got Doctrine entity with some choice field, let's say that it looks like this:
/**
* #var string
* #ORM\Column(name="color", type="string", nullable=false)
* #Assert\Choice(choices = {"red", "green", "blue"}, message="Choose valid color")
*/
protected $color;
And now I've got a form that's associated with my entity, let's say that specific field looks like that inside of it:
$builder->add(
'color',
'choice',
array(
'choices' => array(
'red' => 'Red like roses',
'green' => 'Green like grass',
'blue' => 'Blue like sky'
),
'expanded' => true
)
);
As far as I'm now, it's quite clear that possible values are duplicated inside constraint and inside the form. But let's go further, I'd like to display my entity inside of a template, so I must do something like that:
{% if entity.color == 'red' %}
Red like roses
{% elseif entity.color == 'green' %}
Green like grass
{% elseif entity.color == 'blue' %}
Blue like sky
{% endif %}
So we've now got a third place where not only values, but also labels are duplicated. I've thought of something like a service that serves as a twig extension and can be injected into the form builder but it doesn't solve duplication with constraint. Now I've got no idea how to solve it, I'd really like to have just one single place where I define things like that and most of all I'd like to keep them inside of entity as annotations but I don't know how to proceed with this.
Any ideas?

Instead of hardcoding the choices in your mapping, you should provide them with a callable. You could use my enum package for that.

Related

Displaying an array | Twig

I'm having trouble returning an array of options from an SQL database. I've tried a few different variations of returning the data, but I can only seem to get single data points returning like radio buttons, input text, etc.
I have 3 checkboxes in a form: Blue car, Red car, Green car.
They all save to the database. However, a "1" is been added. eg. [Blue car, Red car, 1] is being stored.
When I try and return the data to the twig template, it doesn't load anything.
I'm new to twig and Symfony, and I'm partly learning through existing code which makes it hard for me to troubleshoot sometimes.
I'm looking for the output to be like this: Blue car (or) Blue car & Red car (or) Blue car, Red car & Green car. Happy for the code to be simplified for this output.
var
/**
* #var string
*
* #ORM\Column(name="sel_cars", type="string", length=255, nullable=false)
*/
private $sel_cars;
/**
* Get cars
*
* #return string
*/
public function getSelcars()
{
return $this->sel_cars;
}
/**
* Set cars
*
* #param string $sel_cars
*
* #return Listing
*/
public function setSelcars($sel_cars)
{
$this->sel_cars = $sel_cars;
return $this;
}
output
{% if item.sel_cars is not empty %}
{% set sel_cars = item.sel_cars|split(',') -%}
{% set sel_carsarray = { 'bluecar': 'Blue car', 'redcar': 'Red car', 'greencar': 'Green car'} %}
{% for row in sel_cars %}
{{ sel_carsarray[row] }}
{% endfor %}
{% endif -%}
Appreciate the help :)
I seemed to have figured it out.
changing all the above output code from
item.sel_cars
to
item.Selcars
It now returns the data.

Twig - Sandbox security policy won't work

I have been trying to get this to work for a while now and can't find much docs on it. Or any use cases where the sandbox policy has been used outside of the Symfony framework.
I'm using Twig as a stand-alone package, so can't use any Symfony pseudo-code.
I have strict mode enabled so the sandbox affects all templates. Most templates render fine except this one which makes a call to a class. However I don't know how to allow it through.
Class:
class GetThings {
public function doStuff() {
return array(
'id' => '...',
'data' => '...'
);
}
}
...
Twig:
$allowedTags = ['if', 'else', 'elseif', 'endif', 'for', 'endfor'];
$allowedFilters = ['upper', 'escape'];
$allowedMethods = [
'GetThings' => array('doStuff') // Possibly this may be wrong?
];
$allowedProperties = [
'GetThings' => array('id', 'data') // Or this is wrong? But not sure the correct way.
];
$allowedFunctions = ['range'];
$policy = new Twig_Sandbox_SecurityPolicy($allowedTags, $allowedFilters, $allowedMethods, $allowedProperties, $allowedFunctions);
$sandbox = new Twig_Extension_Sandbox($policy, true);
...
Template:
{% for i in info %}
{{ i.id }} <- Code that raises securityPolicy exception.
{{ i.data }} <- Code that raises securityPolicy exception.
{% endfor %}
I believe it may be related to the allowed methods or properties, but I wasn't able to find any working examples of these in use. I've tried the full namespaces too, nothing.
EDIT:
So I looked into this error a bit deeper and found the exception stack-trace, for some reason it thinks my class is StdClass rather than GetThings? Not sure why. Any ideas?
Twig_Sandbox_SecurityNotAllowedPropertyError: Calling "id" property on a "stdClass" object is not allowed.
To instantiate the class I simply do the following:
public function index() {
$data = new GetThings();
// echo get_class($data); // returns GetThings as expected...
return $twig->render('index.twig', [
'info' => $data->doStuff()
]);
}
If I do 'StdClass' => array('id', 'data') for the allowed properties, the page works fine. But I feel this is not working as intended, as StdClass could be anything? And GetThings should work, no?
EDIT:
I think I figured it out. So my allowed properties allows 'GetThings' => [id, data] which is fine. doStuff() returns a \PDO array of objects, using the \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ option which causes PDO to convert all returned values into StdClass objects.
Is there any way around this? I want to keep that option, but still want to reference the policy as 'GetThings' => [...] rather than 'StdClass' => [...]
It works as intended:
{% for i in info %}
{{ i.id }} <- Code that raises securityPolicy exception.
{{ i.data }} <- Code that raises securityPolicy exception.
{% endfor %}
Here i is not a GetThings instance. It's whatever you instantiated as value for the id and data key:
return array(
'id' => '...',
'data' => '...'
);
Twig for tag will iterate over the info variable, which happens to be a keyed array. So the loop will iterate over the values of the array - in your case '...' and '...' which I guess are stdClass instances.

Symfony - manyToMany not getting saved

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>

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.

can't access property of stdClass in TWIG

I have googled around and it seems as though the creators of TWIG really insists that what I am doing here, which is to me a pure job for the VIEW, something that the template shouldn't take care of at all?!
I know I can't iterate over an object of stdClass without some custom TWIg filters, so I hacked that for now, but if I can't eve access properties dynamically this TWIG thing really isn't very useful at all.
$fixedModuleNames = array('time', 'date', 'weather'); //since TWIG doesn't iterate over objects by default, this is my solution, don't feel like adding a bunch of twigfilters just for this.
$fixedModules = json_decode($entity->getFixedModules());
/*
Here's what fixedModules look like (although here not JSON but array, before encoded to json, I like to create my JSONs this way in PHP)
$fixedModules["time"] = array(
'show' => true,
'left' => 10,
'top' => 10,
'width' => 100,
'height' => 200,
'fontColor' => '#000000',
'fontSize' => 40,
'fontFamily' => 'Arial',
'borderColor' => '',
'borderRounding'=> 0,
'bgColor' => ''
);
*/
Here's what I am trying to do...
{% for item in fixedModuleNames %}
<TR>
<TD><input type="number" id="left_{{ item }}" value="{{ fixedModules[item].left }}" class="LayoutModuleEditField" /></TD>
So this line fails
{{ fixedModules[item].left }}
There must be a way around this since what I am doing is very routine?
Ah, is this perhaps the preferred way of doing it?
{{ attribute(fixedModules, item).left }}
If your attribute function works then use it.
Consider however fixedModules[item].left. You are asking twig to figure out that item is a variable while left is a constant. Difficult for any system to do to say the least.
I would use something like:
{% for moduleName, module in fixedModules %} {# Time, Date, Weather module #}
{% for itemName,itemValue in module %} {# Process each attribute in the module #}
...
If you want to iterate over an object then just implement the array iterator interface. Usually pretty simple.
item is not the key, but an element of your array. So you can access your attributes this way:
{% for item in fixedModuleNames %}
left = {{ item.left }}
{% enfor %}
If you really want to use the key instead, do something like:
{% for key, item in fixedModuleNames %}
left = {{ fixedModuleNames[key].left }}
{% enfor %}
Hope this helps.

Categories