Symfony - get an entity object in a field constraint class - php

I created my custom constraint validator:
class CustomConstraint extends Constraint
{
public $message = '';
}
class CustomConstraintValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
exit($this->context->getObject()); // returns null
}
}
In docs it is stated that:
Returns the currently validated object.
, but for me it returns NULL instead.
P.S. I do not want to assign this constraint to Entity, only to certain forms or fields.
My form property which is validated:
->add('rejectReasons', null, array(
'property' => 'name',
'multiple' => true,
'constraints' => array(
new CustomConstraint(array(
'message' => 'Application can not be refused.'
)),
)
));
Property in entity:
/**
* #ORM\ManyToMany(targetEntity="RejectReason")
* #ORM\JoinTable(name="relationship_application_reject_reasons",
* joinColumns={#ORM\JoinColumn(name="application_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="reject_reason_id", referencedColumnName="id")}
* )
*/
private $rejectReasons;
UPDATE
I tried putting constraint on other string property, I still get NULL.

Look at ExecutionContextInterface it says that:
getObject() Returns the currently validated object.
If the validator is currently validating a class constraint, the
object of that class is returned. If it is a validating a property or
getter constraint, the object that the property/getter belongs to is
returned.
In other cases, null is returned.
So as you can see, you have to assign to a class or a property or getter. Otherwise you will get null.

For those making form validation using dependencies itself this can help.
I assume that the Symfony version is 3.4 or 4.1 and you have symfony/form on your project.
Build your CustomConstraintValidator
The best way to deal with Symfony Form Validators with some kind of dependency are using CustomValidators
Above is a example that I use to work with them.
Supposed that we have an Entity like
// src/Entity/myEntity.php
namespace App\Entity;
...
class myEntity
{
private $id;
private $name; // string, required
private $canDrive; // bool, not required (default=false)
private $driveLicense; // string, not required (default = null)
public function __construct()
{
$this->canDrive = false;
}
// getters and setters
}
We don't need to populate $driveLicense (cause the attribute its not mandatory), but if $canDrivechange from false to true, now $driveLicense must have a value.
$driveLicense is $canDrive dependent.
To build a form for that and validate $driveLicense correctly on the FormType (the best practice) we need to build a CustomConstraintValidator.
Building CanDriveValidator
// src/Validator/Constraints/CanDrive.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class CanDrive extends Constraint
{
public $message = 'invalid_candrive_args'; // I like translators :D
}
Translator file - optional
//src/translators/validators.en.yaml //
invalid_candrive_args: When "{{ candrivelabel }} " field is checked you must fill "{{ drivelicenselabel }}"
The validator
// src/Validator/Constraints/CanDriveValidator.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CanDriveValidator extends ConstraintValidator
{
/**
* Checks if the passed value is valid.
*
* #param mixed $value The value that should be validated
* #param Constraint $constraint The constraint for the validation
*/
public function validate($value, Constraint $constraint)
{
$canDriveField = $this->context->getObject(); // the Field using this validator
$form = $canDriveField->getParent(); // the formType where the Field reside
$myEntity = $form->getData(); // The Entity mapped by formType
if ($myEntity->getCanDrive() == true && $myEntity->getDriveLicense() == null) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ candrivelabel }}', 'Can Drive')
->setParameter('{{ drivelicenselabel }}', 'Drive License')
->addViolation();
}
}
}
The form myEntityType
//src/Form/myEntityType.php
namespace App\Form;
use App\Entity\myEntity;
use App\Validator\Constraints\CanDrive;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class myEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('canDrive', CheckBoxType::class, [
'required' => false,
'constraints' => array(new canDrive()),
]
)
->add('driveLicense', TextType::class, ['required' => false])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => myEntity::class]);
}
}
Now, when use call isValid() method of myEntityType form and the canDrive field was checked and driveLicense is blank, a Violation will be fired on canDrive field. If canDrive is set to false (not checked, not submitted), nothing happens and form will be valid even when driveLicense is blank.

If you are develep a Class Constraint Validator remember to add the getTargets method as example:
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
As described here in the doc

The answer is simple. Write :
this->context->getRoot()->getData()
and u have the object.

Related

How could my custom Symfony ChoiceType allow extra items?

I'm working on a TagField for EasyAdmin 4 (and Symfony 6) that will rely on a TagType. This TagType will have the native ChoiceType as a parent.
This field will be rendered as a multiple select, with these attributes to allow adding tags on the fly:
[ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ]
To do so, I created a TagListener. Its main goal is to prefill the options with the already existing tags (on other entities) to support tag suggestion. After reading the docs and many articles, I chose to listen to the FormEvents::PRE_SET_DATA event.
Unfortunately there does not seem to be an easy way to "override" the default options, and we're left with having to override the entire field.
Here's what the TagListener looks like:
<?php
// src/Form/EventListener/TagListener.php
namespace eduMedia\TagBundle\Form\EventListener;
use eduMedia\TagBundle\Entity\TaggableInterface;
use eduMedia\TagBundle\Form\Type\TagType;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TagListener implements EventSubscriberInterface
{
public function __construct(private TagService $tagService)
{
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents(): array
{
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
];
}
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$parentForm = $event->getForm()->getParent();
/** #var TaggableInterface $taggable */
$taggable = $parentForm->getData();
// We retrieve the existing options to override some of them
$options = $form->getConfig()->getOptions();
// if ($options['pre_set_data_called']) {
// return;
// }
// We prefill options with the existing tags for this resource type
$allTagNames = $this->tagService->getTypeTagNames($taggable->getTaggableType());
// They are our new choices
$options['choices'] = array_combine($allTagNames, $allTagNames);
// We also need to select the entity's tags
$options['data'] = $this->tagService->loadTagging($taggable)->getTagNames($taggable);
// We override the form field
// $options['pre_set_data_called'] = true;
$parentForm->add($form->getName(), TagType::class, $options);
}
}
Doing so seems to create an infinite loop, where onPreSetData is called when calling $parentForm->add(). Is that normal? Is PRE_SET_DATA dispatched again when adding a field in a listener? Is there a way to prevent this from happening?
I tried adding a pre_set_data_called form option, setting it to true when calling $parentForm->add() and exiting the listener when it is indeed true. It kind of works, but then I get this error:
An exception has been thrown during the rendering of a template ("Field "tags" has already been rendered, save the result of previous render call to a variable and output that instead.").
How can I manage to allow extra items in my custom field type?
For reference, here is my TagType class:
<?php
namespace eduMedia\TagBundle\Form\Type;
use eduMedia\TagBundle\Form\EventListener\TagListener;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function __construct(private TagService $tagService)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new TagListener($this->tagService));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'mapped' => false,
'multiple' => true,
// 'pre_set_data_called' => false,
]);
}
public function getParent()
{
return ChoiceType::class;
}
}
And my TagField class:
<?php
namespace eduMedia\TagBundle\Admin\Field;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use eduMedia\TagBundle\Form\Type\TagType;
class TagField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null)
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TagType::class)
->setFormTypeOption('attr', [ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ])
->setTemplatePath('#eduMediaTag/fields/tag.html.twig')
;
}
}
I ended up not using the ChoiceType as the parent (<select> element), but rather the TextType (<input type=text> element), and splitting/exploding a simple string.
The actual bundle is live on GitHub and even though it might not be perfect (yet 😉), the implementation is way simpler and the end-user behaviour is exactly what I expected.

Symfony form unchanged field returns validation type error

Trying to update an entity, and submitting a field with a value that is unchanged results in a type error. What am I doing wrong?
Entity:
<?php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
...
class User implements UserInterface
{
...
/**
* #ORM\Column(type="bigint", nullable=true)
* #Groups({"default", "listing"})
* #Assert\Type("integer")
*/
private $recordQuota;
...
FormType:
<?php
namespace App\Form;
...
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('recordQuota', IntegerType::class)
;
}
...
}
Controller:
...
/**
* #Route("/api/user/{id}", name="editUser")
* #Method({"PUT", "PATCH"})
* #Rest\View()
*/
public function updateAction(Request $request, User $user)
{
$form = $this->createForm(UserType::class, $user);
$data = $request->request->get('user');
$clearMissing = $request->getMethod() != 'PATCH';
$form->submit($data, $clearMissing);
if ($form->isSubmitted() && $form->isValid()) {
...
I'm using PostMan to submit form data.
If the entity I am updating has a recordQuota of 1000, and I submit the form with a different value. It all works and updates.
But if I submit my form with recordQuota: 1000, which should leave the value unchanged I get an incorrect type error:
"recordQuota": {
"errors": [
"This value should be of type integer."
]
}
Additional info:
I am using $form->submit instead of handleRequest because I am using patch. So I need to be able to enable/disable $clearMissing. But even using handleRequest creates the same issue.
Even typecasting the recordQuota as int before passing it to the form still fails.
If I remove all of the type information from the Form and the Entity, I get "This value should be of type string" when actually making a change.
Edit: note that the following is true if field type is TextType, but IntegerType works fine with #Assert\Type("integer"). Which kinda renders my answer invalid/irrelevant...
You're using #Assert\Type("integer") annotation, but it means this:
value must be integer -- as a PHP type, like calling is_int($value)
and since data comes from form (and probably without any transformers, as I see in your code), it's type is string
and thus, validation always fails
What you need is #Assert\Type("numeric"):
it is equivalent of is_numeric($value)
down the line it will be converted to string when it reaches field of your entity
This was an issue with a combination of Symfony 4.3 validator auto_mapping described here:
https://symfony.com/blog/new-in-symfony-4-3-automatic-validation
And the maker bundle adding the wrong typecast to bigint fields.
See here:
https://github.com/symfony/maker-bundle/issues/429
The answer was to change the getters and setters in the entity from:
public function getRecordQuota(): ?int
{
return $this->recordQuota;
}
public function setRecordQuota(?int $recordQuota): self
{
$this->recordQuota = $recordQuota;
return $this;
}
to
public function getRecordQuota(): ?string
{
return $this->recordQuota;
}
public function setRecordQuota(?string $recordQuota): self
{
$this->recordQuota = $recordQuota;
return $this;
}
Alternatively, one can turn off auto_mapping in the validator config.

Symfony custom Constrait Validate for entity

I use symfony 2.8 and I have some entity and I need validate this entity by some condition
I create constrait ContainsInvValidator and call validate service in action and validate entity but when debugged I did not entered in ContainsInvValidator how to correct use custom validate ?
this is my ContainsInvValidator
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ContainsInvValidator extends ConstraintValidator
{
public function validate($entity, Constraint $constraint)
{
if (!$entity->getInvoiceNumber()) {
$this->context->buildViolation($constraint->message)
->atPath('foo')
->addViolation();
}
if (!$entity->getReference()) {
$this->context->buildViolation($constraint->message)
->atPath('foo')
->addViolation();
}
if (!$entity->getInvoiceDate()) {
$this->context->buildViolation($constraint->message)
->atPath('foo')
->addViolation();
}
}
}
and ContainsInv:
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class ContainsInv extends Constraint
{
public $message = 'The string "{{ string }}" no valid.';
}
add config:
services:
app.contains_test_check_validator:
class: AppBundle\Validator\Constraints\ContainsInv
tags:
- { name: validator.constraint_validator }
and my entity class for which I create custom validator
/**
* InboundInvoice
* #ContainsInv(groups={"test_check"})
*/
class InboundInvoice
{
and then in my action
public function handleInvoiceStatusAction(Request $request, InboundInvoice $invoice)
{
$resultHandling = $invoice->changedStatus();
$errors = $this->get('validator')->validate($invoice, [], ['test_check']);
and in variables errors I have
‌Symfony\Component\Validator\ConstraintViolationList::__set_state(array(
'violations' =>
array (
),
))
Your approach is valid for a property of a class. However if you want to validate the whole class (and not a single property), you have to overwrite the getTargets method in your ContainsInv Constraint class.
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
See also Class Constraint Validator.

How to update the mapping information of an entity

I would like to change the default mapping of the Product entity from the Sylius ProductBundle. So I created a listener to the ClassMetadata Event:
<?php
namespace App\Symfony\EventListener\Sylius;
use Sylius\Component\Product\Model\Product;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
/**
* Remove the reference of a Variant from its parent (Product) without delete it
*/
class ProductLoadMetadataSubscriber implements EventSubscriber
{
/**
* #return array
*/
public function getSubscribedEvents()
{
return array(
'loadClassMetadata',
);
}
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/** #var ClassMetadata $metadata */
$metadata = $eventArgs->getClassMetadata();
if (Product::class !== $metadata->name) {
return;
}
// Property "variants" in "Sylius\Component\Product\Model\Product" was already declared, but it must be declared only once
$metadata->mapOneToMany([
'fieldName' => 'variants',
'targetEntity' => 'Sylius\Component\Product\Model\VariantInterface',
'mappedBy' => 'object',
'orphanRemoval' => false
]);
//$variantsMapping = $metadata->getAssociationMapping('variants');
//$variantsMapping['orphanRemoval'] = false;
}
}
My goal is to set the orphanRemoval option value to false for the variants field.
But I didn't find a method or something else to update the ClassMetadataInfo instance.
At the moment I have this error:
Property "variants" in "Sylius\Component\Product\Model\Product" was
already declared, but it must be declared only once
It's logical because the variants field already exists.
EDIT:
I tried another thing but without success:
unset($metadata->associationMappings['variants']['orphanRemoval']);
// setAssociationOverride doesn't handle 'orphanRemoval' but it calls internally _validateAndCompleteOneToManyMapping
// which set orphanRemoval to false by default
$metadata->setAssociationOverride('variants', []);
To set orphanRemoval, you can try the following snippet:
if (SomeClass::class === $metadata->getName()) {
if (isset($metadata->associationMappings['itemShippingTrackings'])) {
$metadata->associationMappings['itemShippingTrackings']['orphanRemoval'] = false;
}
}

The form's view data is expected to be an instance of another class error creating form instance of another entity

I get the following error when trying to create a form from another entity to pass through to my view.
I have two entities in this context CourseGuide and CourseGuideRow and I would like to pass through a form view of CourseGuideRowType to my view - how can I do this?
The form's view data is expected to be an instance of class
CRMPicco\CourseBundle\Entity\CourseGuide, but is an instance of class
CRMPicco\CourseBundle\Entity\CourseGuideRow. You can avoid this error
by setting the "data_class" option to null or by adding a view
transformer that transforms an instance of class
CRMPicco\CourseBundle\Entity\CourseGuideRow to an instance of
CRMPicco\CourseBundle\Entity\CourseGuide.
This is my controller:
// CourseGuideController.php
public function viewAction(Request $request)
{
if (!$courseId = $request->get('id')) {
throw new NotFoundHttpException('No Course ID provided in ' . __METHOD__);
}
$resource = $this->get('crmpicco.repository.course_guide_row')->createNew();
$form = $this->getForm($resource);
// ...
}
My Symfony FormBuilder class:
// CourseGuideRowType.php
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\Form\FormBuilderInterface;
class CourseGuideRowType extends AbstractResourceType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('channel', 'crmpicco_channel_choice', array('data_class' => null))
->add('name', 'text')
->add('courses', 'text')
;
}
/**
* #return string name
*/
public function getName()
{
return 'crmpicco_course_guide_row';
}
}
I have tried the data_class => null suggestion mentioned elsewhere, but this has no effect.
If I pass through the data_class like this:
$form = $this->getForm($resource, array('data_class' => 'CRMPicco\CourseBundle\Entity\CourseGuideRow'));
I then get this:
Neither the property "translations" nor one of the methods
"getTranslations()", "translations()", "isTranslations()",
"hasTranslations()", "__get()" exist and have public access in class
"CRMPicco\CourseBundle\Entity\CourseGuideRow".
Why is this? There are translations attached to the CourseGuide entity but not the CourseGuideRow.
try to add this function in your FormType:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'YourBundle\Entity\YourEntity',
));
}
And don't forget the specific use:
use Symfony\Component\OptionsResolver\OptionsResolver;
EDIT
In native Symfony (with the Form component):
public function showAction()
{
/.../
$entity = new YourEntity();
$form = $this->createForm('name_of_your_form_type', $entity);
# And the response:
return $this->render('your_template.html.twig', ['form' => $form->createView()]);
}

Categories