Good practices using forms - symfony

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
}
}
}

Related

easyadmin entity field's dynamic custom choices

Installed easyadminbundle with symfony 4, configured for an entity name Delivery and it has a field associated to another entity name WeeklyMenu:
easy_amin.yaml:
Delivery:
...
form:
fields:
- { property: 'delivered'}
- { property: 'weeklyMenu', type: 'choice', type_options: { choices: null }}
I need a dynamically filtered results of weeklyMenu entity here, so I can get a list of the next days menus and so on. It's set to null now but have to get a filtered result here.
I've read about overriding the AdminController which I stucked with it. I believe that I have to override easyadmin's query builder that listing an associated entity's result.
i've figured out, here is the solution if someone looking for:
namespace App\Controller;
use Doctrine\ORM\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilder;
class AdminController extends EasyAdminController {
public function createDeliveryEntityFormBuilder($entity, $view) {
$formBuilder = parent::createEntityFormBuilder($entity, $view);
$fields = $formBuilder->all();
/**
* #var $fieldId string
* #var $field FormBuilder
*/
foreach ($fields as $fieldId => $field) {
if ($fieldId == 'weeklyMenu') {
$options = [
'attr' => ['size' => 1,],
'required' => true,
'multiple' => false,
'expanded' => false,
'class' => 'App\Entity\WeeklyMenu',
];
$options['query_builder'] = function (EntityRepository $er) {
$qb = $er->createQueryBuilder('e');
return $qb->where($qb->expr()->gt('e.date', ':today'))
->setParameter('today', new \DateTime("today"))
->andWhere($qb->expr()->eq('e.delivery', ':true'))
->setParameter('true', 1)
->orderBy('e.date', 'DESC');
};
$formBuilder->add($fieldId, EntityType::class, $options);
}
}
return $formBuilder;
}
}
so the easyAdmin check if a formbuilder exists with the entity's name i.e. create<ENTITYNAME>FormBuilder(); and you can override here with your own logic.
Another approach to this would be to create new FormTypeConfigurator and overwrite choices and/or labels. And tag it as:
App\Form\Type\Configurator\UserTypeConfigurator:
tags: ['easyadmin.form.type.configurator']
and the configurator looks like this:
<?php
declare(strict_types = 1);
namespace App\Form\Type\Configurator;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\TypeConfiguratorInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormConfigInterface;
final class UserTypeConfigurator implements TypeConfiguratorInterface
{
/**
* {#inheritdoc}
*/
public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig)
{
if ($parentConfig->getData() instanceof User) {
$options['choices'] = User::getUserStatusAvailableChoices();
}
return $options;
}
/**
* {#inheritdoc}
*/
public function supports($type, array $options, array $metadata)
{
return in_array($type, ['choice', ChoiceType::class], true);
}
}

FOSRestBundle How to validate the registration?

I'm developing a RESTFul API in Symfony 2.3.* with FOSUserBundle and FOSRestBundle, and I'm having trouble understanding how to create a registration method.
My controller look like this :
class UserRestController extends FOSRestController
{
//Other Methods ...
public function postUserAction()
{
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->createUser();
$param = $paramFetcher->all();
$form = $this->createForm(new UserType(), $user);
$form->bind($param);
if ($form->isValid() == false)
return $this->view($form, 400);
$userManager->updateUser($user);
return $this->view('User Created', 201);
}
//...
}
And my UserType class :
class UserType extends BaseType
{
public function __construct($class = "User")
{
parent::__construct($class);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('username', 'username')
->add('email', 'email')
->add('plainPassword', 'repeated', array(
'first_name' => 'password',
'second_name' => 'confirm',
'type' => 'password'
))
->add('lastname')
->add('firstname')
->add('job_position')
->add('phone')
->add('company_name')
->add('website')
->add('sector')
->add('address')
->add('city')
->add('zip_code')
->add('country')
->add('billing_infos_same_as_company')
->add('billing_address')
->add('billing_city')
->add('billing_zip')
->add('billing_country')
->add('putf')
->add('das');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Wipsea\UserBundle\Entity\User',
'csrf_protection' => false
));
}
public function getName()
{
return 'wipsea_user_registration';
}
}
When I test it, no matter what the form isn't valid and shows no error.
And when I try to get the request :
"Validation Failed" "This form should not contain extra fields."
Is there a way to properly validate the form ?
EDIT : Updating my problem.
I would recommend you this tutorial in 3 parts - there is everything you need:
http://welcometothebundle.com/symfony2-rest-api-the-best-2013-way/
http://welcometothebundle.com/web-api-rest-with-symfony2-the-best-way-the-post-method/
http://welcometothebundle.com/symfony2-rest-api-the-best-way-part-3/
If you want to provide complex user validation you should create UserType form and pass data to this form instead of directly setting all properties:
public function postAction()
{
$user = new User();
$form = $this->createForm(new UserType(), $user);
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
// propel version
$user->save();
$response = new Response();
$response->setStatusCode(201);
// set the `Location` header only when creating new resources
$response->headers->set('Location',
$this->generateUrl(
'acme_demo_user_get', array('id' => $user->getId()),
true // absolute
)
);
return $response;
}
// return form validation errors
return View::create($form, 400);
}
In part 2 of this tutorial you have all information about creating form, passing data and validating it with RestBundle.
There is also a lot information about best practices using REST with Symfony2.
Take a look at this code:
https://github.com/piotrjura/fitness-api/blob/master/src/Fitness/FitnessBundle/Service/UsersService.php#L40
https://github.com/piotrjura/fitness-api/blob/master/src/Fitness/FitnessBundle/Controller/UsersController.php#L30
Also check validation.yml and entity serializer yml files.
You don't need forms to do the validation. And you definitly should not put the user creation and validation logic inside a controller. In case you'd like to make use of that form anyway later, eg. render it on the backend side, you'll have to write the same code twice.
I had to have the getName() method return '' in order for it to work for me.
https://github.com/FriendsOfSymfony/FOSRestBundle/issues/585

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.

symfony2 apply transformer to entity form field - empty array?

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.

Symfony2: adding form fields dynamically using information from request

Scenario:
step 1: fetch information from some archaic web service
step 2: adding x radio buttons to my form, while x depends on the information from the web service
I understand that I should add an Event Subscriber Class as shown in the documentation
$event has a setData method. Probably I have to use it. How can I access this method from the controller?
Additional information:
I am using Doctrine. At the moment I am creating a Doctrine entity and pass this to the form like this:
$product = new Product(); $form = $this->createForm(new ProductType(), $product);
This is my solution I ended up with. Not sure if this is the best way, so this answer is not marked as correct to encourage other, more experience Symfony user to come up with a better solution.
What I don't like about this solution, is that I cannot pass my whole product entity to the form any more, but have to add each attribute individually to the options array:
Anyway, that's how it is implemented now:
The dynamic data is passed using the options array as second parameter to createForm():
// ExampleController.php
use Acme\ExampleBundle\Entity\Product;
public function addAction()
{
// choices contains the dynamic data I am fetching from the webservice in my actual code
$choices = array("key1" => "value1", "key2" => "value2");
// now create a new Entity
$product = new Product();
// and some attributes already have values for some reason
$product->setPrice(42);
$form = $this->createForm(new AddproductType(),
array(
"choices" => $choices,
"price" => $product->getPrice()
)
);
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
// ... standard symfony stuff here
}
}
return array(
'form' => $form->createView()
);
}
And now for the form class. Note the event listener part and the setDefaultOptions method.
// ProductType.php
namespace Acme\ExampleBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formFactory = $builder->getFormFactory();
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (\Symfony\Component\Form\FormEvent $event) use ($formFactory) {
// get the option array passed from the controller
$options = $event->getData();
$choices = $options["choices"];
$event->getForm()->add(
// add a new element with radio buttons to the form
$formFactory->createNamed("my_dynamic_field_name", "choice", null, array(
"empty_value" => "Choose an option",
"label" => "My dynamic field: ",
"choices" => $choices
)
)
);
}
);
// ... add other form elements
$builder->add("price", "text");
// ..
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// register choices as a potential option
$resolver->setDefaults(array(
'choices' => null
));
}
}
You don't have to setData() on the $event object. You've to implement an on POST_SET_DATA logic that builds your form the way you want according to you data.
You've then to initialize your form (inside your controller) using your webservice's reply.
(Note: SET_DATA form event is deprecated since version 2.1. It'll be definitely removed in version 2.3)
Update:
You can set an array as form data and use a DataTransformer to structure your form's data the way you want. Take a look at the Data Transformation part of Symfony 2 Form Tricks during the last San Francisco Symfony Live.

Resources