Symfony Validation on Relations - symfony

Suppose I have the following Entities:
Pizza
PizzaType = [Cheese, Meat, Vegetable, Supreme]
Toppings = [Pepperoni, Sausage, Peppers, Mushrooms]
When I create a pizza and set its type to Meat I want to be able to validate that only a "meat" type of topping is chosen, same with Vegetable, while supreme will allow any topping (meat or vegetable)... Assume there is already a property for each topping that maps to a pizza type. Really just interested in know what type of validator to use...

You can pass a callback to validation_groups option in your form for the desired field. Callback will have access to submitted data like this
... 'validation_groups' => static function (FormInterface $form) {
$data = $form->getData();
// your logic to determine validation groups
}
in there you can check what has been submitted and return an array of validation groups, different for each type of entity you want.
You can find more info here
https://symfony.com/doc/current/form/data_based_validation.html

Related

What is the best way to handle drop down items in symfony form based on item status?

Let's consider a scenario like below:
Entity Car has Many To Many CarType which is a list of different types and each type can be active or inactive.
Car Form:
$builder->add('type', 'entity', array(
'label' => 'Car Type',
'class' => 'SomeBundle:CarType',
'query_builder' => function (EntityRepository $er) {
$qb = $er->createQueryBuilder('type')
->where('type.status = :status')
->setParameter('status', 'active')
;
return $qb;
}
));
In create phase, a user create a Car with an active Type. Assume I have an admin page to mark Type active/inactive.
What is the best way to handle the Car edit phase that has a relationship to an inactive Type? In other word if a Car has a relationship to any Type that was active at some point and not active any more, the choice would not show up in the dropdown.
What is the best way to handle a use case like this which dropdwon items can have different status in different time frame but query only returns the active ones?
One way would be to alert the user that Car is using some inactive Type but is there any other better way?
You ca deal with the problem in the admin level
(like you can't make inactive a carType if he's still used)
Or you deal it with user level:
You propose only choices of active car + the inactive current user choice.
This way the user can only choose to change to an active type of car or keep it's outdated type
$qb = $er->createQueryBuilder('type')
->where('type.status = :status')
->orWhere('id = :currentTypeId')
->setParameter('currentTypeId', $someId)
->setParameter('status', 'active')
;

Populate 3 selection lists using queries on the same entity, using Symfony2 buildForm()

I have an entity Machine which has a relation MM with other entity Piece. The pieces can be from 3 different types. Currently the form Machine is built with a selection list in which the whole array collection Machine.pieces is fetched. My idea is to build 3 different selection lists with a subset of Machine.pieces each.
I have tried two different approaches but I have no been able to accomplish it.
Use a MachineRepository class where a method
public function findPiecesByPieceType($pieceTypeID)
returns the proper query->getResult().
Then I add a choiceType in MachineType but I am not able to populate it from MachineController. I have used $form->get('pieces')->setData($arrcollectPieces) and other methods to add choices but I always get error.
How could I add choices from the controller to a Form?
In the form I use a queryBuilder
->add('pieces', EntityType::class, array(
'label' => 'label_pieces',
'class' => 'AppBundle\Entity\Piece',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('p')
->where('p.pieceType = :pieceType')
->setParameter('pieceType', 1);
},
)
)
this works but when I try to add more queryBuilders (->add('pieces2'... and so on) I have the error because
Neither the property pieces2 nor one of the methods getPiecess2(), pieces2(), isPieces2(), hasPieces2(), __get() exist and have public access in class AppBundle\Entity\Machine.
How can I use the various queryBuilders not bounded to a method name in that way?
Maybe both approaches are incorrect and I should solve this in a different way?
(Posted on behalf of the OP).
How to make it using 1.
In MachineController forget setData(), instead turn the arrayCollection into 2 arrays (arKeys, arValues) and send them to the form as the 3rd parameter in createForm().
$form = $this->createForm(<type>, <data>,
array ('p_keys' => array(...), 'p_values' => array(...)));
From MachineType.ConfigureOptions() we can fetch them
$resolver->setDefined(["p_keys",'p_values']);
and they will be available in MachineType.buildForm()
$options['p_keys'];
$options['p_values'];

Add custom validator on unmapped field, but with context of whole form submission?

tl;dr:
I need a custom validation constraint to run on an unmapped form field
I need the ID of the object the form is manipulating to eliminate it from consideration doing my validation constraint
Attaching the validation to the form itself or the unmapped field doesn't give me enough context to run my validation query
I have an unmapped field on my Person entity form that I need to run a validation on. I've followed this great article on how to do this, but my use case is slightly different and not entirely covered by the article.
I am making my own Unique Constraint that needs to run a custom query to determine the uniqueness. To run the query, I need access to the field value that was submitted, as well as the original Person object (so I can get it's ID if it's an update operation). Without also having the that Person object I won't be able to eliminate it from consideration during the uniqueness query.
If I apply the validator on the PersonType class like so:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver
->setDefaults(array(
'data_class' => 'CS\AcmeBundle\Entity\Person',
'constraints' => array(
new MyUniqueValidator(),
)
))
;
}
Then the validator gets passed the entire Person object to perform the validation on. This doesn't help me, because the submitted form data is not persisted to the Person object (it's an unmapped field that I handle after $form->isValid() is called in the controller).
If I apply the validator to the unmapped field directly instead:
$builder
->add('myUnmappedField', 'text', array(
'mapped' => false,
'constraints' => array(
new MyUniqueValidator(),
)
),
))
Then the object I get passed to the validator is just the standalone form text, and nothing else. I don't have the ID Person object (if it was an update operation) to perform by uniqueness query.
Hopefully I've explained this properly. Do I have any options to do this sort of validation gracefully?
You say you have unmapped field. Would it help, if you make it mapped to the Person entity? Just make a new property in the Person class with getter and setter methods, but not to the ORM, since you don't want it persisted.
If you do not want to polute your Person class, you can also make another composite class, which will hold your currently unmapped field and a Person object (you will then make it mapped). Ofcourse you will then set data_class to match the new object's namespace.
Both above solutions should work with the upper code you have there. Please let me know it it helped.
You can achieve this by using a callback constraint with a closure.
To access your Person entity, you will need to add the field via an event listener.
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$person = $event->getData();
$form->add('myUnmappedField', TextType::class, [
'mapped' => false,
'constraints' => [
new Symfony\Component\Validator\Constraints\Callback([
'callback' => function ($value, ExecutionContextInterface $context) use ($person) {
// Here you can use $person->getId()
// $value is the value of the unmapped field
}
])
],
]);
});

Symfony creates empty association when checkbox used on embedded form

I have a form for Person that has an embedded form for Address (bi-directional one to one relationship, with Address as the owning side w/ FK).
If a user submits the form, Symfony will initialize an empty Address object and assign it to the $address property on the $person object. Instead, I want Symfony to recognize all the form fields for Address were blank and it should NOT initialize an empty Address object.
Is this possible?
EDIT: I discovered that this only happens when I have a checkbox on the embedded form type. If there is no checkbox, Symfony will NOT create an empty association object. I think the problem is an unchecked checkbox is assumed to be a "false" value, so Symfony has no choice but to interpret that as a submitted value. Still looking for a reasonable workaround.
It's possible to do this with form events
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
Search for FORM_SUBMIT
However, I find it a bit convoluted.
Consider instead treating Person and Address as independent objects then setting the relation in your controller after checking Address. Something like:
$person = new Person();
$address = new Address();
$formData = array('person' => $person, 'address' => $address);
$builder = $this->formFactory->create('form',$formData);
$builder->add('person', new PersonFormType());
$builder->add('address', new AddressFormType());
...
if ($form->isValid()
{
if ($address was filled in properly)
{
$person->setAddress($address);
}
---
I find the above approach a bit easier to understand than going through the form event stuff.

What's the 'correct' way to include unrelated entities subset in a form?

Imagine three entities - Account, Address, Purchase. Account has a OneToMany relationship with Address. A Purchase is related to an Account, but not with an Address - it does however have a text field for the address (this is because addresses can change, I don't want the address directly related). On the users' account page they can add addresses. Whilst logged into the site, a Purchase id is stored in the session and used to retrieve the Purchase details when required.
What I want to do on the checkout page is display a list of all the addresses a user currently has in a <select>, allow them to pick one, and update the address in the current Purchase. $account->getAddresses() exists and will show the addresses relevant to the user.
I have read http://symfony.com/doc/current/reference/forms/types/collection.html and http://symfony.com/doc/current/cookbook/form/form_collections.html and can't see how to apply it to this situation, although an embedded form isn't really necessary - I don't want to change other details of the Purchase at that stage.
So my question is (at least one of): how do I pass the results of $account->getAddresses() to a form type? Or in the form type, should I use an entity field type, and if so, how do I get the custom query_builder to contain the current user in a form type? Or how else should I do this?
You need to pass the entity in to the Type's constructor and then use it to get the parameter.
Class YourType extends AbstractType
{
private $account;
public function __construct($account)
{
$this->account = $account;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$accountId = $account->getAccountId();
$builder->add('addressId',
'entity',
array('class' => 'YourBundle:Address',
'query_builder' => function(EntityRepository $er) use ($accountId) {
return $er->createQueryBuilder('a')
->where('a.accountId = ?1')
->setParameter(1, $accountId)));
}
}

Resources