symfony2 apply transformer to entity form field - empty array? - symfony

I am trying to get a dataTransformer to work on an entity field in symfony 2.
context:
form displays sails that user can select (checkboxes)
this is the first step in a multi-step sail ordering process (later steps display options available for each sail, colors, etc)
This is my form type class:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Co\QuoteBundle\Form\DataTransformer\SailCollectionToStringsTransformer;
class PartsTypeStep1 extends AbstractType {
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Co\QuoteBundle\Entity\Parts',));
$resolver->setRequired(array('sailsAvailable', 'em'));
}
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$transformer = new SailCollectionToStringsTransformer($options['em']);
$formBuilder->add(
$formBuilder->create('mainsailparts', 'entity', array(
'class' => 'CoQuoteBundle:Mainsail',
'choices' => $options['sailsAvailable']['mains'],
'multiple' => true,
'expanded' => true,
'label' => 'Mainsails',))
->addModelTransformer($transformer)); //line 58
}
public function getName() {
return 'partsStep1';
}
}
The above works with no errors, but does not display the transformed data. The view is:
__ Race main
__ Cruising main
(__ stands for checkbox)
However, the view I want is:
__ Race main ($1400)
__ Cruising main ($800)
The transformer I have is:
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Co\QuoteBundle\Entity\Sail;
use Doctrine\Common\Collections\ArrayCollection;
class SailCollectionToStringsTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforms a collection of sails to a collection of strings.
* #param ISail|null $sail
* #return string
*/
public function transform($sailCollection)
{
if (null === $sailCollection) {
return null;
}
$labels = new ArrayCollection();
foreach($sailCollection as $sail){
$labels[] = $sail->getName().' ($'.$sail->getBuildPrice().')';
}
return $labels;
}
//reverse transformer... not the issue (yet) because the forward transformer doesn't work
}
When running this through the netbeans debugger, an empty array is passed to the transformer. However, if I change line 58 to ->addViewTransformer($transformer)); and debug, it correctly passes two booleans with the sail id's as the array keys to the transformer. Unfortunately, I can't use the ViewTransformer because that no longer contains the original strings to change.
Why does the ArrayCollection that should contain the main sails get passed to the transformer as an empty ArrayCollection? The function returns an empty $labels collection.
I'm not sure what I am doing wrong... Help is much appreciated!!!!
Thanks.

I never did find a way how to implement what I was attempting to do. However, the workaround I used is described below.
for the form type class, I used a form event (symfony2 book), and I saved the boatId (that the sails correspond to) in the parts object in the controller, like so:
$partsObject = new Parts($boat->getId());
$form = $this->createForm(new PartsTypeStep1(), $partsObject, array(
'em' => $this->getDoctrine()->getManager()));
The form type class now looks like this:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Doctrine\ORM\EntityRepository;
class PartsTypeStep1 extends AbstractType {
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Co\QuoteBundle\Entity\Parts',));
$resolver->setRequired(array('em'));
}
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$factory = $formBuilder->getFormFactory();
$em = $options['em'];
$formBuilder->addEventListener(
FormEvents::PRE_SET_DATA,
function(FormEvent $event) use($factory, $em){
$form = $event->getForm();
$data = $event->getData();
if (!$data || !$data->getDateTime()) {
return;
}
else {
$boatClass = $data->getBoatId();
$formOptions = array(
'class' => 'CoQuoteBundle:Mainsail',
'multiple' => true,
'expanded' => true,
'property' => 'displayString',
'label' => 'Mainsails',
'query_builder' => function(EntityRepository $er) use ($boatClass) {
return $er->createQueryBuilder('m')
->where('m.boatType = :boatClass')
->setParameter('boatClass', $boatClass);
},
);
$form->add($factory->createNamed('mainsailparts', 'entity', null, $formOptions));
}
}
);
}
public function getName() {
return 'partsStep1';
}
I also needed to add the displayString property in the Mainsail class (I only added a getter, not an actual variable for the string). So the Mainsail class now has this:
public function getDisplayString(){
return $this->name . ' - ' . $this->descr . ' ($' . $this->buildPrice . ')';
}
The only issue I ran into with this workaround is what happens if the query returns an empty result, because twig will automatically render the form label ('Mainsails') whether or not it has any checkboxes to render. I got around that issue like this:
{% if form.mainsailparts|length > 0 %}
<div class="groupHeading">{{ form_label(form.mainsailparts) }}</div>
{% for child in form.mainsailparts %}
{# render each checkbox .... #}
{% endfor %}
{% else %}
{% do form.mainsailparts.setRendered %}
{% endif %}
I don't know if this is the recommended solution in this case, but it does work with form validation (at least disallowing progression if no sails are selected, I don't need anything more rigorous).
I'm not going to mark this as the answer since it doesn't answer the question (how to apply transformer to entity field), but it is a workaround for anyone dealing with the same problem.

Related

How to customize form field based on user roles in Symfony2/3?

Is there a correct way to customize a form depending on the role of the user that requests it?
My scenario is pretty simple: I need to hide some fields if the user has not the ROLE_ADMIN granted. I tried to avoid the field display on Twig, but
{% if is_granted('ROLE_ADMIN') %}
{{form_row(form.field)}}
{% endif %}
not works, because the form builder bypass this check.
Symfony version: 2.8.2
EDIT
Thanks to the #Rooneyl suggestion I've found the solution:
At first, you need to add the 'role' key to the options parameter. So, in the configureOptions() $options['role'] is always ROLE_USER.
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Ticket',
'role' => 'ROLE_USER'
));
}
Then in the controller you have to pass the getRoles() array:
$user_roles = $this->getUser()->getRoles();
$form = $this->createForm('MyBundle\Form\TicketType', $ticket, array('role' => $user_roles));
You can do it in your form.
Make a service for your form
app.form.type.task:
class: AppBundle\Form\FormType
arguments: ["#security.authorization_checker"]
tags:
- { name: form.type }
In your FormType, add a constructor to get your service.
private $authorization;
public function __construct(AuthorizationChecker $authorizationChecker)
{
$this->authorization = $authorizationChecker;
}
Then, in your builder, you will be able to check user permission
$builder->add('foo');
if($this->authorization->isGranted('ROLE_ADMIN'))
{
$builder->add('bar');
}
And then, finally, you can render your form
{% if form.formbar is defined %}
{{ form_row(form.formbar ) }}
{% endif %}
Please note that it mean that your field may be null. Because maybe you want to see some of them visible by some users and others not.
Else, you can set a default value in your entity construct method, to make sure value won't be null if user don't/can't fill it.
You could use an option passed to the form builder to say what elements are generated.
This way you can change the content and validation that gets done (using validation_groups).
For example, your controller (assuming roles is an array);
you controller;
$form = $this->createForm(new MyType(), $user, ['role' => $this->getUser()->getRoles()]);
And your form:
<?php
namespace AppBundle\Form\Entity;
use AppBundle\Entity\UserRepository;
use Symfony\Component\Form\AbstractType,
Symfony\Component\Form\FormBuilderInterface,
Symfony\Component\OptionsResolver\OptionsResolver;
class MyType extends AbstractType
{
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User',
'validation_groups' => ['create'],
'role' => ['ROLE_USER']
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// dump($options['roles']);
if (in_array('ROLE_ADMIN', $options['role'])) {
// do as you want if admin
$builder
->add('name', 'text');
} else {
$builder
->add('supername', 'text');
}
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_my_form';
}
}

Symfony2 Conditional Validation

I have a FormType method.
<?php
class PostType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('text')
->add('paymentMethod', 'choice', array(
'required' => true,
'choices' => array(
'cach' => 'Cach',
'check'=> 'Check'
)
))
->add('checkId', 'integer', array(
'required' => true
))
->add('submit', 'submit');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Post',
]);
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_post';
}
}
?>
The field checkId is displaying only when the choice 'check' is selected with the field paymentMethod.
Here is the javascript:
PaymentMethod = {
hideCheckId: function(){
$('.check-id').hide();
},
showCheckId: function(){
$('.check-id').fadeIn();
},
whenPaymentMethodChange: function(){
$('#appbundle_post_paymentMethod').on('change', function(){
var method = this.value ;
if(method == 'check'){
PaymentMethod.showCheckId();
}else{
PaymentMethod.hideCheckId();
}
})
}
};
PaymentMethod.hideCheckId();
PaymentMethod.whenPaymentMethodChange();
I would like the field 'checkId' to be required to TRUE only when the option check is selected.
How can I do that?
Ok thanks, Here is the solution for everyone:
Add a callback method function in your entity class.
/**
* #param ExecutionContextInterface $context
* #Assert\Callback()
*/
public function isPaymentIsCheck(ExecutionContextInterface $context)
{
if ($this->getPaymentMethod() == 'check' and $this->getCheckId() == '') {
$context->buildViolation('A check ID as to be defined')
->atPath('checkID')
->addViolation();
}
}
Don't forget to add the Assert and ExecutionContextInterface component in you entity class:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
And finally don't forget to display errors in the twig template:
{% if form.vars.errors|length %}
<div class="alert alert-danger">{{ form_errors(form) }}</div>
{% endif %}
Hope it will help you.
More info here: http://symfony.com/doc/current/reference/constraints/Callback.html
What KevinR wrote would definitely solve your problem, but in my opinion there's even cleaner solution:
http://symfony.com/doc/current/book/validation.html#group-sequence-providers
Imagine a User entity which can be a normal
user or a premium user. When it's a premium user, some extra
constraints should be added to the user entity (e.g. the credit card
details). To dynamically determine which groups should be activated,
you can create a Group Sequence Provider. First, create the entity and
a new constraint group called Premium:
This is exactly your how you would solve this kinda of problem
public function getGroupSequence()
{
$groups = array('Post'); //Or array('Default') whichever you prefer
if ($this->getPaymentMethod() === 'Check') {
$groups[] = 'Check';
}
return $groups;
}
Of course you'll have to add validation group paymentMethod property in your entity mapping. Here's annotation example, for more examples check above link.
/**
* #Assert\IsTrue(groups={"Check"})
*/
private $checkId;

Allow a OneToOne relationship to be optional in Symfony2

I have a form responsible of creating and updating users. A user can (or not) have an address (OneToOne unidirectional relation from user).
When I create a user, no problem.
When I update a user, usually no problem.
Problems come up when i update a user which already has an address and try to unset all the address fields. There is then a validation error.
The wanted behavior would be to have the user->address relation set to null (and delete the previously set address on the DB).
There is a cascade_validation, the addess field in form (nested form) is set to not be required and the user entity allow the address to be null.
UPDATE
Relevant entities and forms :
User entity (Getters & Setters are classical, Symfony generated):
class User
{
[...]
/**
* #var \Address
*
* #ORM\OneToOne(targetEntity="Address", cascade="persist")
* #ORM\JoinColumn(
* name="address_id", referencedColumnName="id"
* )
*/
private $address;
[...]
}
The address entity is classical, no bidirectionnal relation to user.
User form
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
[...]
->add('address', new AddressType(), array('required' => false))
[...]
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\User',
'cascade_validation' => true
));
}
public function getName()
{
return 'user';
}
}
The address nested form is classical
As you can see, the is a quite classical and straightforward code. The only particular case is that address is optional. Leading to an validation error only in the case that the address was previously set (and, thus, exist in the DB and as a not null relation with the user) and the user want to unset it (all address fields are left empty).
It seems that if the related address has not an actual instance it can still be optional. But, if an instance of the address exist and is linked with the user, it can not be optional anymore.
UPDATE 2
namespace Xentia\FuneoBundle\Form\Type;
use Doctrine\Common\Util\Debug;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add("suggestLocality", null, array(
'mapped' => false,
'label' => 'Locality'
))
->add("suggestStreet", null, array(
'mapped' => false,
'label' => 'Street'
))
->add('street')
->add('locality')
->add('postalCode')
->add('country', null, array(
'label' => false,
))
->add('latitude', 'hidden')
->add('longitude', 'hidden');
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function(FormEvent $event) {
$address = $event->getData();
if (!empty($address)) {
$addressLocality = $address['locality'];
if (empty($addressLocality)) {
$event->setData(null);
}
}
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\Address',
'validation_groups' => array('Default'),
));
}
public function getName()
{
return 'address';
}
}
Try setting orphanRemoval on your relation
/** #OneToOne(targetEntity="...", orphanRemoval=true) */
$address
EDIT
I see now, you have placed the wrong listener. First of all it should be POST_SUBMIT, PRE_SUBMIT is to process request data and modify form. On POST SUBMIT you can modify the object.

Good practices using forms

I have to change a web that is using different types to generate forms. There is in Bundle/Form/ folder 2 files:
ProductType.php
ProductEditType.php
It's working fine, the first one is used to generate the new product form and the second one the form to edit it.
Almost 95% of both files is the same, so I guess it has to exist any way to use one type to generate more than one form.
I have been reading about how to modify forms using form events, but I have not found clearly what is the general good practice about it.
Thanks a lot.
Update
I wrote an Event Subscriber as follows.
<?php
namespace Project\MyBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Description of ProductTypeOptionsSubscriber
*
* #author Javi
*/
class ProductTypeOptionsSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents() {
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if( !$data || !$data->getId() ){
// No ID, it's a new product
//.... some code for other options .....
$form->add('submit','submit',
array(
'label' => 'New Produtc',
'attr' => array('class' => 'btn btn-primary')
));
}else{
// ID exists, generating edit options .....
$form->add('submit','submit',
array(
'label' => 'Update Product',
'attr' => array('class' => 'btn btn-primary')
));
}
}
}
In ProductType, inside buildForm:
$builder->addEventSubscriber(new ProductTypeOptionsSubscriber());
So that's all, it was very easy to write and it works fine.
You can read this cookbook event subscriber, the first scenario can do for you.
Returning to the example of the documentation..
Add the fields that you want them to being modified in this way:
$builder->addEventSubscriber(new AddNameFieldSubscriber());
Then create the event event subscriber by entering your logic:
// src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace Acme\DemoBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AddNameFieldSubscriber implements EventSubscriberInterface
{
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_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
// check if the product object is "new"
// If you didn't pass any data to the form, the data is "null".
// This should be considered a new "Product"
if (!$data || !$data->getId()) {
$form->add('name', 'text');
....
..... // other fields
}
}
}

Symfony2 Forms - How to use parametrized constructors in form builders

I am learning to use Symfony2 and in the documentation I have read, all entities being used with Symfony forms have empty constructors, or none at all. (examples)
http://symfony.com/doc/current/book/index.html Chapter 12
http://symfony.com/doc/current/cookbook/doctrine/registration_form.html
I have parametrized constructors in order to require certain information at time of creation. It seems that Symfony's approach is to leave that enforcement to the validation process, essentially relying on metadata assertions and database constraints to ensure that the object is properly initialized, forgoing constructor constraints to ensure state.
Consider:
Class Employee {
private $id;
private $first;
private $last;
public function __construct($first, $last)
{ .... }
}
...
class DefaultController extends Controller
{
public function newAction(Request $request)
{
$employee = new Employee(); // Obviously not going to work, KABOOM!
$form = $this->createFormBuilder($employee)
->add('last', 'text')
->add('first', 'text')
->add('save', 'submit')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
Should I not be using constructor arguments to do this?
Thanks
EDIT : Answered Below
Found a solution:
Looking into the API for the Controllers "createForm()" method I found something that is not obvious from the examples. It seems that the second argument is not necessarily an object:
**Parameters**
string|FormTypeInterface $type The built type of the form
mixed $data The initial data for the form
array $options Options for the form
So rather than pass in an instance of the Entity, you can simply pass in an Array with the appropriate field values:
$data = array(
'first' => 'John',
'last' => 'Doe',
);
$form = $this->createFormBuilder($data)
->add('first','text')
->add('last', 'text')
->getForm();
Another option (which may be better), is to create an empty data set as a default option in your Form Class.
Explanations here and here
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first');
$builder->add('last');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => new Employee('John', 'Doe'),
));
}
//......
}
class EmployeeFormController extends Controller
{
public function newAction(Request $request)
{
$form = $this->createForm(new EmployeeType());
}
//.........
}
Hope this saves others the head scratching.

Resources