Symfony 4 Custom Constraint atPath not working - symfony

I have a form that has a date type and a checkbox type, the date type is only a required field when the checkbox is checked.
So the checkbox is called overrideDates and the date field is overrideDate
So i created a constraint like this:
<?php
namespace App\Validator\Constraints\Instruction;
use App\Validator\Validators\Instruction\MainInstructionValidator;
use Symfony\Component\Validator\Constraint;
/**
* Class MainInstructionConstraint
* #package App\Validator\Constraints\Instruction
* #Annotation
*/
class MainInstructionConstraint extends Constraint{
/**
* #var string
*/
public $overrideDatesError = "You Must Enter An Override Date";
/**
* #return string
*/
public function getTargets() : string{
return self::CLASS_CONSTRAINT;
}
/**
* #return string
*/
public function validatedBy() : string{
return MainInstructionValidator::class;
}
}
And a validator like this:
<?php
namespace App\Validator\Validators\Instruction;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Class MainInstructionValidator
* #package App\Validator\Validators\Instruction
*/
class MainInstructionValidator extends ConstraintValidator{
/**
* #param mixed $instruction
* #param Constraint $constraint
*/
public function validate($instruction, Constraint $constraint){
if($instruction->isOverridingDates()){
// make sure the override date is set
if(!is_null($instruction->getOverrideDate()) || !is_a($instruction->getOverrideDate(),'DateTime')){
$this->context->buildViolation($constraint->overrideDatesError)
->atPath('overrideDate')->addViolation();
}
}
}
}
The validation is working perfectly fine, and error message is coming in from the constraint fine, but for some reason it's not displaying on the form from the following:
form_errors(form.overrideDate)
I was under the impression that is what atPath() is for so i can tell it which form field to display the error on since i am passing the entire entity to the validator.

This is a little old now, but this works for me.
change
atPath('overrideDate')
to
atPath('[overrideDate]')
Craig

For me the problem was that I had error_bubbling turned on. If error_bubbling is turned on, it will override atPath and bubble the error up the form.

Related

FOSUserBundle not working Validation

I'm using symfony 3.3 and php 7.0 with the FOSUserBundle version 2.1.1 and I just realize that in the User Entity if you just add in the setters parameters the type hint like for example this... the validation will fail.
<?php
namespace AppBundle\Entity;
use ...;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #Assert\NotBlank()
* #var string
* #ORM\Column(type="string", nullable=false)
*/
private $firstName;
/**
* #return string
*/
public function getFirstName()
{
return $this->firstName;
}
/**
* #param string $firstName
*/
public function setFirstName(string $firstName) <- IF YOU ADD THIS STRING AS THE PARAMETER THE VALIDATION FAILS
{
$this->firstName = $firstName;
}
}
So finally the line should be like this next:
public function setFirstName($firstName)
If anyone knows how to add the typehint without giving problems to the validation will be nice the hear news.
This is how Symfony Validator works by default: it first sets the value (null for instance), then the validation is performed, not the other way. Because your method does not accept null values, only strings:
public function setFirstName(string $firstName)
Most probably, you are encountering error Exception: Argument 1 passed to setFirstName() must be of the type string, null given.
To overcome this you either have to set empty data for the corresponding field to '' or detach your entities from the form component. Or you can force method to accept null values:
// php 7
public function setFirstName(string $firstName = null)
{
$this->firstName = (string) $firstName;
}
// >= php7.1
public function setFirstName(?string $firstName)
I urge you to stop mixing entities with forms and validators. Your core domain should be free from such low level concerns (vide SRP from the SOLID). Also by the look of your setters I can tell you are moving towards the antipattern called Anemic Domain Model.
Apparently the FOSUserBundle does not allow the typehint at the moment if you want to use the validation functionality.
Your method setFirstName() in the User class must be compatible with FOSUserBundle/Model/UserInterface->setFirstName($username)
The interface has no typehint, so you can't add the typehint in your method...

Symfony using NotEqualTo validator with an object

According to Symfony docs it should be possible to use a NotEqualTo constraint with objects
value
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
I have the following entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\UniqueConstraint;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotEqualTo;
/**
* Class Template
* #ORM\Entity(repositoryClass="NoteRepository")
* #ORM\Table(uniqueConstraints={#UniqueConstraint(name="note_unique",columns={"from_id", "to_id"})})
*/
class Note
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #var $id int
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Template")
* #var $from Template
* #NotBlank()
* #NotEqualTo(value="$to")
*/
protected $from;
/**
* #ORM\ManyToOne(targetEntity="Template")
* #NotBlank()
* #var $to Template
*/
protected $to;
/**
* #ORM\Column(type="text")
* #var $notes string
*/
protected $notes;
}
I want to avoid getting $from == $to, how can I configure the constraint to use an instance of the class Template at validation time or more generrally how can I configure the constraint to use an object
Right now if a dump the values that the validator is receiving
class NotEqualToValidator extends AbstractComparisonValidator
{
/**
* {#inheritdoc}
*/
protected function compareValues($value1, $value2)
{
var_dump($value1);
var_dump($value2);
return $value1 != $value2;
}
}
I get
object(AppBundle\Entity\Template)
string '$to' (length=3)
Okay, despite Symfony docs states to be possible pass object to NotEqualTo Constraint it isn't true when using YML or Annotations.
So in order to dynamically unsure Note::$to is equals (or not equals) to Note::$from you could to use either Getters Constraint Targets or Callback Constraint.
However, there is a third option much easier (IMO): Expression Constraint
Expression Constraint approach
This constraint allows you to use an expression for more complex,
dynamic validation. See Basic Usage for an example. See Callback for a
different constraint that gives you similar flexibility.
Here is an example based on your question:
use Symfony\Component\Validator\Constraints as Assert;
/*
* Entity class...
*/
class Note
{
//...
/**
* #ORM\ManyToOne(targetEntity="Template")
* #var $from Template
* #Assert\NotBlank()
* #Assert\Expression('not (value == this.getTo())')
*/
protected $from;
//...
}
That is, just add #Assert\Expression('not (value == this.getTo())') to $from property.
Constraints Targets: Getters approach
use Symfony\Component\Validator\Constraints as Assert;
/*
* Entity class...
*/
class Note
{
//...
/**
* #Assert\isFalse(message='"from" cannot be equals to "to"')
* #return bool True if self::$from is not equals to self::$to
*/
function isFromAndToNotEqual()
{
return !($this->getFrom() != $this->getTo());
}
//...
}
Setting the property path validation:
Be aware taking this approach (Getters As Constraints Targets) and using validation with forms, it'll not show error messages next/closer to form field.
However, you always can set/config the error_mapping option (in Form type) in order to show the custom errors display next to a specific field, thus the solution would be something like this:
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('from')
->add('to')
// more fields...
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
//...
# it cause error message display next to
# "from" field (or whaterver field you which).
'error_mapping' => array(
'fromAndToNotEqual' => 'from',
),
));
}
}
Please note the current solutions is based on Symfony 2.7, but it
should be work nicely on other older versions (except some details for
Form class, etc).

Found the public method "add", but did not find a public "remove" in symfony2 entity

I get this exeption when I submit my form:
Found the public method "addRemote", but did not find a public "removeRemote" on class App\CoreBundle\Entity\Scene
The weired think is that the remove method exist ...
But i wrote it myself (When I did php app/console doctrine:generate:entities) doctrine didn't generated it. Did I make something wrong ?
/**
* #var array $remote
*
* #ORM\Column(name="remote", type="array", nullable=true)
*/
private $remote;
/**
* Set remote
*
* #param array $remote
* #return Scene
*/
public function addRemote($value, $key=null) {
if($key!=null){
$this->remote[$key] = $value;
}else{
$this->remote[] = $value;
}
return $this;
}
/**
* Remove remote
*/
public function removeRemote(){
unset($this->remote);
}
I allso tried:
/**
* Remove remote
*/
public function removeRemote($key=null){
if($key!=null && array_key_exists($key, $this->remote)){
unset($this->remote[$key]);
}
unset($this->remote);
return $this;
}
You have bigger problem than this; you are abusing your forms :)
Add.. and Remove... methods should be used for relations, not columns as per your code. Also, both add and remove methods must accept parameter that will be either added or removed.
If you still need an array, than getRemotes() method should return key=>value array. Adder and remover will later get that key, based on what user have picked in choice form type.

Optional embed form in Symfony 2

I have two entities in my system: Person and Phone as the following code.
class Person
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="string", length=100)
*/
private $lastName;
/**
* #ORM\OneToOne(targetEntity="Phone", cascade={"persist"})
*/
private $phone;
};
class Phone
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="PhoneType")
*/
private $type;
/**
* #ORM\Column(type="integer")
*/
private $countryCode;
/**
* #ORM\Column(type="integer")
*/
private $regionCode;
/**
* #ORM\Column(type="integer")
*/
private $number;
};
I also have a form to create and update a person (with the phone), so I have a PersonType that have a embed form representing a phone (PhoneType).
My problem is that a person can has optionally a phone, but if the person has a phone, all the phone fields are required. So, if user write nothing on all phone fields, this represent a person without phone and this case is valid. But if the user fills at least one form field, all other fields are required.
I try to take an approach by setting the phone on null if all phone fields are not filled, this was implemented on setPhone in Person entity. But having a null phone, Symfony tell me that all phone fields are required but are not filled.
I believe that Symfony will not validate the phone because I suppose that Symfony will apply the validation directly on person entity. Having a null phone, why tell me that all phone fields are not filled?
Is there a way to do what I want (preferably without modify all my controllers and form types, that is, at entity or validation component level)?
EDIT: Sorry, there is something not mentioned, if user fill a phone field, all phone fields need to be validated separately with different validators (to check if a field is a well formed number, to check correct length, etc). But if user leaves empty all phone fields, the per-field validation should be ignored.
Thanks!
I would try this method:
create additional method in your Phone entity which validates if all fields are null or all fields are not null (these two cases are correct) - and then return true. If some fields are null and some aren't return false. Add an assert annotation to your new method - this will be your new constraint.
/**
* #Assert\True(message = "Fill all fields or leave all them blank")
*/
And this should work.
For more information look here:
http://symfony.com/doc/master/book/validation.html#getters
edit:
Try this one:
define your custom validation method (this one which check if any of phone fields is filled) as Callback (at the top of the class):
* #Assert\Callback(methods={"checkPhoneFields"})
*/
class Phone {
Next you mark field wich have to be validated with validation group, eg.
/**
* #ORM\Column(type="string", length=16, groups={"phone_validation"})
*/
private $number;
And the last thing, in your custom constraint method you need to switch on "phone_validation" group if any of field isn't empty:
public function checkPhoneFields(ExecutionContext $context) {
if (/* fields are not empty */) {
$context->getGraphWalker()->walkReference($this, 'phone_validation', $context->getPropertyPath(), true);
}
And that should work.
For those passing by, with newer versions of Symfony you can do :
/**
*
* #Assert\Callback()
*/
public function validatePhone(ExecutionContextInterface $context)
{
if (/* Fields are not empty */)
{
$context->validate($this->getPhone(), 'phone', array("phone_validation"));
}
}
Do this:
First on your phone entity, declare the fields as "nullable" like this:
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $countryCode;
This will allow these fields to have a null value.
Then you need to declare a validator that will check if either none of the fields or all of them are filled
class Phone {
...
/**
* #Assert\True(message = "Phone must be empty of fill all fields")
*/
public function isPhoneOK()
{
// check if no field is null or all of them are
// return false if these conditions are not met
}
It is also possible to use a very simple data Transformer to solve this problem.
Create your Phone data transformer :
class PhoneTransformer implements DataTransformerInterface
{
public function transform($phone)
{
if (is_null($phone))
return new Phone();
return $phone;
}
public function reverseTransform($phone)
{
if (is_null($phone))
return null;
if (!$phone->getType() && !$phone->getCountryCode() /* ... Test if phone is not valid here */)
return null;
return $phone;
}
Then simply prepend this transformer in your PhoneType form :
class PhoneType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
/* Add fields here :
->add( ... )
*/
->prependNormTransformer(new PhoneTransformer())
;
}
}
See http://symfony.com/doc/2.0/cookbook/form/data_transformers.html for more details on how implement data transformers.
frankie567's answer was useful to me. For symfony >= 3.0:
/**
*
* #Assert\Callback()
*/
public function validatePhone(ExecutionContextInterface $context)
{
if (/* Fields are not empty */)
{
$validator = $context->getValidator();
$validator->inContext($context)
->atPath('phone')
// you can pass custom validation instead of null, like new Assert\Valid()
->validate($this->getPhone(), null, array('phone_validation'));
}
}

Symfony2: Setting entity value from parent

I am using FoSUserBundle. I Have a User entity. I need to set code property with the username during registration. How can i do this.
protected $code = parent::username;
Is this correct? I don't know much of symfony.
One way would be creating #PrePersist callback if I understand your problem correctly. E.g
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class User extends BaseUser
{
/**
* #ORM\PrePersist
*/
public function setCodeValue()
{
$this->code = $this->username;
}
}
For more info check here.

Resources