Guess custom form type when embedding custom classes - symfony

Two simple classes form my app model: Money and Product.
As Money app form being reusable, I've decided to create MoneyType extending AbstractType.
// App\Entity\Product
/**
* #ORM\Embedded(class="Money\Money")
*/
private $price;
// App\Form\ProductType
$builder->add('price', MoneyType::class)
// App\Form\Type\MoneyType
class MoneyType extends AbstractType
{
private $transformer;
public function __construct(MoneyToArrayTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('amount', NumberType::class, [
'html5' => true,
'constraints' => [
new NotBlank(),
new PositiveOrZero(),
],
'attr' => [
'min' => '0',
'step' => '0.01',
],
])
->add('currency', ChoiceType::class, [
'choices' => $this->getCurrenciesChoices(),
'constraints' => [
new NotBlank(),
],
]);
$builder->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => null
]);
}
...
}
Is it possible to guess the field type without specifying it explicitly for obtaining the following code?
// App\Form\ProductType
$builder->add('price')
Any help is welcome. Thank you in advance.

You can implement a custom TypeGuesser that reads the doctrine metadata and checks if the field is an embeddable of the desired type. This is a basic implementation
namespace App\Form\TypeGuesser;
use App\Form\Type\MoneyType;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Doctrine\ORM\EntityManagerInterface;
class MoneyTypeGuesser implements FormTypeGuesserInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function guessType($class, $property)
{
if (!$metadata = $this->em->getClassMetadata($class)) {
return null;
}
if (
isset($metadata->embeddedClasses[$property]) &&
'Money\Money' == $metadata->embeddedClasses[$property]['class']
) {
return new TypeGuess(MoneyType::class, [], Guess::HIGH_CONFIDENCE);
}
}
// Other interface functions ommited for brevity, you can return null
}
You can see all the interface methods that you need to implement here.

The Form TypeGuesser is mostly based on the annotation #var Money\Money and you should be able to build your down guesser for your own types, see https://symfony.com/doc/current/form/type_guesser.html
Also take a look at https://github.com/symfony/symfony/blob/4.3/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php on how to guess the type by doctrine orm specific types.
You could derive your own app specific guesser with those two examples.

Related

Symfony5 custom validator - pass multiple submitted fields (no Entity)

We have a simple form:
namespace App\Form;
...
class SimpleForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field_1', TextType::class, [
'required' => true,
'mapped' => false,
'constraints' => [
new NotBlank()
]
])
->add('field_2', TextType::class, [
'required' => true,
'mapped' => false,
'constraints' => [
new NotBlank()
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new CustomCheck()
]
]);
}
}
If my understanding is right, CustomCheck() can refer to a complex validation over the whole form data (for instance, to validate some combinations of inputs).
My next step is to create the App\Validator\CustomCheck and App\Validator\CustomCheckValidator classes, as per Symfony's manual.
However, I do not know how to pass the submitted field_1 and field_2 data to "new CustomCheck()". Or, how to access all submitted fields from within my custom validator.
I found it is possible if I were using an Entity (Class Constraint Validator, https://symfony.com/doc/current/validation/custom_constraint.html#class-constraint-validator). But I want to know if it's possible without using an Entity.
Okay, so my findings on the matter is that there is no programmatically way to access and pass the form unmapped fields data as arguments at the level of CustomCheck() within:
$resolver->setDefaults([
'constraints' => [
new CustomCheck()
]
]);
In my case, with no mapped Entity and no mapped fields, I found two ways to have a custom validator that can access any form field data:
A custom in-form callback validator:
// custom callback validator
public function CustomCheck($data, ExecutionContextInterface $context){
// $data doesn't contain the unmapped fields, so I need to extract the form data differently
//var_dump($data['field_1']); // this works only for mapped fields (no Entity/DTO needed for this to work, only mapped fields is sufficient)
$field1_data = $context->getRoot()->get('field_1')->getData(); // this works
$field2_data = $context->getRoot()->get('field_2')->getData();
if(...something_not_good...) {
$context
->buildViolation('Custom error here')
->addViolation();
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new Callback([$this, 'CustomCheck'])
]
]);
}
A custom validator where form data needs to be extracted with $this->context:
// form builder
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new CustomCheck()
]
]);
}
// CustomCheck constraint
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
class CustomCheck extends Constraint
{
public string $message = 'Invalid blah blah.';
}
// CustomCheck validator
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CustomCheckValidator extends ConstraintValidator
{
// $value will always be null, because nothing gets passed in the first argument to this custom validator (no mapped entity, no mapped fields)
/**
* #param mixed $value
*/
public function validate($value, Constraint $constraint)
{
// extract unmapped form fields data manually
$values = [
'field_1' => $this->context->getRoot()->get('field_1')->getData(),
'field_2' => $this->context->getRoot()->get('field_2')->getData()
];
if(...something_not_good...) {
$this->context->buildViolation('Custom error here')->addViolation();
}
}
}

Initialize custom type form data

I have defined a custom form like this:
class EditOwnerProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add("user", new UserType())
->add("dog", new DogType())
->add('save', 'submit');
}
public function getName()
{
return 'edit_owner';
}
}
I want to create this form and initialize it with some data like this:
$user = new User();
$user->setLatitude(1.1)
->setLongitude(2.2)
->setAddress("custom address");
$dog = new Dog();
$dog->setDogName("Bruno")
->setDogSize("small")
->setDogBreed("Bulldog");
$formData = array(
"user" => $user,
"dog" => $dog
);
$form = $this->createForm(new EditOwnerProfileType(), $formData, array("csrf_protection" => false))->handleRequest($request);
DogTypeand UserTypeonly have NotBlank constraints
Every time i want to validate data, it allways throws error for every field like this:
"errors": {
"user": {
"latitude": [
"This value should not be blank."
],
"longitude": [
"This value should not be blank."
],
"address": [
"This value should not be blank."
]
},
"dog": {
"dogName": [
"This value should not be blank."
],
"dogSize": [
"This value should not be blank."
],
"dogBreed": [
"This value should not be blank."
]
},
Isn't supposed that i'm initializing all the values? So, if user don't pass any value for this field, is initialized with the values I defined?
Which is the correct way to initialize values on EditOwnerProfileType form?
EDIT: I tried to change the form creation (just for testing), but didn't work either.
$form = $this->createForm(new EditOwnerProfileType(), $formData, array("csrf_protection" => false));
$form->setData($formData);
$form->handleRequest($request);
EDIT2: To include DogType and UserType code
class DogType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('dogSize')
->add('dogBreed')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Dog',
));
}
public function getName()
{
return 'dog_type';
}
}
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('latitude')
->add('longitude')
->add('address')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User',
));
}
public function getName()
{
return 'user_type';
}
}
I'm using Symfony 2.7.9
I answer my own question, the problem was...
I was misunderstanding the Symfony Form Component.
I can initialize the form like:
$user = new User();
$user->setLatitude(1.1)
->setLongitude(2.2)
->setAddress("custom address");
$dog = new Dog();
$dog->setDogName("Bruno")
->setDogSize("small")
->setDogBreed("Bulldog");
$formData = array(
"user" => $user,
"dog" => $dog
);
$form = $this->createForm(new EditOwnerProfileType(), $formData, array("csrf_protection" => false))
At this point, the form is correctly initialized and all fields have the correct values.
Passing one parameter in the request edit_owner[dog][name]=othername and doing:
$form->handleRequest($request);
I expected only the edit_owner[dog][name] to be changed (And the rest of fields maintain its default value), but i noticed that the rest of fields of the form were processed and parsed as blank values and replaced my "initial values".
That was the problem and Symfony works on that way.
I make this answer so it can be useful for someone else.

FOS UserBundle - Override the FormFactory

i need some help overriding the FormFactory.
My Goal is to change the Profile. So, as i also use Facebook Login, i do not want them to change email, username and password.
So i use the ProfileController in my bundle to hand over the current user to the ProfileFormType class.
What i'm trying to do is to implement my own FormFactory, so i can set the user and put it into the options array inside the call
return $this->formFactory->createNamed($this->name, $this->type, null, array('validation_groups' => $this->validationGroups, 'user' => $this->user));
To achieve this, i need to define my FormFactory in sevices.yml.
Here is the original one from FOSUserBundle:
<service id="fos_user.profile.form.factory" class="FOS\UserBundle\Form\Factory\FormFactory">
<argument type="service" id="form.factory" />
<argument>%fos_user.profile.form.name%</argument>
<argument>%fos_user.profile.form.type%</argument>
<argument>%fos_user.profile.form.validation_groups%</argument>
</service>
I have difficulties to translate this into yml, as i do not understand the usages of aliases completely.
Could you help me to define it correct? Something like
skt_user.profile.form.factory:
class: SKT\UserBundle\Form\Factory\FormFactory
arguments: ???
Funny, after posting it, I found the solution. This is the correct configuration for my FormFactory:
skt_user.profile.form.factory:
class: SKT\UserBundle\Form\Factory\FormFactory
arguments: ["#form.factory", "%fos_user.profile.form.name%", "%fos_user.profile.form.type%", "%fos_user.profile.form.validation_groups%"]
In my controller, I simply used these 2 lines:
$formFactory = $this->container->get('skt_user.profile.form.factory');
$formFactory->setUser($user);
In the factory, I implemented this function
namespace SKT\UserBundle\Form\Factory;
use Symfony\Component\Form\FormFactoryInterface;
use FOS\UserBundle\Form\Factory\FactoryInterface;
class FormFactory implements FactoryInterface
{
private $formFactory;
private $name;
private $type;
private $validationGroups;
private $user;
public function __construct(FormFactoryInterface $formFactory, $name, $type, array $validationGroups = null)
{
$this->formFactory = $formFactory;
$this->name = $name;
$this->type = $type;
$this->validationGroups = $validationGroups;
}
public function createForm()
{
return $this->formFactory->createNamed($this->name, $this->type, null, array('validation_groups' => $this->validationGroups, 'user' => $this->user));
}
public function setUser($user)
{
$this->user = $user;
}
}
and this is how my Formtype looks
<?php
namespace SKT\UserBundle\Form\Type;
use SKT\CaromBundle\Repository\PlayerRepository;
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProfileFormType extends \FOS\UserBundle\Form\Type\ProfileFormType
{
private $class;
/**
* #param string $class The User class name
*/
public function __construct($class)
{
$this->class = $class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Do not show email and username if login uses facebook
if (!$options['user']->getFacebookId()) {
$builder
->add('email', 'email', array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle'))
->add('username', null, array('label' => 'form.username', 'translation_domain' => 'FOSUserBundle'));
}
$builder
->add('firstname', null, array('label' => 'Vorname'))
->add('lastname', null, array('label' => 'Nachname'))
->add('player', 'entity', array(
'label' => 'Spieler',
'class' => 'SKTCaromBundle:Player',
'property' => 'name',
'query_builder' => function (PlayerRepository $er) {
return $er->createQueryBuilder('p')
->orderBy('p.name', 'ASC');
},
'empty_value' => 'Verbinde Dich mit einem Spieler',
'required' => false,
));
}
public function getName()
{
return 'skt_user_profile';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => $this->class,
'intention' => 'profile',
'user' => null
));
}
}
works perfect!

Symfony2 FOSUserBundle override profile form : field form empty?

I overrided registration form from FOSUserBundle with additionals fields: it works well.
When I apply the same logic to override Profile Form : the form appears well with my additionals fields but all is empty (the fields do not contain values ​​of the connected user).
Note: when I use the default form from the bundle the profile form contains the values ​​of the connected user.
Is there a specific action compared to override the registration form to retrieve the values ​​of the connected user ?
HERE IS CODE :
src/Vn/UserBundle/Resources/config/services.yml
services:
...
vn_user.profile.form.type:
class: Vn\UserBundle\Form\Type\ProfileFormType
arguments: [%fos_user.model.user.class%]
tags:
- { name: form.type, alias: vn_user_profile }
vn_user.form.handler.profile:
class: Vn\UserBundle\Form\Handler\ProfileFormHandler
arguments: ["#fos_user.profile.form", "#request", "#fos_user.user_manager"]
scope: request
public: false
symfony/app/config/config.yml
fos_user:
...
profile:
form:
type: vn_user_profile
handler: vn_user.form.handler.profile
src/Vn/UserBundle/Form/Type/ProfileFormType.php
namespace Vn\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilder;
use FOS\UserBundle\Form\Type\ProfileFormType as BaseType;
class ProfileFormType extends BaseType
{
public function buildUserForm(FormBuilder $builder, array $options)
{
parent::buildUserForm($builder, $options);
// custom field
$builder->add('profile',new MyProfileFormType(),array(
'label' => 'PROFILE'
));
}
public function getName()
{
return 'vn_user_profile';
}
}
src/Vn/UserBundle/Form/Type/MyProfileFormType.php
namespace Vn\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class MyProfileFormType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('birthday','birthday', array(
'input' => 'array',
'widget' => 'choice',
'label' => 'Birthday',
))
->add('firstname','text', array(
'trim' => true,
'label' => 'Firstname',
))
->add('lastname','text', array(
'trim' => true,
'label' => 'Lastname',
))
->add('gender','choice', array(
'choices' => array('1' => 'Male', '2' => 'Female'),
'expanded' => true,
'required' => true,
'label' => 'Vous êtes',
));
}
public function getName()
{
return 'vn_user_myprofile';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Vn\UserBundle\Document\Profile',
);
}
}
I found the mistake in my file ProfilFormeHandler.php : in the function process() I called parent::onSucess() instead of parent::process() ...
The result is a "silent" bug (silent because not fatal error appears) due to my fault of course
Thanks for time you spent to try to help me, very sorry !
<?php
// src/Vn/UserBundle/Form/Handler/RegistrationFormHandler.php
namespace Vn\UserBundle\Form\Handler;
use FOS\UserBundle\Form\Handler\ProfileFormHandler as BaseHandler;
use FOS\UserBundle\Model\UserInterface;
class ProfileFormHandler extends BaseHandler
{
public function process(UserInterface $user)
{
//parent::onSuccess($user);
parent::process($user); // sound better of course : )
}
protected function onSuccess(UserInterface $user)
{
$this->userManager->updateUser($user);
}
}

How to dynamically add's collections within collections in Symfony2 form types

I have 3 form types in symfony2
FaultType which is the parent of all next collections
<?php
namespace My\FaultBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class FaultType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title')
->add('steps', 'collection', array(
'type' => new StepType(),
'allow_add' => true,
'prototype' => true,
'by_reference' => false,
))
->add('created')
->add('updated')
;
}
public function getDefaultOptions()
{
return array(
'data_class' => 'My\FaultBundle\Entity\Fault'
);
}
public function getName()
{
return 'my_fault_fault';
}
}
StepType:
<?php
namespace My\FaultBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class StepType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('body')
->add('photos', 'collection', array(
'type' => new PhotoType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
))
;
}
public function getDefaultOptions()
{
return array(
'data_class' => 'My\FaultBundle\Entity\Step'
);
}
public function getName()
{
return 'my_fault_step';
}
}
and the last PhotoType:
<?php
namespace My\FaultBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class PhotoType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('name')
->add('description')
->add('filename')
;
}
public function getDefaultOptions()
{
return array(
'data_class' => 'My\FaultBundle\Entity\Photo'
);
}
public function getName()
{
return 'my_fault_photo';
}
}
I found excelent article link about prototype, and with one nested form type is very good, but I have a problem when a want to get this to work with third nest mean PhotoType... Photos are in collection of Steps, which is collection of Fault..., how can I achive dynamically add/remove photos for steps with this example...?
I made a JS snippet that can be of help here. you just have to add two buttons [add new, delete last].
https://gist.github.com/juanmf/10483041
it can handle recursive/nested prototypes.
It's coupled with a mediator (same as Symfony event Dispatcher) that allows you to bind generated controls to events. If you dont need the mediator delete these lines:
docdigital.mediatorInstance.send(
docdigital.constants.mediator.messages.clonePrototype_prototypeAdded,
$clone
);
You have to make you own prototype.
There are 2 solutions:
Find with regex all digit segments of a property_path, and replace them with placeholder
$segments_found = preg_match('/\[(\d+)\]/', $prototype, $matches);
Use recursion to find top collection parent and build path manually from there.
Did you try reordering items? This is total disaster;)

Resources