Symfony validation - php

I'm working in a bundle where the user creates a "comision" using a form, and I'm trying to check that the user still have "credit". So I created a custom validator that queries past comisions and throws an error if credit is not enough.
My problem is that if the user submits a date with a wrong format in the "date" field (i.e. 32-13-20122 24:05) Symfony still tries to run my custom validation and I get all sort of errors (because $comision->getDate() is null and not a valid DateTime object).
I'm getting this error:
clone method called on non-object
I can also check if value of $comision->getDate() is a valid datetime in my custom validator, but it seems to me that it should be not necessary since I added this rules in the date property.
This is my entity (simplified)
/**
* #MyValidation\TotalHours()
*/
class Comision
{
/**
* #ORM\Column(type="datetime")
* #Assert\DateTime()
* #Assert\NotNull()
*/
protected $date;
/**
* #ORM\Column(type="decimal", nullable=false, scale=1)
* #Assert\NotBlank()
*/
protected $hours;
...
My form class ...
class NewComisionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('date', DateTimeType::class, array(
'widget' => 'single_text',
'label' => 'Starting date and time',
'format' => 'dd/MM/yyyy HH:mm'
))
->add('hours', ChoiceType::class, array(
'label'=> 'How many hours',
'choices' => array(
'1:00' => 1,
'1:30' => 1.5,
'2:00' => 2,
'2:30' => 2.5,
'3:00' => 3
)
))
...
And my cutom validator that checks past comisions to find if user still has "credit"
public function validate($comision, Constraint $constraint)
{
$from = clone $comision->getDate();
$from->modify('first day of this month');
$to = clone $comision->getDate();
$to->modify('last day of this month');
$credit = $this->em->getRepository("ComisionsBundle:Comision")->comisionsByDate($comision,$from, $to);
...

One way would be to group your constraints as described in the docs.
This way your can define two groups of contraints whereas the second group will be validated only if all constraints in the first group are valid.
Regarding your use case, you could put your custom constraint in a different group than the default one to be sure your have a proper $comision DateTime object.

To do this, you can use the GroupSequence feature. In this case, an object defines a group sequence, which determines the order groups should be validated.
https://symfony.com/doc/current/validation/sequence_provider.html
The solution should look like this:
/**
* #MyValidation\TotalHours(groups={"Strict"})
* #Assert\GroupSequence({"Comision", "Strict"})
*/
class Comision
In this way, it will first validate all constraints in the group Comision (which is the same as the Default group). Only if all constraints in that group are valid, the second group, Strict, will be validated, making sure $comision->getDate() will have a DateTime instance.

IIRC data transformers run before validation, you can go with data transformer for your date:
https://symfony.com/doc/current/form/data_transformers.html
and then in data transformer check if the date is valid and if not throw an error.

Related

Validation NotBlank on a non nullable field

I'm trying to validate a form that includes a field that absolutely cannot be empty or null. So in my model, it is defined like this:
/**
* #var string
*/
private $end;
/**
* #param string $end
* #return Blabla
*/
public function setEnd(string $end): Blabla
{
$this->end = $end;
return $this;
}
Here is the validation of this field in my form:
$builder
->add('end', TextType::class, [
'label' => 'blabla',
'constraints' => [
new Length([
'min' => 3,
'minMessage' => 'Min {{ limit }} chars',
]),
new NotBlank([
'message' => 'not null blabla',
]),
],
])
Here is the error I receive when I send a wrong input per example "multiple spaces" :
Expected argument of type "string", "null" given at property path "end".
I can correct this error by adding the possibility to receive a null in my setter
/**
* #var string|null
*/
private $end;
/**
* #param string|null $end
* #return blabla
*/
public function setEnd(?string $end=null): blabla
{
$this->end = $end;
return $this;
}
But I don't find it very coherent to allow a field to receive null only to validate it and prevent to set the field with null value.
Can't we do otherwise?
When form is submitted, all empty values are set to null by default so you should either accept null in your setter or use empty_data to customise default value:
The model:
#[Assert\NotBlank]
private string $end = ''; // Remember to initialise the field with empty string if you use typed properties
public function setEnd(string $end): void
{
$this->end = $end;
}
Form:
$builder->add('end', TextType::class, [
'label' => 'blabla',
'empty_data' => '',
]);
Since NotBlank checks for empty string, you can still use it this way.
This works for some built-in fields like TextType but may still convert an empty string to null in other fields because DataTransformers are applied to empty_data too: docs
Form data transformers will still be applied to the empty_data value. This means that an empty string will be cast to null. Use a custom data transformer if you explicitly want to return the empty string.
If you want to costomize null handling in your form types, add a custom DataTransformer or implement DataTransformerInterface directly in your form type just like it's done in TextType: https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Form/Extension/Core/Type/TextType.php
Please note that I used some of modern PHP features (Attributes and typed properties) to make the code a bit shorter.

PUT not updating entity

I can't seem to update an entity when making a PUT request.
It works fine when I want to modify single elements belonging to the entity but not anymore as soon as I try to update an array or an object within it.
This works fine
{
"order_number": "TEST NAME"
}
As soon as an array is involved nothing seems to happen.
The quantities in the request are different from the one in the original entity.
{
"order_number": "TEST NAME",
"items": [
{
"id": 248,
"quantity": 0,
"quantity_shipped": 252
}
]
}
EDIT:
Foo and Items have a OneToMany relationship as shown in the code below.
class Foo implements FooInterface
{
/**
* One Foo has Many Items.
* #ORM\OneToMany(targetEntity="Item", mappedBy="foo", cascade={"persist","remove"})
* #JMSSerializer\Groups({"list_orders", "order_details"})
* #JMSSerializer\MaxDepth(1)
* #JMSSerializer\Expose
*/
private $items;
}
I also use a Form to validate the data in the request, and the Items are defined as a CollectionType as expected.
class FooType extends AbstractType implements FormTypeInterface
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('items',CollectionType::class, array(
'entry_type'=>ItemType::class,
'entry_options' =>array('label'=>false),
'allow_add' => true,
'by_reference' => false,
))
}
}
The same was done for the Items:
class SalesOrderLineItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('quantity',TextType::class,array(
'constraints' =>array(
new NotBlank())
)),
->add('quantityShipped',TextType::class,array(
'constraints' =>array(
new NotBlank())
))
->add('Foo',EntityType::class, array(
'class'=>'App:Api\Foo',
'constraints' =>array(
new Required(),
),
))
}
}
Does anyone know why this is happening?
Do I need to use a forEach loop to check each element?
I'm sure there's a more direct and efficient way of doing this.
Doctrine has hard time to detect changes on object and array, you need to reset the child object in order to update it again.
You can check how to reset the child here: How to force Doctrine to update array type fields?
If i understand you correctly you looking for cascade persist analog for updating. Doctrine hasn't such opportunities, so if you want update order item you need do this apart from the order, send PUT request to, for example, /api/order//items/ and then update your item.

Symfony3: Binding Form to Entity Classes

I'm creating a form using built-in Symfony3 services:
1) creating new class in AppBundle\Form which extends AbstractType
2) creating a form object in my controller (using createForm())
3) pushing that object directly to twig layer (by createView())
In my Entities direction I've got two classes, already mapped to database by ORM.
First one is User, and the second one is UserAttribute. User is related to UserAttribute by OneToMany annotation. Relationship looks like:
class UserAttr
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="userAttr" )
* #ORM\JoinColumn(nullable=false)
*/
private $user;
And from the User side:
class User
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="UserAttr", mappedBy="user")
* #ORM\JoinColumn(nullable=false)
*/
private $userAttr;
When I'm adding new fields (using $builder->add()) everything works fine if they are associated to User class properties. But if I'm doing the same with UserAttribute properties - symfony can't find get/set methods for that properties. I know - I can fix it by class User extends UserAttribute but probably that's not the point. Symfony must have another solution for that, probably I missed something.
Thanks for your time !
// SOLVED | there should be defined an EntityClassType as below:
$builder->add('credit',EntityType::class,array(
'class' => UserAttr::class
));
You have One-To-Many Association to UserAttr from User Entity. Hence, A User might have multiple credit.
OPTION 1 :
Considering this, you have to use a collection field type in UserFormType, which is a bit lengthy process.
$builder->add('userAttr', CollectionType::class, array(
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => UserAttrType::class
));
Then create another FormType : UserAttrType to represent UserAttr where you can have the credit field as a property from UserAttr.
$builder
->add('credit', TextType::class, array(
'label' => 'Credit',
))
This way, The form will load and submit accordingly, The credit value will also be updated when User form gets updated. Here is a link for collection docs. This is how to embed a collection form.
OPTION 2 :
But, If you want to simplify it further, add mapped = false (doc) to the credit field. This will ignore the current error. However, you have to manually collect the credit value from Form Object and set value to appropriate UserAttr Object in Submit Handler.
Hope it helps!

Save and get Date format in symfony2 via ORM

According to this http://symfony.com/doc/2.8/reference/forms/types/date.html#format I created this form type:
->add('warranty', DateType::class, array(
'widget' => 'single_text',
'format' => 'yyyy-MM-dd')
)
Here what i have in my entity
/**
* #ORM\Column(
* type="date"
* )
*
* #JMS\Groups("list")
* #Assert\NotBlank()
*
* #var \DateTime
*/
protected $warranty;
And I'm trying to save date like this 2012-12-12.
And this works fine, bun when I tried to get back data from DB i got this
2012-12-12T00:00:00+0200
I need work only with date, not datetime, can some one help me?
Doctrine maps date sql fields to an instance of \DateTime class which always has time stored (set to zero).
You can consider using the date twig filter:
http://twig.sensiolabs.org/doc/filters/date.html
Or if you have localised pages, the localised date filter using the intl extension:
http://twig.sensiolabs.org/doc/extensions/intl.html

Conditional validation of fields based on other field value in Symfony2

So here is the scenario: I have a radio button group. Based on their value, I should or shouldn't validate other three fields (are they blank, do they contain numbers, etc).
Can I pass all these values to a constraint somehow, and compare them there?
Or a callback directly in the controller is a better way to solve this?
Generally, what is the best practice in this case?
I suggest you to use a callback validator.
For example, in your entity class:
<?php
use Symfony\Component\Validator\Constraints as Assert;
/**
* #Assert\Callback(methods={"myValidation"})
*/
class Setting {
public function myValidation(ExecutionContextInterface $context)
{
if (
$this->getRadioSelection() == '1' // RADIO SELECT EXAMPLE
&&
( // CHECK OTHER PARAMS
$this->getFiled1() == null
)
)
{
$context->addViolation('mandatory params');
}
// put some other validation rule here
}
}
Otherwise you can build your own custom validator as described here.
Let me know you need more info.
Hope this helps.
You need to use validation groups. This allows you to validate an object against only some constraints on that class. More information can be found in the Symfony2 documentation http://symfony.com/doc/current/book/validation.html#validation-groups and also http://symfony.com/doc/current/book/forms.html#validation-groups
In the form, you can define a method called setDefaultOptions, that should look something like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// some other code here ...
$builder->add('SOME_FIELD', 'password', array(
'constraints' => array(
new NotBlank(array(
'message' => 'Password is required',
'groups' => array('SOME_OTHER_VALIDATION_GROUP'),
)),
)
))
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$groups = array('Default');
$data = $form->getData();
if ($data['SOME_OTHER_FIELD']) { // then we want password to be required
$groups[] = 'SOME_OTHER_VALIDATION_GROUP';
}
return $groups;
}
));
}
The following link provides a detailed example of how you can make use them http://web.archive.org/web/20161119202935/http://marcjuch.li:80/blog/2013/04/21/how-to-use-validation-groups-in-symfony/.
Hope this helps!
For anyone that may still care, whilst a callback validator is perfectly acceptable for simpler dependencies an expression validator is shorter to implement.
For example if you've got a field called "Want a drink?" then if yes (true) "How many?" (integer), you could simplify this with:
/**
* #var bool|null $wantDrink
* #ORM\Column(name="want_drink", type="boolean", nullable=true)
* #Assert\NotNull()
* #Assert\Type(type="boolean")
*/
private $wantDrink;
/**
* #var int|null $howManyDrinks
* #ORM\Column(name="how_many_drinks", type="integer", nullable=true)
* #Assert\Type(type="int")
* #Assert\Expression(
* "true !== this.getWantDrink() or (null !== this.getHowManyDrinks() and this.getHowManyDrinks() >= 1)",
* message="This value should not be null."
* )
*/
private $howManyDrinks;
You write the expression in PASS context, so the above is saying that $howManyDrinks must be a non-null integer at least 1 if $wantDrink is true, otherwise we don't care about $howManyDrinks. Make use of the expression syntax, which is sufficient for a broad range of scenarios.
Another scenario I find myself frequently using a expression validator are when I've got two fields to the effect of "date start" and "date end", so that they can each ensure that they are the right way around (so that the start date is before or equal to the end date and the end date is greater or equal to the start date).

Categories