I need a validator that needs to query database. This means I have to create a Constraint a ConstraintValidator and setup a service to inject EntityManager.
I did it, and researched official documentation and lots of posts and couldn't make it work. It seems that validatedBy() is not able to start the service (injecting then the EntityManager).
Here is the code I'm struggling with:
services.yml
validator.frontend.class:
class: Project\UsersBundle\Validation\Constraints\ConstrainsActiveValidator
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: validator.constrain_validator, alias: the_alias }
ConstrainsActive.php
namespace Project\UsersBundle\Validation\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
* */
class ConstrainsActive extends Constraint
{
public function validatedBy()
{
return 'the_alias'; // get_class($this).'Validator';
}
}
ConstrainsActiveValidator.php
namespace Project\UsersBundle\Validation\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManager;
class ConstrainsActiveValidator extends ConstraintValidator
{
protected $em;
public function __construct(EntityManager $v)
{
$this->em = $em;
}
public function validate($value, Constraint $constraint)
{
$this->context->buildViolation('This name sounds totally fake2!')
->atPath('useEmail')
->addViolation();
}
}
Update:
Thanks for the typo issue. It was a mistake when adapting code to posting here. I fixed! :)
Here you are the error I'm always getting:
Attempted to load class "the_alias" from the global namespace in /var/www/Project/current/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Validator/ConstraintValidatorFactory.php line 71. Did you forget a use statement for this class?
I temporally edited the file ConstraintValidatorFactory.php, and did a var_dump for the var $this->validators and get the response:
array (size=4)
'validator.expression' => string
'validator.expression' (length=20)
'Symfony\Component\Validator\Constraints\EmailValidator' => string
'validator.email' (length=15) 'security.validator.user_password' =>
string 'security.validator.user_password' (length=32)
'doctrine.orm.validator.unique' => string
'doctrine.orm.validator.unique' (length=29)
It seems like symfony framework didn't consider the alias "the_alias" set up in my custom service validator.frontend.class, because it didn't come in the var_dump.
I hope this can give you some clue. Thanks!
You code is ok, check the typo in the construct method where you name the EntityManager as $v and ferer it as $m.
Change the construct of ConstrainsActiveValidatorclass as:
public function __construct(EntityManager $v)
{
$this->em = $v;
}
What error do you have?
Just for people that has the same issue. I finally decided to set checking form parameters inside a type, I mean:
Bundle/Form/addType.php
adding an event listener to the $builder object:
public function buildForm(FormBuilderInterface $builder, array $options) {
...
$builder->addEventListener(FormEvents::POST_SUBMIT, function ($event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) return;
// Checking comes here!!!
// ......
}
}
In my opinion, validators are great but when a project gets bigger, checking information inside the event is the best way. You can also use some constraints that provide symfony as EmailConstraints:
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
Answering myself. Let's validate that an entered slug for a post in a blog is unique:
Let's assume we have a bundle called AppBundle
1) Create the subfolder structure "Validator/Contraints" inside src/AppBundle.
2) Inside .../Validator/Constraints, create a file called SlugUnique.php (The constraint):
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class SlugUnique extends Constraint
{
public $message = 'Slug "{{ slug }}" already taken';
public function getTargets()
{
return array(self::PROPERTY_CONSTRAINT);
}
public function validatedBy()
{
return 'app.validator.blog.slug_unique';
}
}
3) Create a file called SlugUniqueValidator.php at the same folder level (The constraint validator):
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManager;
use AppBundle\Entity\Blog as BlogEntity;
/**
* #Annotation
*/
class SlugUniqueValidator extends ConstraintValidator
{
/** #var EntityManager */
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function validate($value, Constraint $constraint)
{
/** #var BlogEntity $blogEntity */
$blogEntity = $this->context->getRoot()->getViewData();
if (null !== $this->em->getRepository('AppBundle:Blog')->findBy(['slug' => $blogEntity->slug])) {
$this->context->buildViolation($blogEntity->slug)
->atPath('slug')
->addViolation();
}
}
}
4) Create an entry into services.yml with properly tag items:
app.validator_constraints_blog.slug_unique_validator:
class:AppBundle\Validator\Constraints\SlugUniqueValidator
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: 'validator.constraint_validator', alias: 'app.validator.blog.slug_unique' }
Note that the value for the alias parameter must match the value returned by validatedBy in the SlugUnique class constraint.
5) Finally, assign such validation to the corresponding entity field:
namespace AppBundle\Entity;
use AppBundle\Validator\Constraints as ValidatorConstraints;
class Blog
{
...
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255, nullable=false)
* #ValidatorConstraints\SlugUnique
*/
private $slug;
public getSlug()
...
Notice that depending on the Entity fieldName and the form fieldName, that you may have to set the parameter 'property_path' when building the form type. Example:
$builder->add(
'slug', TextType::class,
array('required' => false, 'property_path' => 'slug')
);
Please note that you can inject a custom service into SlugUniqueValidator instead on the EntityManager for better separation of concerns and bests practices.
Related
i have a problem that make my head blow about eloquent went to the wrong table here my code for my activity model
class Activity extends Model{
use HasFactory;
protected $table = 'activities';
protected $guarded = ['id'];
public function getRouteKeyName()
{
return 'slug';
}
and the problem is when i store to the database, eloquent went to the wrong table
here is my store method
public function store(StoreActivityRequest $request)
{
$validatedData = $request->validate([
'name' => 'required',
'slug' => 'required|unique:activies'
]);
Activity::create($validatedData);
return redirect('/activities')->with('success', 'Tindakan Berhasil Ditambahkan');
}
this is my customrequest (actually my laravel make it by default)
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreActivityRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
//
];
}
}
here is the message
Illuminate\Database\QueryException
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'klinik_app.activies' doesn't exist (SQL: select count(*) as aggregate from `activies` where `slug` = konsultasi)
its go to activies table... if i change the tables on migrate file to activies, then the other method gone wrong because it's can't found activities. and this problem just happen to my store method, another method just do the right thing... is there any body can help me?
Update: Added from comments:
public function store(StoreActivityRequest $request) {
$validatedData = $request->validate([
'name' => 'required',
'slug' => 'required|unique:activies'
]);
Activity::create($validatedData);
return redirect('/activities')->with('success', 'Tindakan Berhasil Ditambahkan');
}
My goal: I built a custom constraint in SYMFONY, I needed to pass a variable to that constraint.
The context: The constraint do a check if a value is unique in the DB, if it is not, it raises a CONSTRAINT alert. That works alright when the FORM is used to create a new tuple in the DB but if it is an edit it raises an exception which should be bypass by checking that the value already existing, exists for the tuple Id being edited.
Hence I needed to pass the Id of the tuple being edited to my constraint check.
At first I implemented my custom constraint in my entity:
class MyEntity{
/**
* #MyBundleAssert\CheckValueAlreadyInDB(
* message = "already_exists_in_db",
* fieldToSearch = "my_value",
* tableToSearch = "my_table"
*)
*/
private myValue;
}
As one can see, I did not find a way to implement a way to pass a VARIABLE using the constraint with ANNOTATION. By searching, I understood I could do that by using the __construct() of my custom constraint class:
/**
* #Annotation
*/
class CheckValueAlreadyInDB extends Constraint{
public $message;
public $fieldToSearch;
public $tableToSearch;
public $idToCheck;
public $idToCheckFieldName;
public function __construct($options){
if(count($options)>0){
$this->idToCheck = $options['idToCheck'];
$this->idToCheckFieldName = $options['idToCheckFieldName'];
$this->fieldToSearch = $options['fieldToSearch'];
$this->tableToSearch = $options['tableToSearch'];
$this->message = $options['message'];
}
}
public function validatedBy()
{
return 'validator_check_value_already_in_db';
}
}
And, the ConstraintValidator extended class linked to it:
class CheckValueAlreadyInDBValidator extends ConstraintValidator
{
private $con;
public function __construct($con){
$this->con = $con;
}
public function validate($value, Constraint $constraint)
{
////My stuff to get a record from the DB////
$sel = new PdoSelect($this->con);
$search = $sel->returnRecordsInTableForSpecificKey([$constraint->fieldToSearch],[$value], $constraint->tableToSearch,false);
//////////////////////////////////////////////
$sameId = false;
if($constraint->idToCheck!==null){
$idToCheckInRetrieveRecord = $search->{$constraint->idToCheckFieldName};
$sameId = ($idToCheckInRetrieveRecord==$constraint->idToCheck)?true:false;
}
if($search!=null&&!$sameId){
$this->context->buildViolation($constraint->message)
->setParameter('%string%', $value)
->addViolation();
}
}
}
With service:
validator.unique.check_value_already_in_db:
class: MyBundle\Form\CustomConstraints\CheckValueAlreadyInDBValidator
arguments: ['#doctrine.dbal.default_connection']
tags:
- { name: validator.constraint_validator, alias: validator_check_value_already_in_db }
I my FORM (AbstractType extended class) for the field regarding myValue, I did edit the constraints attribute.
class MyEntityType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
....
$builder->add('myValue',****Type::class,array(
'constraints' => array(
new CheckValueAlreadyInDB(array(
'idToCheck'=>$options['data']->getId(),
'idToCheckFieldName'=>'id',
'fieldToSearch'=>'my_value',
'tableToSearch'=>'my_table',
'message' => "value_already_exists_in_db"))
)
));
...
}
}
I thought that the CONSTRAINT defined in the buildForm() would override the one defined in the * #MyBundleAssert\CheckValueAlreadyInDB(..) of MyEntity class (which should be the default behaviour). But It did not! I had to delete the ANNOTATION above MyEntity to make the constraint work as defined in the buildForm().
Does anyone know if there is a setting that could permit to have a constraint in a buildForm() overriding one existing as an ANNOTATION in MyEntity, but still let the ANNOTATION above a field in MyEntity be the default behavior? Or is there is a way to pass VARIABLE to ANNOTATIONS?
I found the solution.
My mistake was to try to use constraints in class MyEntityType extends AbstractType:
$builder->add('myValue',****Type::class,array(
'constraints' => array(
new CheckValueAlreadyInDB(array(
'idToCheck'=>$options['data']->getId(),
'idToCheckFieldName'=>'id',
'fieldToSearch'=>'my_value',
'tableToSearch'=>'my_table',
'message' => "value_already_exists_in_db"))
)
));
Update:
DON'T USE IT HERE
Have a look at class-constraint-validator section in the doc.
Implement the ConstraintValidator extended class above the class of the Entity where the validator has to execute its check and not above one attribute of the Entity class. That way one can have access to other attributes of the entity and use it as conditionals in the ConstraintValidator extended class.
I am new of Symfony, and I am trying to create a form bound to an Entity User.
One field of this entity is of type ArrayCollection. It is actually a OneToMany relationship with objects of another class.
So, a little bit of code just to be clearer.
class User
{
\\...
/**
* #ORM\OneToMany(targetEntity="UserGoods", mappedBy="users")
* #ORM\JoinColumn(name="goods", referencedColumnName="id")
*/
private $goods;
public function __construct()
{
$this->goods = new ArrayCollection();
}
\\...
}
And the associated class
class UserGoods
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="inserted_at", type="datetime")
*/
private $insertedAt;
/**
* #var float
*
* #ORM\Column(name="value", type="float")
*/
private $value;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="goods")
*/
protected $users;
}
Now, I want to create a FormBuilder that does something extremely simple, yet I couldn't figure it out how to do it by myself.
I want just a field of type number, and if an object of type Goods with the current date exists, modify it, otherwise add a new object to the collection.
This could be easily done inside the controller, but I have a lot of instances of this form, and this would make my program impossible to maintain.
Is there a way to add some post-processing of submitted data inside the form builder?
I already tried with DataTransformers but these won't suffice, as at most they would transform a number to a UserGoods object, and the original ArrayCollection would not be preserved (and what about doctrine associations?).
In addition, if I declare the field type as collection of number types, all the items inside the ArrayCollection would be displayed when rendering the form, not just the last one.
Any idea on how to get out of this?
Thank you in advance for your help.
As suggested, use Form Events. Inside the event you will check if the Goods with the submitted date already exist (load them from database) and your will modify them with the post data. If they dont exist, you will be creating new ones. You can also make another method in your entity, getLastItemsInCollection(), where you can use Criteria, to only load the last one from the database (recommended), or get the last item from original ArrayCollection. You can make a field unmapped, and map the Goods manually in the FormEvent, as described above. I hope that helps and I hope I understood correctly.
I followed Cerad and tomazahlin suggestions and I came up with a solution.
I am sure that every year at least 2 people over the world share my same problem, so I'll take some time to post my outcome.
Feel free to correct, criticize or add me, in the end I am a newbie of Symfony!
First, how I defined my two classes in the end.
class User
{
//...
/**
* #ORM\ManyToMany(targetEntity="UserGoods", inversedBy="users", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="goods", referencedColumnName="id")
*/
// Should have been a OneToMany relationship, but Doctrine requires the
// owner side to be on the Many side, and I need it on the One side.
// A ManyToMany relationship compensate this.
private $goods;
public function __construct()
{
$this->goods = new ArrayCollection();
}
//...
}
And the connected class
/**
* #ORM\HasLifecycleCallbacks()
**/
class UserGoods
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="inserted_at", type="datetime")
*/
private $insertedAt;
/**
* #var float
*
* #ORM\Column(name="value", type="float", nullable=true)
*/
// I do not want this field to be null, but in this way when
// persisting I can look for null elements and remove them
private $value;
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="goods")
*/
protected $users;
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
// This automatically sets InsertedAt value when inserting or
// updating an element.
public function setInsertedAtValue()
{
$date = new \DateTime();
$this->setInsertedAt( $date );
}
}
As I said, I wanted a FormBuilder to handle my array collection. The best form type for this purpose is... collection type.
This require a subform to be defined as its type.
<?php
namespace MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use MyBundle\Entity\UserGoods;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('goods', 'collection', array(
'type' => new GoodsdataWithDateType(),
'required' => false,
)
);
\\ ...
And the subform.
Since I need only the today's value to be displayed, and not all of them, I also need to add a FormEvent clause to check which items to insert.
namespace MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class GoodsdataWithDateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Here I add the event listener:
// Since I want only today's value to be displayed, I implement
// a check on this field of each element
$builder->addEventListener(
FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$goods = $event->getData();
$form = $event->getForm();
$datetime1 = $goods->getInsertedAt();
$datetime2 = new \DateTime();
$datetime2->setTime(0, 0, 0);
if ($datetime1 > $datetime2)
{
$form->add('value', 'number', array(
'required' => false,
));
// I am setting this value with LifecycleCallbacks, and I do not
// want the user to change it, I am adding it commented just for
// completeness
// $form->add('insertedAt', 'date', array(
// 'widget' => 'single_text',
// 'format' => 'yyyy,MM,dd',
// ));
}
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\UserGoods',
));
}
public function getName()
{
return 'goodsdatawithdate';
}
}
This works fine, but is displayed very badly when rendered with something like {{ form(form) }} in twig files.
To make it more user-friendly, I customized how the form was presented, in order to remove some garbage and include only the labels that were necessary.
So in my twig:
{{ form_start(form) }}
{{ form_errors(form) }}
<div>
{{ form_label(form.goods) }}
{{ form_errors(form.goods) }}
<br>
{% for field in form.goods %}
{{ form_widget(field) }}
{% endfor %}
</div>
{{ form_end(form) }}
This is nice so far, but I also want to include new elements in my collection, in particular if today's good has not been inserted yet.
I can do this inside my FormBuilder, by manually add a new item in the array before calling the $builder.
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$thisuser = $builder->getData();
// I added the following function inside the User class.
// I use a for loop to scroll all the associated Goods to get the
// latest one.
$mygoods = $thisuser->getLatestGoods();
if ( $mygoods && null !== $mygoods->getId() ) {
// The Array contains already some elements
$datetime1 = $mygoods->getInsertedAt();
$datetime2 = new \DateTime();
$datetime2->setTime(0, 0, 0);
// Check when was the last one inserted
if ($datetime1 < $datetime2) // Nice way to compare dates
{
// If it is older than today, add a new element to the array
$newgoods = new UserGoods();
$thisuser->addGoods($newgoods);
}
} else {
// The array is empty and I need to create the firs element
$newgoods = new UserGoods();
$thisuser->addGoods($newgoods);
}
$builder->add('goods', 'collection', array(
'type' => new GoodsdataWithDateType(),
'required' => false,
'allow_add' => true, // this enables the array to be
// populated with new elements
)
);
But I also want that if a user removes an inserted value (i.e., inserts nothing in the form), the associated array element should be removed.
Allowing the user to remove elements is a little bit trickyer. I cannot rely on 'allow_delete' property, since by working only with the last item in the collection, all the previous ones would be removed when the form is submitted.
I cannot rely on LifecycleCallbacks neither, because the changes made to relationships are not persisted in the database.
Thankfully to open source, I found a post here that helped me.
What I needed was an EventListener on Doctrine Flush operations.
namespace MyBundle\EventListener;
use Doctrine\ORM\Event\OnFlushEventArgs;
use MyBundle\Entity\UserGoods;
class EmptyValueListener
{
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$entities = array_merge(
$uow->getScheduledEntityInsertions(),
$uow->getScheduledEntityUpdates()
);
foreach ($entities as $entity) {
if ($entity instanceof UserGoods) {
if ($entity && null !== $entity )
{
if ( empty($entity->getValue()) )
{
$users = $entity->getUsers();
foreach ($users as $curruser)
{
$curruser->removeGoods($entity);
$em->remove($entity);
$md = $em->getClassMetadata('MyBundle\Entity\UserGoods');
$uow->computeChangeSet($md, $entity);
$em->persist($curruser);
$md = $em->getClassMetadata('MyBundle\Entity\User');
$uow->computeChangeSet($md, $curruser);
}
}
}
}
}
}
}
and registered it in my config.yml as
mybundle.emptyvalues_listener:
class: MyBundle\EventListener\EmptyValueListener
tags:
- { name: doctrine.event_listener, event: onFlush }
I have created form which requires data transformer, but got myself into single problem: I transform data by exploding string (string should be be exploded to 3 parts), everything works, if I supply correct format string, but otherwise it throws error inside data transformer, because transformation cannot occur if wrong string format is supplied (this is expected behavior).
So the question is is there a way to validate form field for correct string before data transformation? I know that data transformation by default occurs before validation, but maybe there's a way to do it other way around?
I found one solution that might work on this thread: Combine constraints and data transformers ,
but it's looks like rough solution, besides I need to translate validation message, and I would really like to do it using default translation methods for symfony forms (without using translation service)
I thought, and also someone from symfony IRC (Iltar) suggested do it by using events, but I'm not sure how to go about this - how to attach data transformer dynamically to form field? Or maybe there's other way?
It's maybe too late but I eventually manage to do it.
Maybe it will help you.
Here is my FormType:
class PersonType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('mother', 'personSelector', array('personEntity' => $options['personEntity']));
}
}
Here is my customField where are validations:
class PersonSelectorType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options){
$transformer = new PersonByFirstnameAndLastnameTransformer($this->entityManager,$options);
$builder->addModelTransformer($transformer);
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmitForm'));
}
public function onPreSubmitForm(FormEvent $event){
$mother = $event->getData();
$form = $event->getForm();
$options = $form->getConfig()->getOptions();
if (!empty($mother)){
preg_match('#(.*) (.*)#', $mother, $personInformations);
if (count($personInformations) != 3){
$form->addError(new FormError('[Format incorrect] Le format attendu est "Prénom Nom".'));
}else{
$person = $this->entityManager->getRepository($options['personEntity'])->findOneBy(array('firstname' => $personInformations[1],'lastname' =>$personInformations[2]));
if ($person === null) {
$form->addError(new FormError('Il n\'existe pas de person '.$personInformations[1].' '.$personInformations[2].'.'));
}
}
}
}
}
Here is my transformer:
class PersonByFirstnameAndLastnameTransformer implements DataTransformerInterface{
public function reverseTransform($firstnameAndLastname) {
if (empty($firstnameAndLastname)) { return null; }
preg_match('#(.*) (.*)#', $firstnameAndLastname, $personInformations);
$person = $this->entityManager->getRepository($this->options['personEntity'])->findOneBy(array('firstname' =>$personInformations[1],'lastname' =>$personInformations[2]));
if (count($personInformations) == 3){
$person = $this->entityManager->getRepository($this->options['personEntity'])->findOneBy(array('firstname' =>$personInformations[1],'lastname' =>$personInformations[2]));
}
return $person;
}
public function transform($person) {
if ($person === null) { return ''; }
return $person->getFirstname().' '.$person->getLastname();
}
}
Perhaps you could pass the instance of your form to your transformer. If the string doesn't parse correctly, simply add a validation error to the form, like so:
<?php
// src/Acme/MyBundle/Form/DataTransformer/StringTransformer.php
namespace Acme\MyBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\MyBundle\Entity\MyEntity;
use Acme\MyBundle\Entity\AnotherEntity;
use Acme\MyBundle\Type\MyEntityType;
class StringTransformer implements DataTransformerInterface
{
/**
* #var MyEntityType
*/
private $form;
/**
* #param ObjectManager $om
*/
public function __construct(MyEntityType $form)
{
$this->form = $form;
}
/**
* Transforms an object (entity) to a string (number).
*
* #param MyEntity|null $entity
* #return string
*/
public function transform($value)
{
// ...
}
/**
* Transforms a string (number) to an object (entity).
*
* #param string $number
*
* #return MyEntity|null
*
* #throws TransformationFailedException if object (entity) is not found.
*/
public function reverseTransform($value)
{
$collection = new ArrayCollection();
try{
$vals = explode(',', $value);
foreach($vals as $v){
$entity = new AnotherEntity();
$entity->setValue($v);
$collection->add($v);
}
} catch(\Exception $e){
$this->form
->get('my_location')
->addError(new FormError('error message'));
}
return $collection;
}
}
but it's looks like rough solution, besides I need to translate validation message, and I would really like to do it using default translation methods for symfony forms (without using translation service)
I know this question is old, but as any answer has been marked yet as the right solution, I share with you another approach.
emottet solution, using a presubmit listener to validate the data before the model transformer has been applied, is a good approach, based on this discussion.
If you want to keep using Symfony validation system for these errors too, you could use Symfony validator service (ValidatorInterface) in your pre-submit listener and pass it the required constraints, for example:
$builder
->add('whatever1', TextType::class)
->add('whatever2', TextType::class)
;
$builder->get('whatever1')
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
/** #var ConstraintViolationListInterface $errors */
if ($errors = $this->validator->validate($data, new Choice([
'choices' => $allowedChoices,
'message' => 'message.in.validators.locale.xlf'
]))) {
/** #var ConstraintViolationInterface $error */
foreach ($errors as $error) {
$form->addError(new FormError($error->getMessage()));
}
}
})
->addModelTransformer($myTransformer)
;
Kind of redundant, but it works. More info here.
I've got the following scenario: I'm validating appointments and there's a custom validator, which tells the user if his choosen date is valid or not. It's not valid, if the date is already blocked by another entity. This works flawlessly on adding new entities.
Now I'd like to trigger the date validation on edit only if the date itself has changed. So just changing the title of the appointment should not validate the date.
My entity class:
use Doctrine\ORM\Mapping as ORM;
use Acme\Bundle\Validator\Constraints as AcmeAssert;
/**
* Appointment
*
* #ORM\Entity
* #AcmeAssert\DateIsValid
*/
class Appointment
{
/**
* #ORM\Column(name="title", type="string", length=255)
*
* #var string
*/
protected $title;
/**
* #ORM\Column(name="date", type="date")
*
* #var \DateTime
*/
protected $date;
}
The validator class (used as a service):
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the date of an appointment.
*/
class DateIsValidValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($appointment, Constraint $constraint)
{
if (null === $date = $appointment->getDate()) {
return;
}
/* Do some magic to validate date */
if (!$valid) {
$this->context->addViolationAt('date', $constraint->message);
}
}
}
The corresponding Constraint class is set to target the entity class.
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class DateIsValid extends Constraint
{
public $message = 'The date is not valid!';
/**
* {#inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
/**
* {#inheritdoc}
*/
public function validatedBy()
{
return 'acme.validator.appointment.date';
}
}
Now I don't find a clean way to depend on a date change. I could simply track the old date in my entity, but that doesn't feel like a proper solution, if I'd like to implement more complex constraints. :[
Cheers
Since symfony 2.3 you can use Form Events to solve this problem. I added the change-check code to my FormType, by storing (and cloning) the original entity at the form creation.
Then added a POST_SUBMIT event listener to check if the fields were changed. The listener can add validation errors to your fields.
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Acme\Bundle\Entity\Appointment;
class AppointmentType extends AbstractType
{
private $originalAppointment;
public function __construct(Appointment $original)
{
// save the original entity
$this->originalAppointment = clone $original;
}
// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
// define your fields
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this, 'dateCheckListener']);
}
public function dateCheckListener(FormEvent $event)
{
$appointment = $event->getData();
$form = $event->getForm();
// if no appointments exist, we can skip the check
if (empty($appointment) || empty($this->originalAppointment)) {
return;
}
if ($appointment->getDate() !== $this->originalAppointment->getDate()) {
// the dates changed, you can call your validator here
if ('dates are not valid') {
$form->get('date')->addError(new FormError('We have a problem.'));
}
}
}
}
In your controller, you can create this formType with the original appointment:
$appointment = $this->getYourAppointmentSomehow();
$form = $this->createForm(new AppointmentType($appointment), $appointment);
Maybe you will find this article useful, to check which property is changed. Everything is possible in symfony. You might end up writing entity listeners, listener resolvers and so on. Things can get ultra advanced.
http://docs.doctrine-project.org/en/latest/reference/change-tracking-policies.html
Pay attention to the setter method:
public function setData($data)
{
if ($data != $this->data) {
$this->_onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
Do you see the trick?:)
I would also use !== operator to also check variable type.
You can also simplify things. You dont need to call _onPropertyChanged, but call the function, which will set a property 'dateChanged' to true. Then use method:
public function getGroupSequence()
{
if($this->dateChanged)
{
return ['date_check'];
}
else
{
return false;
}
}
And also tell your class that it implements GroupSequenceProviderInterface.
You can then use the validation group in your validation.yml for example.
maybe you want to try it with a preUpdate-Listener instead of a custom validation constraint?
Section 10.5.4 in the doctrine documentation gives an example of a validation listener "ValidCreditCardListener".
i know this will not work for automagic form validation, but i think it's the fastest way atm.
edit:
another option could be to use #UniqueEntiy constraint for the date field of your Appointment class. this will not break form validation but will cause an additional database query (as far as i know)