I'm having to make changes to collections (Symfony form CollectionType) to respect ordering and entity removal and I'm using a preSubmit event listener to make the changes as per this issue suggestion. However the changes aren't making it back to the controller/persistence.
The data is correct after the preSubmit and being set on the event using $event->setData($data) but the changes are not retained after submitting in $form->getData()
My pre submit abstract class:
abstract class AbstractEntityFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit']);
}
/**
* #param FormEvent $event
*/
public function onPreSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
$document = $form->getData();
foreach ($form->all() as $field) {
/** #var FormInterface $field */
$fieldName = $field->getName();
if ($field->getConfig()->getType()->getInnerType() instanceof CollectionType) {
// .. DO ALL TRANSFORMATION
}
}
$event->setData($data);
}
}
When I dump $data here, everything is correct.
FormType extending this AbstractEntityFormType class:
class DocumentType extends AbstractEntityFormType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
* #return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** ADD THE LISTENER **/
parent::buildForm($builder, $options);
$builder->add('title', TextType::class, [
'required' => true,
'constraints' => [
new NotBlank([
'message' => '"title" is empty',
]),
],
]);
$builder->add('symptoms', CollectionType::class, [
'entry_type' => SymptomType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
$builder->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
$builder->add('submit', SubmitType::class);
}
/**
* #param OptionsResolver $resolver
* #return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Document::class,
]);
}
}
My controller action:
/**
* #param Request $request
* #param int|null $documentId
* #return Response
*/
public function formAction(Request $request, int $documentId = null)
{
if (!$documentId) {
$document = new Document();
} else {
try {
$document = $this->documentService->getDocument($documentId);
} catch (InvalidDocumentException $e) {
return new NotFoundResponse('Document not found');
}
}
$document = $this->setAuthor($document);
$form = $this->formFactory->create(DocumentType::class, $document);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$document = $this->documentService->createDocument($form->getData());
return new RedirectResponse($this->router->generate('ui.document.view', [
'documentId' => $document->getId()
]));
}
return $this->templating->renderResponse(
'#KBS/Document/form.html.twig', [
'form' => $form->createView(),
]
);
}
When I dump $form->getData() before persisting the document with the service, the data is incorrect - it hasn't been affected by the preSubmit. What am I missing?
FYI, this is using Symfony 3.2 base components.
Update
Thanks to #LBT, I'm a bit closer but still facing some issues. Using this approach in the controller, I am able to get the correct data out of the request at the point of submitting the form:
public function formAction(Request $request, int $documentId = null)
{
if (!$documentId) {
$document = new Document();
} else {
try {
$document = $this->documentService->getDocument($documentId);
} catch (InvalidDocumentException $e) {
return new NotFoundResponse('Document not found');
}
}
$document = $this->setAuthor($document);
$form = $this->formFactory->create(DocumentType::class, $document);
if ($request->isMethod('POST')) {
$form->submit($request->request->get($form->getName()));
if ($form->isSubmitted() && $form->isValid()) {
$document = $this->documentService->createDocument($form->getData());
return new RedirectResponse($this->router->generate('ui.document.view', [
'documentId' => $document->getId()
]));
}
}
return $this->templating->renderResponse(
'#KBS/Document/form.html.twig', [
'form' => $form->createView(),
]
);
}
However, $form->submit($request->request->get($form->getName())); is also saving incorrect (original) data before being transformed. Even though the data in the request is correct:
/var/www/html/src/Controller/DocumentController.php:94:
array (size=8)
'title' => string 'Tag test' (length=8)
'symptoms' =>
array (size=1)
0 =>
array (size=3)
'position' => string '0' (length=1)
'title' => string 'The smell is terrible' (length=21)
'description' => string 'pew' (length=3)
'tags' =>
array (size=4)
0 =>
array (size=1)
'title' => string 'Volvo' (length=5)
1 =>
array (size=1)
'title' => string 'Engine' (length=6)
2 =>
array (size=1)
'title' => string 'BMW' (length=3)
3 =>
array (size=1)
'title' => string 'Funky' (length=5)
Feel like I'm getting closer but I don't seem to be able to use this request data as suggested in the documentation for some reason.
Related
I have this form :
class RegistrationFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(),
]
])
->add('username')
->add('password')
;
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'app_user_register';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'allow_extra_fields' => true,
'csrf_protection' => false,
]);
}
}
My api in controller :
/**
* #Route("/api/register")
* #Method("POST")
* #param Request $request
*
* #return JsonResponse
*/
public function register(
UserService $userService,
Request $request
)
{
try {
return $userService->register(json_decode($request->getContent(), true));
} catch (\Exception $e) {
return $this->json([
'status' => Response::HTTP_INTERNAL_SERVER_ERROR,
'result' => $e->getMessage()
]);
}
}
And my function in service :
public function register($formData)
{
$user = new User();
$form = $this->formFactory->create(RegistrationFormType::class, $user);
$form->submit($formData);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($user);
$this->entityManager->flush();
return new JsonResponse([
'status' => Response::HTTP_OK,
'result' => true
]);
}
return new JsonResponse([
'status' => Response::HTTP_BAD_REQUEST,
'result' => FormErrorFormatter::getErrorsFromForm($form)
]);
}
When I tried to call the api /api/register in postman with
{
"username": "test1",
"email": "test1",
"password": "123456"
}
I get 200 code, but normally should drop an error because the email is not valid, as I put in form creation that the field email should be in the email format, even if I put an empty string in email I get the 200 code. So seems the validations is not working.
EmailType, as far as I can tell, has no default constraints. However, you override the constraints by demanding it's NotBlank which is definitely not the same as the Email constraint. the Form does add type="email" to the html, which the browser will enforce (which is technically unreliable, because the user can just turn it into a text field).
Solution is probably to use the Email constraint and set the required property to true.
Try :
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(),
new Email(),
]
])
and add to your Entity :
/**
* #Assert\Email(
* message = "The email '{{ value }}' is not a valid email.",
* checkMX = true
* )
*/
protected $email;
I try to save new data (when I modify, it's works ok) in embed form, but allow_add doesn't work.
When i send data throw prototype inputs, the request->request is:
ads[adsCapPublisher][new_created_0][publisher]:2302
ads[adsCapPublisher][new_created_0][dailyLimit]:2
ads[adsCapPublisher][edit_0][publisher]: 5201
ads[adsCapPublisher][edit_0][dailyLimit]: 5
The edit_0 was good procesed and creates the AdsType with the AdsCapPublisherType, but when i added new inputs throws javascript, when procesed the data, fails in $request->handleRequest():
Catchable fatal error: Argument 1 passed to addAdsCapPublisher() must be an instance of AdsCapPublisher, null given in Ads.php on line 721
The data is null. And when if I set allow_add to false, the edit entities are procesed good but the news return a form error:
this form should not contain extra fields
Here my clases:
Ads.php
class Ads{
/**
* #ORM\OneToMany(targetEntity="AdsCapPublisher", mappedBy="ad", cascade={"persist", "remove"})
**/
protected $adsCapPublisher;
public function addAdsCapPublisher(AdsCapPublisher $adCapPublisher)
{
$this->adsCapPublisher[] = $adCapPublisher;
return $this;
}
public function removeAdsCapPublisher(AdsCapPublisher $adCapPublisher)
{
$this->adsCapPublisher->removeElement($adCapPublisher);
}
public function getAdsCapPublisher()
{
return $this->adsCapPublisher;
}
public function setAdsCapPublisher($adsCapPublisher)
{
$this->adsCapPublisher = $adsCapPublisher;
return $this;
}
[...]
}
AdsCapPublisher.php
class AdsCapPublisher{
/**
* #var Users
*
* #ORM\ManyToOne(targetEntity="Users", fetch="LAZY")
* #ORM\JoinColumn(name="id_publisher", referencedColumnName="id_user")
*/
protected $publisher;
/**
* #var decimal $dailyLimit
*
* #ORM\Column(name="daily_limit", type="decimal", nullable=false)
*/
protected $dailyLimit;
[...]
}
AdsType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$entityId= $builder->getData();
$builder->add('adsCapPublisher', 'collection', array(
'type' => new AdsCapPublisherType(),
'required' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
AdsCapPublisherType.php
class AdsCapPublisherType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// -- SECTION: General
$builder
->add('publisher', 'hidden', array(
'attr' => array(
'class' => 'js-field-popup-publisher-added'
)))
->add('dailyLimit', 'text', array(
'required' => false,
'attr' => array(
'class' => 'js-field-popup-budget-added form-control k-input-field k-is-input-text k-ellipsis k-has-currency-inside-input',
'min' => 0,
)
));
}
public function getName()
{
return 'adsCapPublisher';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'data_class' => 'Entity\AdsCapPublisher',
'csrf_field_name' => '_token',
'intention' => 'ads',
'translation_domain' => 'ads',
'empty_data' => null
));
}
}
Any help? :S
I am simply trying to add an email constraint to the unmapped field below and for some reason in the controller action it isn't picking it up. Is there something else I need to be doing?
Form Class
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', null, [
'label' => false,
'attr' => ['placeholder' => 'Name*']
]);
$builder->add('email', null, [
'mapped' => false,
'label' => false,
'attr' => ['placeholder' => 'Email*'],
'constraints' => [
new Email(["message" => "Please enter a valid Email Address"])
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Test\AppBundle\Entity\Feedback',
'cascade_validation' => true,
'validation_groups' => function(Form $form)
{
$feedback = $form->getData(); /** #var Feedback $feedback */
if ($feedback->getType() == Feedback::Type_Feedback)
return Feedback::ValidationGroup_Feedback;
else if ($feedback->getType() == Feedback::Type_Link)
return Feedback::ValidationGroup_Link;
throw new \Exception("Couldn't generate a valid Validation Group for FeedbackForm.php");
},
));
}
/**
* #return string
*/
public function getName()
{
return 'site_feedback_form';
}
Controller action
public function feedbackSiteSubmitAction(Request $request)
{
// The form award
$feedback = new Feedback();
// Create the form so we can bind send form values to it
$form = $this->createNewFeedbackForm($feedback);
// Bind form values
$form->handleRequest($request);
// Save
if ($form->isValid())
{
// Add the submission to a member
$email = $form->get('email')->getData();
$email = strtolower($email);
$member = $this->getMemberRepository()->loadByEmail($email);
if (!$member)
$member = $this->generateNewPersistedMember($email);
// Update the mail property
$member->setReceiveEmail(!$form->get('dontReceiveAlerts')->getData());
// Add the feedback to the member
$feedback->setMember($member);
// Persist the Feedback
$this->getEntityManager()->persist($feedback);
// Commit
$this->getEntityManager()->flush();
// Response
return $this->jsonSuccess([
'html' => $this->renderView('TestAppBundle:Site/partials:feedback_form_success.html.twig', [
'typeString' => $feedback->getType() == Feedback::Type_Feedback ? "Feedback" : "Resource suggestion"
])
]);
}
// Return errors
return $this->jsonError($this->getAllFormErrors($form));
}
In Symfony 2.8 upwards you do the following:
$builder->add('email', Symfony\Component\Form\Extension\Core\Type\EmailType::class);
Versions before that use:
$builder->add('email', 'email');
Then you add all your validation criteria via annotations in your Feedback class. See the options for EmailType here.
My form data returned after the flow has finished is a bit odd. Two of the entity properties (that were populated using an embedded form) are in the array format keyed by the field names and not a string.
Engine and model arrays of the values lol.
Anything I have done to cause this?
Here is my setup.
The flow
<?php
namespace AppBundle\Form;
use AppBundle\Form\Type\CreateVehicleFormType;
use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Form\FormFlowInterface;
/**
* Class CreateVehicleFlow
* #package AppBundle\Form
*/
class CreateVehicleFlow extends FormFlow
{
protected $allowRedirectAfterSubmit = true;
/**
* #return array
*/
protected function loadStepsConfig()
{
return [
// Step 1.
[
'label' => 'Wheels',
'form_type' => CreateVehicleFormType::class,
],
// Step 2.
[
'label' => 'Engine',
'form_type' => CreateVehicleFormType::class,
'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) {
return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine();
},
],
// Step 3.
[
'label' => 'Model',
'form_type' => CreateVehicleFormType::class
],
// Step 4.
[
'label' => 'Confirmation',
],
];
}
}
Parent create vehicle form
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CreateVehicleFormType
* #package AppBundle\Form\Type
*/
class CreateVehicleFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
switch ($options['flow_step']) {
case 1:
$validValues = [2, 4];
$builder->add(
'numberOfWheels',
ChoiceType::class, [
'choices' => array_combine($validValues, $validValues),
'attr' => [
'class' => 'form-control input-lg',
'required' => 'required'
],
]);
break;
case 2:
$builder->add('engine', VehicleEngineFormType::class, []);
break;
case 3:
$builder->add('model', VehicleModelFormType::class, [
'numberOfWheels' => $options['data']->getNumberOfWheels()
]);
break;
}
}
/**
* #return string
*/
public function getBlockPrefix()
{
return 'createVehicle';
}
/**
* #return string
*/
public function getName()
{
return 'create_vehicle';
}
}
Child vehicle engine form
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CreateVehicleFormType
* #package AppBundle\Form\Type
*/
class VehicleEngineFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('engine', ChoiceType::class, [
'label' => false,
'choices' => [
'Diesel' => 'Diesel',
'Petrol' => 'Petrol',
'Electric' => 'Electric',
'Hybrid' => 'Hybrid'
],
'attr' => [
'class' => 'form-control input-lg',
'required' => 'required'
],
]
);
}
/**
* #return string
*/
public function getName()
{
return 'vehicle_engine';
}
}
Child vehicle model form
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class CreateVehicleFormType
* #package AppBundle\Form\Type
*/
class VehicleModelFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [];
if ($options['numberOfWheels'] === 2) {
$choices['BMW R nineT'] = 'BMW R nineT';
$choices['Ducati Monster 1200S'] = 'Ducati Monster 1200S';
} else {
$choices['Ferrari LaFerrari'] = 'Ferrari LaFerrari';
$choices['Aston Martin One-77'] = 'Aston Martin One-77';
}
$builder->add('model', ChoiceType::class, [
'label' => false,
'choices' => $choices,
'attr' => [
'class' => 'form-control input-lg',
'required' => 'required'
],
]
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'numberOfWheels' => 2,
]);
}
/**
* #return string
*/
public function getName()
{
return 'vehicle_model';
}
}
And finally my controller including my hack to get them back as string properties
/**
* One form type for the entire flow.
*
* #param Request $request
* #return Response
*
* #Route("/create.vehicle", name="create.vehicle")
*/
public function CreateVehicleAction(Request $request)
{
$formData = new Vehicle();
$flow = $this->get('app.form.flow.create_vehicle'); // must match the flow's service id
$flow->setGenericFormOptions([
'action' => $this->generateUrl('create.vehicle')
]);
$flow->bind($formData);
// form of the current step
$form = $submittedForm = $flow->createForm();
if ($flow->isValid($form)) {
$flow->saveCurrentStepData($form);
if ($flow->nextStep()) {
// form for the next step
$form = $flow->createForm();
} else {
// flow finished
// Bit hacky but not sure hy they are arrays set to the property.
if (is_array($formData->getEngine())) {
$formData->setEngine($formData->getEngine()['engine']);
}
if (is_array($formData->getModel())) {
$formData->setModel($formData->getModel()['model']);
}
$flow->reset();
$this->flashMessage(
'success',
[
'title' => 'Vehicle Created',
'message' => sprintf(
'You have created a %s %d wheeled vehicle.',
$formData->getModel(),
$formData->getNumberOfWheels()
)
]
);
return $this->redirect(
$this->getPresencePath('sign_in.page')
);
}
}
if ($flow->redirectAfterSubmit($submittedForm)) {
$params = $this
->get('craue_formflow_util')
->addRouteParameters(
array_merge(
$request->query->all(),
$request->attributes->get('_route_params')
),
$flow
);
return $this->redirect(
$this->getPresencePath('sign_in.page', $params)
);
}
return $this->render('AppBundle:multi-step-form:create-vehicle.html.twig', array(
'form' => $form->createView(),
'flow' => $flow
));
}
Can anyone spot the issue?
I just recently updated to 2.8 and now I get the following error when calling the create function of the Form Factory.
Error: Class Symfony\Component\Form\Extension\Core\Type\FormType contains 1
abstract method and must therefore be declared abstract or implement the
remaining methods (Symfony\Component\Form\FormTypeInterface::setDefaultOptions)
The call of the FormFactory looks like this:
$this->formFactory->create(
get_class(new ProductType()),
$product,
[
'method' => 'POST',
'type' => $type,
'locales' => $context->shop->getLocales(),
'product' => $product,
'numberDataTransformer' => $this->numberTransformerFactory->createFromLocale(
$context->user->getLocale(),
$context->shop->getDefaultLocale()
),
'priceType' => $context->shop->getConfiguration()->getProductConfiguration()->getPricesBackend(),
'isShortDescriptionEnabled' => $context->shop->getConfiguration()->getProductConfiguration()->isShortDescriptionEnabled()
]);
I tried several ways to pass the ProductType to the function, but none seems to work. I always get one out of two results: Either the result is that the type cannot be found or the error is returned that the FormType does not implement setDefaultOptions.
What did I miss?
EDIT:
Here are some additional code:
The declaration of the formFactory parameter:
public function __construct(Request $request, FormFactoryInterface $formFactory)
{
$this->request = $request;
$this->formFactory = $formFactory;
}
The ProductType class
<?php
namespace CustomNamespace\BackendBundle\Product\Form;
use CustomNamespace\BackendBundle\Common\NumberDataTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use CustomNamespace\BackendBundle\Product\Form\ImageType;
use ShopwareEasy\BackendBundle\Product\Form\AttachmentType;
use Symfony\Component\Validator\Exception\InvalidOptionsException;
/**
* Form element type for products.
*/
class ProductType extends AbstractType
{
/**
* #var string
*/
private $method;
/**
* #var string
*/
private $type;
/**
* #var array
*/
private $locales;
/**
* #var Product
*/
private $product;
/**
* #var \CustomNamespace\BackendBundle\Common\NumberDataTransformer
*/
private $numberDataTransformer;
/**
* #var string
*/
private $priceType;
/**
* #var bool
*/
private $isShortDescriptionEnabled;
/**
* #param FormBuilderInterface $builder
* #param array $options
* #return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->setMethod($this->method);
$regionType = new RegionShippingTimeType();
if ($this->type == 'download') {
$regionType->enableForDownloadableProduct();
}
$builder->add('regions', 'collection', array(
'type' => $regionType,
'label' => false,
'options' => array(
'required' => false,
'attr' => array('class' => 'email-box')
),
));
$builder->add('vendor', 'text', ['label' => 'form_product_vendor']);
if ($this->type == 'normal') {
$builder->add(
'mainVariant',
new MainVariantNormalType(
$this->method,
$this->locales,
$this->product,
$this->numberDataTransformer,
$this->priceType,
$this->isShortDescriptionEnabled
),
['error_bubbling' => false, 'label' => false]
);
} elseif ($this->type == 'download') {
$builder->add(
'mainVariant',
new MainVariantDownloadType(
$this->method,
$this->locales,
$this->product,
$this->numberDataTransformer,
$this->priceType,
$this->isShortDescriptionEnabled
),
['error_bubbling' => false, 'label' => false]
);
} elseif ($this->type == 'variant') {
$builder->add(
'mainVariant',
new MainVariantVariantType(
$this->method,
$this->locales,
$this->product,
$this->numberDataTransformer,
$this->priceType,
$this->isShortDescriptionEnabled
),
['error_bubbling' => false, 'label' => false]
);
}
if ($this->method == 'PUT') {
$builder->add(
'images',
new ImageType(),
['error_bubbling' => true, 'label' => false]
);
$builder->add(
'attachments',
new AttachmentType(),
['error_bubbling' => true, 'label' => false]
);
}
}
/**
* #param \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver
*/
public function configureOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'CustomNamespace\\BackendBundle\\Product\\Product',
'csrf_protection' => false,
'error_bubbling' => false,
'cascade_validation' => true,
'method' => 'POST',
'type' => 'normal'
)
);
parent::configureOptions($resolver);
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
/** #var OptionResolver $resolver */
$this->configureOptions($resolver);
}
public function getName() {
return get_class($this);
}
}
The problem was produced by files that were not updated at all.. The implementation of setDefaultOptions should still exist in the Symfony 2.8 classes - but they didn't.
After destroying my vagrant and recreating it again, everything worked just fine.
But thanks everyone for the help!