Symfony ValidatorComponent > AnnotationMapping in FormComponent - forms

Im working on a project where I'm using some Symfony Components. My problem is how to make the Form Component's validation of Forms use AnnotationMapping to find the constraints.
SetUp:
global $loader; //composer - autoload
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator();
$formFactory = Forms::createFormFactoryBuilder()
[...]
->addExtension(new ValidatorExtension($validator))
->getFormFactory();
Entity
/**
* #ORM\Entity
* #ORM\Table(name="..")
*/
class Conductor extends AbstractEntity {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #Assert\NotBlank()
* #ORM\Column(type="string")
*/
protected $pattern;
[...]
}
Building the Form
$builder = $App->getFormFactory()->createBuilder(FormType::class, $entity_data);
foreach ($fields as $field) {
$builder->add(
$field,
null,
[
"attr" => array("class" => "..."),
]
);
}
$builder->getForm();
FormSubmit / Validation
if($request->isMethod('POST')) {
$formTable = $this->createFormTable( array() );
$form = $formTable->buildForm($entity);
$form->submit($this->dataMapper->formDataFromPost());
/*
$entity = $this->dataMapper->mapFromPost();
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator();
*/
if($form->isValid()) {
[...]
} else {
[...]
}
}
Im trying to make the NotBlank() Constraints work. But my form passes the validation in any case. If I use a new validator and validate with it, it will show me the correct Errors. But the Form->isValid() function does not. Maybe it is not configured correctly to use AnnotationMapping? Thank you very much in advance for tipps or solutions!
Problem localization
The form handleRequest / submit and validation are working as expected!
The form does not have any constraints!!
-> Mapping the Constraints from Annotation is not happening / working.
I did find a similar question: Why does Symfony form not validate my DTO with constraint annotations?

I wasn't able to find a solution to enable the mapping that should happen inside the FormComponent with the ValidatorExtension.
But I did find a functional workaround. My approach is to get the Constraints from the readPropertyMetadata function of the validator:
use Symfony\Component\Validator\Validation;
public function buildForm(AbstractEntity $entity) {
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator();
$fields = [*ENTITY PRPERTIES*];
$classMeta = $validator->getMetadataFor($entity);
foreach ($fields as $field) {
$metadata = $classMeta->getPropertyMetadata($field);
if(is_array($metadata) && count($metadata) > 0) {
$constraints = $classMeta->getPropertyMetadata($field)[0]->constraints;
} else {
$constraints = [];
}
$builder->add(
$field,
null,
[
"attr" => array("class" => "..."),
"constraints" => $constraints
]
);
}
}
As now the constraints are added to the form the validation finally works as expected.

Related

Full text search in mongodb in Symfony 3.4

I'm trying to use full text search in mongodb:
db.Product.createIndex({"name": "text"})
db.Product.find({$text: {$search: "xxxxx"}})
how to use this in controller to symfony?
First of all create Product entity(adjust to your needs)
<?php
/**
* #Document
* #Index(keys={"name"="text"})
*/
class Product
{
/** #Id */
public $id;
/** #Field(type="string") */
public $name;
/** #Field(type="float") */
public $price;
}
Look at $name and #Index annotation
Then use query builder text() method
// Run a text search against the index
$qb = $dm->createQueryBuilder('Product')
->text('words you are looking for');
More info you can find here
The other way is to create native query with expr() from doctrine query builder
Thank you for all the answers. In summary, the controller for the search engine looks like the following:
class SearchController extends Controller
{
public function searchBarAction()
{
$form = $this->createFormBuilder(null)
->setMethod('GET')
->add('search', TextType::class)
->getForm();
return $this->render('AppBundle:Components:_searchBar.html.twig', [
'form' => $form->createView()
]);
}
/**
* #param Request $request
*/
public function handleSearchAction(Request $request)
{
$searchData = $request->query->get('form')['search'];
$dbName = 'ece';
$connection = $this->container->get('doctrine_mongodb')->getConnection();
$mongo = $connection->getMongo();
$db = $mongo->selectDB($dbName);
$resultSetProduct = $db->Product->find([
'$text' => ['$search' => $searchData]
]);
$resultSet = $db->MainData->find([
'$text' => ['$search' => $searchData]
]);
$itemProduct = $resultSetProduct->count();
$itemSet = $resultSet->count() + $itemProduct;
return $this->render('search/index.html.twig', [
'searchData' => $searchData,
'resultSetProduct' => $resultSetProduct,
'itemProduct' => $itemProduct,
'itemSet' => $itemSet,
'resultSet' => $resultSet
]);
}
}

Unit Test for FormErrorSerializer in Symfony 4 - always valid form

I am trying to write a unit test for FormErrorSerializer that converts Symfony $form->getErrors() to a readable array.
My current approach is to create the form, give it data, and look for validation errors, but form is always valid. I don't get any errors no matter what data I provide to form.
In normal REST request/response it is working well and I am getting appropriate error message. I need help with getting the error messages in unit test.
namespace App\Tests\Unit;
use App\Form\UserType;
use App\Serializer\FormErrorSerializer;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Translation\Translator;
class FormErrorSerializerTest extends TypeTestCase
{
/**
* ValidatorExtensionTrait needed for invalid_options
* https://github.com/symfony/symfony/issues/22593
*/
use ValidatorExtensionTrait;
public function testConvertFormToArray(){
$form_data = [
'email' => 'test',
'plainPassword' => [
'pass' => '1',
'pass2' => '2'
]
];
$translator = new Translator('de');
$form = $this->factory->create(UserType::class);
$form->submit($form_data);
if( $form->isValid() ) {
echo "Form is valid"; exit;
}
$formErrorSerializer = new FormErrorSerializer($translator);
$errors = $formErrorSerializer->convertFormToArray($form);
print_r($errors); exit;
}
}
Find below the Serializer:
namespace App\Serializer;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Serializes invalid Form instances.
*/
class FormErrorSerializer
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function convertFormToArray(FormInterface $data)
{
$form = $errors = [];
foreach ($data->getErrors() as $error) {
$errors[] = $this->getErrorMessage($error);
}
if ($errors) {
$form['errors'] = $errors;
}
$children = [];
foreach ($data->all() as $child) {
if ($child instanceof FormInterface) {
$children[$child->getName()] = $this->convertFormToArray($child);
}
}
if ($children) {
$form['children'] = $children;
}
return $form;
}
private function getErrorMessage(FormError $error)
{
if (null !== $error->getMessagePluralization()) {
return $this->translator->transChoice(
$error->getMessageTemplate(),
$error->getMessagePluralization(),
$error->getMessageParameters(),
'validators'
);
}
return $this->translator->trans($error->getMessageTemplate(), $error->getMessageParameters(), 'validators');
}
}
Ok, I was able to do this in 2 different ways.
First solution was to load the validator in getExtensions method. The factory in TypeTestCase doesn't bring the validator with it. So, not only you have to load the validator but you also have to explicitly specify the validations. You can specify validation using methods provided by symfony or you can directly point validator to the YAML or xml file if you are using one.
public function getExtensions()
{
$validator = (new ValidatorBuilder())
->addYamlMapping("path_to_validations.yaml")
->setConstraintValidatorFactory(new ConstraintValidatorFactory())
->getValidator();
$extensions[] = new CoreExtension();
$extensions[] = new ValidatorExtension($validator);
return $extensions;
}
However, I didn't use the above approach. I went with even better solution. Due to high complexity of my test case (as it needed multiple services), I went with a special container provided by Symfony's KernelTestCase. It provides private services in tests, and the factory it provides comes with validator and validations, just like you code in controller. You do not need to load validator explicitly. Find below my final test that extends KernelTestCase.
namespace App\Tests\Unit\Serializer;
use App\Entity\User;
use App\Form\UserType;
use App\Serializer\FormErrorSerializer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Translation\TranslatorInterface;
class FormErrorSerializerTest extends KernelTestCase
{
/**
* {#inheritDoc}
*/
protected function setUp()
{
$kernel = self::bootKernel();
}
public function testConvertFormToArray_invalidData(){
$form_data = [
'email' => 'test',
'plainPassword' => [
'pass' => '1111',
'pass2' => ''
]
];
$user = new User();
$user->setEmail($form_data['email']);
$user->setPlainPassword($form_data['plainPassword']['pass']);
$factory = self::$container->get(FormFactoryInterface::class);
/**
* #var FormInterface $form
*/
$form = $factory->create(UserType::class, $user);
$form->submit($form_data);
$this->assertTrue($form->isSubmitted());
$this->assertFalse($form->isValid());
$translator = self::$container->get(TranslatorInterface::class);
$formErrorSerializer = new FormErrorSerializer($translator);
$errors = $formErrorSerializer->convertFormToArray($form);
$this->assertArrayHasKey('errors', $errors['children']['email']);
$this->assertArrayHasKey('errors', $errors['children']['plainPassword']['children']['pass']);
}
public function testConvertFormToArray_validData(){
$form_data = [
'email' => 'test#example.com',
'plainPassword' => [
'pass' => 'somepassword#slkd12',
'pass2' => 'somepassword#slkd12'
]
];
$user = new User();
$user->setEmail($form_data['email']);
$user->setPlainPassword($form_data['plainPassword']['pass']);
$factory = self::$container->get(FormFactoryInterface::class);
/**
* #var FormInterface $form
*/
$form = $factory->create(UserType::class, $user);
$form->submit($form_data);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isValid());
$translator = self::$container->get(TranslatorInterface::class);
$formErrorSerializer = new FormErrorSerializer($translator);
$errors = $formErrorSerializer->convertFormToArray($form);
$this->assertArrayNotHasKey('errors', $errors['children']['email']);
$this->assertArrayNotHasKey('errors', $errors['children']['plainPassword']['children']['pass']);
}
}
Please note that Symfony 4.1 has a special container that allows fetching private services.
self::$kernel->getContainer(); is not special container. It will not fetch private services.
However, self::$container; is special container that provides private services in testing.
More about this here.

Symfony forms -- removing an optional one-to-one relationship in an embedded form

I'm trying to use Symfony Forms to edit an existing entity that has a one-to-one relationship with another entity that is optional. I want to make it so that if it receives nothing for the associated entity, it will delete the entity.
Here's the gist of the code I've written:
class AType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('BType', new BType(), ['required' => false]);
}
}
The A entity's association with B:
class A
{
/**
* #ORM\OneToOne(
* targetEntity="B",
* mappedBy="a",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
* #Assert\Valid()
*/
private $b;
....
When I try binding the form with the data, even though nothing was submitted for B, it'll make validation errors that B's fields are blank, because there is an existing B entity associated with A.
I've also looked at FormEvents, but I have not been able to find a way to fix the problem that way either -- example of what I've tried adding to AType's buildForm:
$builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
$a = $event->getData();
if($a->getType() != 'Package') {
$a->setB(null);
}
});
Even with this additional code, the errors regarding B are still appearing.
Update:
I think I've gotten closer, but though I can tell it's getting into the new code correctly, the B entity still doesn't ultimately get deleted, I think because despite the $a->setB(null), for $a = $form->getData(); in the controller, $a->getB(); still returns data (i.e. editing the entity in AType's POST_SUBMIT seems to only change that data locally?)
New Code:
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
if(!isset($data['B'])) {
$event->getForm()->remove('B');
}
});
$builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
$a = $event->getData();
if(!$event->getForm()->has('B') && $a->getB()) {
$a->setB(null);
}
});
I'm really at a loss because while $a->setB(null) does not perpetuate to the controller, if I create a property $a->test and set that in the same location, that does perpetuate to the controller.
Update 2
As per comments, I'm adding some code from Entity B and the controller:
class B
{
/**
* #var \A
*
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\OneToOne(targetEntity="A", inversedBy="b", cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="a_id", referencedColumnName="id")
* })
*/
private $a;
As for the controller, it gets more complicated as I'm actually dealing with a 2 deep relation: C hasMany A hasOne B. It's also not using form inputs, but rather formatted json. I'm using http://jmsyst.com/bundles/JMSSerializerBundle/master/installation to serialize the output back into json, as well as a helper function to serialize any error messages.
//Where $c is either a new C entity or an existing C entity to be edited
private function handleForm($c)
{
$json = $this->getJsonFromRequest();
if (false === $json) {
throw new \Exception('Invalid JSON');
}
$form = $this->createForm(new CType(), $c);
$em = $this->getDoctrine()->getEntityManager();
if($form->bind(json_decode($json, true)) && $form->isValid()) {
$c = $form->getData();
...
$em->persist($c);
$em->flush();
$response = new Response($this->getSerializer()->serialize($c, 'json'));
$response->headers->set('Content-Type', 'application/json');
return $response;
} else {
$response = array('code' => 'invalid', 'details' => $this->getErrorMessages($form));
return new JsonResponse($response, 400);
}
}
class C
{
/**
* #ORM\OneToMany(targetEntity="A", mappedBy="c", cascade={"persist", "remove"}, indexBy="id")
* #Assert\Valid()
* #Serializer\Expose
*/
protected $a;
A's relationship to C
/**
* #var \C
*
* #ORM\ManyToOne(targetEntity="C", inversedBy="a")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="c_id", referencedColumnName="id")
* })
* #Assert\NotBlank
*/
private $c;
And CType
class CType extends AbstractType
{
$builder->add(
'a',
'collection',
array(
'type' => new AType(),
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true
)
);

Checkbox field in symfony2

Good morning everyone! Is a form of.
Class ReleasesType:
$builder
->add('doid', 'text')
->add('dourl', 'text')
->add('artists', 'entity', array(
'class' => 'MReleaseCoreBundle:Artists',
'property' => 'name',
'expanded' => true ,
'multiple' => true
));
Сonnection with them one-to-many:
Class 'Artists':
/**
* #ORM\OneToMany(targetEntity="ReleasesArtists" , mappedBy="artists" , cascade={"all"})
* */
private $da;
public function __construct() {
$this->da = new \Doctrine\Common\Collections\ArrayCollection();
}
Class 'ReleasesArtists':
/**
* #ORM\ManyToOne(targetEntity="Releases", inversedBy="da")
* #ORM\JoinColumn(name="releases_id", referencedColumnName="id")
* */
private $releases;
/**
* #ORM\ManyToOne(targetEntity="Artists", inversedBy="da")
* #ORM\JoinColumn(name="artists_id", referencedColumnName="id")
* */
private $artists;
And of course the entity 'Releases':
/**
* #ORM\OneToMany(targetEntity="ReleasesArtists" , mappedBy="releases", cascade={"all"} , orphanRemoval=true)
*/
private $da;
public function getArtists() {
$artists = new ArrayCollection();
foreach($this->da as $p) {
$artists[] = $p->getArtists()->getName();
}
return $artists;
}
public function addDa($da) {
$this->da[] = $da;
}
public function setArtists($artists) {
foreach($artists as $p) {
$po = new \MRelease\CoreBundle\Entity\ReleasesArtists();
$po->setReleases($this);
$po->setArtists($p);
$this->addDa($po);
}
}
Connection is working correctly, all outputs. But does not "checked". In what may be the problem?
Thanks!
Into your controller, where you build and output your form, you have to do something like this
public function myFooAction(Request $request, $releasesId)
{
$repo = $this->getDoctrine()->getManager()->getRepository('YourBundleName:Releases');
$releasesObject = $repo->findOneById($releasesId);
$form = $this->createForm(new ReleasesType(), $releasesObject);
return $this->render('YourBundle::TemplateToRender, array('form'=>$form);
}
What happen here, and why is working?
I've made some assumptions as you don't provide any controller code. First of all, I assume that you have an action like myFooAction() where you do form operation and I suppose, also, that you pass to this action an id for load object from DB and tie it to your form - if I understood correctly your question.
So, first line of action is for retrieve repository for this object. Once you've got repo, you can fetch your object (second line). On third line I use Symfony2 form's facility and "connect" object to his form type: with this, all values contained into this object will be reported into your form (so checkboxes will have correct value). Last line is for render form.
Obviously, your action logic could be different but concept expressed here could be replicated with "different" implementation everywhere.

Testing symfony 2 forms

I develop new type, but I don't know how I can test it.
Assert annotation is not load and validations is not called.
Could any one please help me?
class BarcodeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->
add('price');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Bundles\MyBundle\Form\Model\Barcode',
'intention' => 'enable_barcode',
));
}
public function getName()
{
return 'enable_barcode';
}
}
A have following model for storing form data.
namepspace Bundles\MyBundle\Form\Model;
class Barcode
{
/**
* #Assert\Range(
* min = "100",
* max = "100000",
* minMessage = "...",
* maxMessage = "..."
* )
*/
public $price;
}
I develop some test like this, the form didn't get valid data but it is valid! (Because annotation is not applied)
I try adding ValidatorExtension but I dont know how can I set constructor paramaters
function test...()
{
$field = $this->factory->createNamed('name', 'barcode');
$field->bind(
array(
'price' => 'hello',
));
$data = $field->getData();
$this->assertTrue($field->isValid()); // Must not be valid
}
Not sure why you need to unit-test the form. Cant You unit test validation of Your entity and cover controller with your expected output?
While testing validation of entity You could use something like this:
public function testIncorrectValuesOfUsernameWhileCallingValidation()
{
$v = \Symfony\Component\Validator\ValidatorFactory::buildDefault();
$validator = $v->getValidator();
$not_valid = array(
'as', '1234567890_234567890_234567890_234567890_dadadwadwad231',
"tab\t", "newline\n",
"Iñtërnâtiônàlizætiøn hasn't happened to ", 'trśżź',
'semicolon;', 'quote"', 'tick\'', 'backtick`', 'percent%', 'plus+', 'space ', 'mich #l'
);
foreach ($not_valid as $key) {
$violations = $validator->validatePropertyValue("\Brillante\SampleBundle\Entity\User", "username", $key);
$this->assertGreaterThan(0, count($violations) ,"dissalow username to be ($key)");
}
}
Functional test. Given that you generate a CRUD with app/console doctrine:generate:crud with routing=/ss/barcode, and given that maxMessage="Too high" you can:
class BarcodeControllerTest extends WebTestCase
{
public function testValidator()
{
$client = static::createClient();
$crawler = $client->request('GET', '/ss/barcode/new');
$this->assertTrue(200 === $client->getResponse()->getStatusCode());
// Fill in the form and submit it
$form = $crawler->selectButton('Create')->form(array(
'ss_bundle_eavbundle_barcodetype[price]' => '12',
));
$client->submit($form);
$crawler = $client->followRedirect();
// Check data in the show view
$this->assertTrue($crawler->filter('td:contains("12")')->count() > 0);
// Edit the entity
$crawler = $client->click($crawler->selectLink('Edit')->link());
/* force validator response: */
$form = $crawler->selectButton('Edit')->form(array(
'ss_bundle_eavbundle_barcodetype[price]' => '1002',
));
$crawler = $client->submit($form);
// Check the element contains the maxMessage:
$this->assertTrue($crawler->filter('ul li:contains("Too high")')->count() > 0);
}
}
Include this line must be in Model and try it after include look like your model.
/* Include the required validators */
use Symfony\Component\Validator\Constraints as Assert;
namespace Bundles\MyBundle\Form\Model;
class Barcode
{
/**
* #Assert\Range(
* min = "100",
* max = "100000",
* minMessage = "min message here",
* maxMessage = "max message here"
* )
*/
public $price;
}