Symfony2 - simplecms : how add extras data? - php

I've a problem in my form for the entity Page from simplecms
I want to add an item in the array Extras, so i added it in my formtype :
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Cmf\Bundle\SimpleCmsBundle\Doctrine\Phpcr\Page;
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', 'text', array(
'label' => 'Titre',
'attr' => array('placeholder' => 'Titre complet de la page')
))
->add('name', 'text', array(
'label' => 'Label',
'attr' => array('placeholder' => 'nom-simplifie-de-la-page')
))
->add('body', 'ckeditor')
->add('locale', 'hidden')
->add('publishable')
->add('extras_link','text', array(
'property_path' =>"extras['link']",
));
}
The vars are in class Page (i didnt had to override it) and functions removeExtra() and addExtra() too (necessary to my form alimentation)
/**
* Add a single key - value pair to extras
*
* #param string $key
* #param string $value - if this is not a string it is cast to one
*/
public function addExtra($key, $value)
{
$this->extras[$key] = (string) $value;
}
/**
* Remove a single key - value pair from extras, if it was set.
*
* #param string $key
*/
public function removeExtra($key)
{
if (array_key_exists($key, $this->extras)) {
unset($this->extras[$key]);
}
}
form is working, but when i submit, it find removeExtra() but not addExtra()
"Found the public method "removeExtra()", but did not find a public "addExtra()" on class Symfony\Cmf\Bundle\SimpleCmsBundle\Doctrine\Phpcr\Page"
Somebody already had this problem? or know how to add data in extras?
THX (sorry for my english)

This is a limitation of the form layer unfortunately. The logic that looks for the right method only finds a adder with one parameter. What we ended up doing is using the burgov/key-value-form-bundle : https://github.com/Burgov/KeyValueFormBundle/
This required a PR like this to enabled in SeoBundle: https://github.com/symfony-cmf/SeoBundle/pull/158
from the code in Page::setExtras i think this should already work, though a similar PR to the one on SeoBundle would make it slightly more efficient.

Related

TransformationFailedException when entity is passed to Form

I'm using Symfony 3.3 and im getting this TransformationFailedException Error, when i load my profile page:
Unable to transform value for property path "postalcode": Expected a numeric.
The postalcode value for this user in the database is:
'34125abc'
The postalcode attribute defined in UserProfile Entity:
/**
* #ORM\Column(type="string")
*/
private $postalcode;
My ProfileController:
class ProfileController extends Controller{
/**
* #Route("/edit_profile", name="edit_profile")
*/
public function profileAction(Request $request){
$profile = $this->getDoctrine()->getManager()->getRepository('AppBundle:UserProfile')->findOneBy(['user_id' => $this->getUser()->getUserId()]);
// If no UserProfile exists, create a UserProfile Object to insert it into database after POST
if(null === $profile){
$profile = new UserProfile();
$profile->setUserId($this->getUser()->getUserId());
}
$form = $this->createForm(EditProfileFormType::class);
$form->setData($profile);
// only handles data on POST
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$result = $this->forward('AppBundle:API\User\Profile:update_profile', array(
'profile' => $profile
));
if(200 === $result->getStatusCode()){
$this->addFlash('success', "Profile successfully created!");
}
}
return $this->render(':User/Profile:edit_profile.html.twig', [
'EditProfileForm' => $form->createView(),
]);
}
}
My EditProfileFormType:
class EditProfileFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', ChoiceType::class, array(
'choices' => array(
'Mr' => 'Mr',
'Mrs' => 'Mrs'
)
))
->add('firstName')
->add('lastName')
->add('street')
->add('postalcode', NumberType::class)
->add('city')
->add('telephone')
->add('mobile')
->add('company')
->add('birthday' , BirthdayType::class)
->add('callback', CheckboxType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\UserProfile',
'validation_groups' => array('edit_profile')
]);
}
public function getBlockPrefix()
{
return 'app_bundle_edit_profile_form_type';
}
}
So the problem here seems to be the not numeric string value in the databse
'34125abc'
which is stored in the $profile entity object and is passed to the form by $form->setData($profile); So when the data is being set, the error is thrown because of the Numbertype in this line ->add('postalcode', NumberType::class). Is there a way to pass the postalcode value to the form, even if it's not numeric and only check the Numbertype, when the form is submitted? Because i don't need validation, when I pass data to the form. Just when, it's submitted.
The solution was quite simple, but hard to find out.. I changed
->add('postalcode', NumberType::class)
to
->add('postalcode', TextType::class)
in my EditProfileFormType.php. Why? Because the form builder just need to know the type of the field in the database. It's shouldn't care in this case if it is numeric or not, because thats the task of the model restriction. In this case it is a string, so it is Texttype in a form. All the form type's are applied, when the form is set, but the validation groups are just validated, when the form is submitted! That's exactly they way it should be!

Date after another date in symfony form

I have a form that contain 2 dates: start date(datedebut) and end date(datefin).
I want the end date to be always after the start date. How can i do that?
My form type:
class ReservationType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('datedebut',DateType::class,array(
'widget' => 'choice',
'years' => range(date('Y'), date('Y')+20),
))
->add('datefin',DateType::class,array(
'widget' => 'choice',
'years' => range(date('Y'), date('Y')+20),
))
->add('nbplaces')
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Bridge\TravelBundle\Entity\Reservation'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'Bridge_TravelBundle_Reservation';
}
}
You can use a Callback Validator for this. Injected into that callback is a ExecutionContextInterface by which you can access the form, and thus other form params.
Here's an example:
use Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
// …
$builder
->add('start', 'datetime',
'constraints' => [
new Constraints\NotBlank(),
new Constraints\DateTime(),
],
])
->add('stop', 'datetime', [
'constraints' => [
new Constraints\NotBlank(),
new Constraints\DateTime(),
new Constraints\Callback(function($object, ExecutionContextInterface $context) {
$start = $context->getRoot()->getData()['start'];
$stop = $object;
if (is_a($start, \DateTime::class) && is_a($stop, \DateTime::class)) {
if ($stop->format('U') - $start->format('U') < 0) {
$context
->buildViolation('Stop must be after start')
->addViolation();
}
}
}),
],
]);
Usually these kind of tasks are solved by adding validation constraints to check if value of one field is greater then the other. Implement callback validation constraint as stated in the documentation: http://symfony.com/doc/current/reference/constraints/Callback.html You can also create your custom class constraint validator and place validation logic there: http://symfony.com/doc/current/validation/custom_constraint.html
This way whenever a user tries to submit value of datefin which is less than selected value of datedebut he will see a validation error and the form will not be processed.
After that you can always add some javascript code that will filter available dates in datefin field after value in datedebut field is changed.
Also you can use dynamic form modification to render the second date field (and filter its available dates on server side) only if value of the first one is submitted. Check this out: http://symfony.com/doc/current/form/dynamic_form_modification.html

Symfony, how to use form event to validate dynamic client-side form

I'm using the select2 plugin with ajax to have a dynamic field on my form, but when i submit the it return me an error "This value is not valid", which is normal cause i use the ChoiceType with an empty array() in the choices options on creation. According to this part of the symfony doc, the form event is my savior, so trying to use it but it look like something wrong with my code and can't really see what.
So My Question Is :
HOW to pass the choices possibility to the field, for the form to be valid.
My form Type
class ArticleType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//My other field
//My functions to add the field with the possible choices
$formModifier = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$listImages = array();
}
//die(var_dump($listImages)); //Array of Image
$form->add('image', ChoiceType::class, array(
'attr' => array(
'id' => 'image'),
'expanded' => false,
'multiple' => false,
'choices' => $listImages));
};
$formModifierSubmit = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$form->get('image')->addError(new FormError(
'Veuillez choisir une image s.v.p.'
));
}
//die(var_dump($listImages)); //Array of Image object
$config = $form->get('image')->getConfig();
$opts = $config->getOptions();
$chcs = array('choices' => $listImages);
//die(var_dump($chcs)); //output an array with a 'choices' keys with array value
array_replace($opts, $chcs); //not work
//array_merge($opts, $chcs); //not work
//die(var_dump($opts)); //replacements/merge are not made
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be the entity Article
$data = $event->getData();
$formModifier($event->getForm(), $data->getImage());
}
);
//$builder->get('image')->addEventListener( //give error cause the field image don't exist
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifierSubmit) {
$imageVal = $event->getData();
//die(var_dump($imageVal)); //return all the submitted data field in an array
//But when change this event to Submit it return the Article model populated by the submitted data, EXCEPT the image field which have null as value
$formModifierSubmit($event->getForm(), $imageVal['image']);
}
);
}
public function getChoiceValue($imageValue, $options)
{
$listImages = $options['em']->getRepository('AlmotivAppBundle:Image')->findBy(array(
'id' => $imageValue
));
return $listImages; //array of Image object
}
[...]
}
For Info
My image field is not depending on any other field like the doc example, so i need to populate the choices options on PRE_SUBMIT event to give the possible choice.
And also image have a ManyToOne relation in my Article entity
class Article implements HighlightableModelInterface
{
//some properties
/**
* #ORM\ManyToOne(targetEntity="Image\Entity\Path", cascade={"persist"})
* #Assert\Valid()
*/
private $image;
}
If i'm in the bad way let me know cause i'm out of idea now, i try much thing, like
array_replace with the options in the configuration of the field but didn't wrong.
make an ajax request to the url of the form action url : $form.attr('action'), i think it will load the choices option with the possible of <option> but my select is still returned with none <option>.
and much more (can't remmenber).
And also i'm using the v3.1 of the framework with the v4.0.3 of the select2 plugin, if need more info just ask and thx for reading and trying help.
Edit
Just add some info to be more clear
You making things way too complicated. In your documentation example they add eventListener for already existing form field ('sport') and you are adding it to only later added field which does not exist (your 'image' field and 'position' field from the documentation example).
You should use EntityType and if you need (which I'm not if sure you are) filter your images using query_builder option, for validation add constraints (example with controller).
class ArticleType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder
// My other field
$imageFieldFunction = $this->getImageFieldFunction();
$builder->addEventListener(FormEvents::PRE_SET_DATA, $imageFieldFunction);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $imageFieldFunction);
}
private function getImageFieldFunction()
{
return function(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
//when your data_class is Article
$image = $data->getImage();//depending on your Article class
/*if you are using data_class => null
$image = $data['image'];
*/
$imageId = $image ? $image->getId() : 0;
$builder->add('image', EntityType::class , array(
'class' => 'AlmotivAppBundle:Image',
'attr' => array(
'id' => 'image'
) ,
'expanded' => false,
'multiple' => false,
'constraints' => new NotBlank(),
'query_builder' => function (EntityRepository $er) use ($imageId) {
return $er->createQueryBuilder('i')
->where('i.id = :image_id')
->setParameter('image_id', $imageId);
}
));
}
}
}

Symfony2 LexikFormFilterBundle: Empty values for filter_entity cause form errors

I'm using the LexikFormFilterBundle (current dev-version) with Symfony 2.7.0-BETA1. It works as expected, but…
The Problem
all filter_entity fields throw form errors, when the empty option is submitted (i.e. no entity is selected). It is documented in the basic example, that one has to set
'validation_groups' => array('filtering') // avoid NotBlank() constraint-related message
But in my case, the ManyToOne Entities don't use a NotBlank()-Assert. Despite this, the form throws errors about non existant entities ''. When I select an entity, the filter filters correctly and no error message appears. Other filter field types also don't throw errors, when they are empty.
Here is a stripped down representation of my code:
Entity code:
<?php
// src/AppBundle/Entity/Serviceevent.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Serviceevent
*
* #ORM\Table(name="serviceevents")
* #ORM\Entity(repositoryClass="AppBundle\Entity\ServiceeventRepository")
*/
class Serviceevent
{
/**
* #var \AppBundle\Entity\Park
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Park", inversedBy="serviceevents")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="park_id", referencedColumnName="id", nullable=false)
* })
*/
private $park;
}
Form filter code:
<?php
// /src/AppBundle/Filter/ServiceeventFilterType.php
namespace AppBundle\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ServiceeventFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('park', 'filter_entity', array(
'class' => 'AppBundle:Park',
'property' => 'identifyingName',
'label' => 'Park',
'placeholder' => '',
'required' => false,
));
;
}
public function getName()
{
return 'serviceevent_filter';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'validation_groups' => array('filtering'), // avoid NotBlank() constraint-related message
));
}
}
What I expect
I expect, that with no value selected, no filtering for that field happend without an error thrown.
What I get
Instead, I get form errors for all entity-fields like this one:
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).children[park] =
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "[park]": The choice "" does not exist or is not unique
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
The choice "" does not exist or is not unique
Question is
The question is: how do I get rid of those errors?
User wcluijt's fix in https://github.com/symfony/symfony/issues/14393#issuecomment-94996862 fixed this for me. This confirms, that my problem here was provoked by a Bug in Symfony 2.7.0-BETA1. So wo can close this question as fixed. Sorry for wasting your time with a beta-related bug.

Symfony Embedded Form Conditional Validation

I have a form which contains three objects:
$builder
->add('customer', new CustomerType())
->add('shippingAddress', new AddressType())
->add('billingAddress', new AddressType())
->add('sameAsShipping', 'checkbox', ['mapped' => false])
;
Each of the embedded forms has their own validation constraints and they work. In my main form, I have cascade_validation => true so that all of the embedded form validation constraints are applied. This also works.
I am having trouble 'disabling' the validation on the billingAddress form if the sameAsShipping checkbox is enabled. I can't make the validation in the AddressType form conditional because it always needs to be enforced for the shippingAddress form.
I've solved this same problem by using validation groups.
First, this is important: use the validation_groups option in your AddressType to set the validation groups of every constraint of each field in the type:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\FormBuilderInterface;
class AddressType extends \Symfony\Component\Form\AbstractType
{
function buildForm(FormBuilderInterface $builder, array $options)
{
$groups = $options['validation_groups'];
$builder->add('firstName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
$builder->add('lastName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
}
}
Then, in the parent form pass different validation groups to the two fields:
<?php
$formBuilder = $this->get('form.factory')
->createNamedBuilder('checkout', 'form', null, [
'cascade_validation' => true,
])
->add('billingAddress', 'address', [
'validation_groups' => 'billingAddress'
])
->add('shippingAddress', 'address', [
'validation_groups' => 'shippingAddress'
]);
Then, determine determine your validation groups by looking at the value of the checkbox.
if ($request->request->get('sameAsShipping')) {
$checkoutValidationGroups = ['Default', 'billingAddress'];
} else {
$checkoutValidationGroups = ['Default', 'billingAddress', 'shippingAddress'];
}
You can then validate only either the billingAddress or the shippingAddress, or both using the validation group mechanism.
I chose to use a button:
$formBuilder->add('submitButton', 'submit', ['validation_groups' => $checkoutValidationGroups]);
Create a form model (I use it in nearly every form, but this code here is not tested):
/**
* #Assert\GroupSequenceProvider()
*/
class YourForm implements GroupSequenceProviderInterface {
/**
* #Assert\Valid()
*/
private $customer;
/**
* #Assert\Valid()
*/
private $shippingAddress;
/**
* #Assert\Valid(groups={'BillingAddressRequired'})
*/
private $billingAddress;
private $billingSameAsShipping;
public function getGroupSequence() {
$groups = ['YourForm'];
if(!$this->billingSameAsShipping) {
$groups[] = 'BillingAddressRequired';
}
return $groups;
}
}
Try to use meaningful names. sameAsShipping is hard to understand. Read the if-condition in getGroupSequence: if not billing (address) same as shipping (address) then billing address required.
That's all, clear code in my opinion.

Categories