Building Symfony 2 Custom Validator that Uses Multiple Fields - symfony

I'm building a custom validator that needs to validate the value from TWO form fields in the db in order to get this constraint to pass.
My question is this: the ContractValidator's validate method only has one $value in it's signature so how do I get access to the values from more than just a single field to do the validation?
Here is a typical custom validator:
namespace Acme\WebsiteBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class MyCustomValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
// check $value and return an error
// but in my case, i want the value from more than one form field to do a validation
// why? i'm checking that two pieces of information (ssn + dob year) match
// the account the user is registering for
}
}
Here's an example of a form class with some validations set:
namespace ACME\WebsiteBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
use ACME\WebsiteBundle\Validator\Constraints\UsernameAvailable;
class AccountRegistration extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('ssn', 'number', array(
'max_length' => 9,
'required' => true,
'error_bubbling' => true)
);
$builder->add('year_of_birth', 'choice', array(
'choices' => range(date("Y") - 100, date("Y")),
'required' => true,
'empty_value' => 'Select ...',
'label' => 'Year of Birth',
'error_bubbling' => true)
);
$builder->add('username', 'text', array(
'required' => true,
'error_bubbling' => true)
);
$builder->add('password', 'password', array(
'max_length' => 25,
'required' => true,
'error_bubbling' => true)
);
$builder->add('security_question', 'choice', array(
'empty_value' => 'Select ...',
'choices' => array(),
'label' => 'Security Question',
'required' => true,
'error_bubbling' => true)
);
$builder->add('security_question_answer', 'text', array(
'label' => 'Answer',
'required' => true,
'error_bubbling' => true)
);
}
public function getName()
{
return 'account_registration';
}
public function getDefaultOptions(array $options)
{
$collectionConstraint = new Collection(array(
'allowExtraFields' => true,
'fields' => array(
'ssn' => array(new MinLength(array('limit' => 9, 'message' => 'too short.')), new NotBlank()),
'year_of_birth' => array(new NotBlank()),
'username' => array(new NotBlank(), new UsernameAvailable()),
'password' => array(new NotBlank(), new Regex(array(
'message' => 'password must be min 8 chars, contain at least 1 digit',
'pattern' => "((?=.*\d)(?=.*[a-z]).{8,25})"))
),
'security_question' => array(new NotBlank()),
'security_question_answer' => array(new NotBlank()))
)
);
return array(
'csrf_protection' => true,
'csrf_field_name' => '_token',
'intention' => 'account_registration',
'validation_constraint' => $collectionConstraint
);
}
}

Any custom validator that extends ConstraintValidator has access to the $context property. $context is an instance of ExecutionContext that gives you access to submitted data:
Example:
<?php
namespace My\Bundle\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class AppointorRoleValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$values = $this->context->getRoot()->getData();
/* ... */
}
}

You need to use CLASS_CONSTRAINT as described in the cookbooks. You then get passed the whole class and can use any property of this class. In your example above, this would mean that instead of the $value beeing one string/integer, it would be the whole object you want to validate.
The only thing you need to change is getTargets() functions, which has to return self::CLASS_CONSTRAINT.
Also make sure that you define your validator on class level, not on property level. If you use annotations this means that the validator must be described above the class defnition, not above one specific attribute definition:
/**
* #MyValidator\SomeValidator
*/
class MyClass {
}

Related

Field with query should not be blank

How do I add validation constraints "Field is required" in symfony2 PostType class? Any suggestions? I am new in SF2 and I'm just editing what has been done by previous developer.
use Symfony\Component\Validator\Constraints\NotBlank;
class BlogPostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$blogPostCategories = BlogPostCategoryQuery::create()
->filterByBlogPost($builder->getData())
->find();
$categoryIds = array();
foreach ($blogPostCategories as $blogPostCategory) {
$categoryIds[] = $blogPostCategory->getCategory()->getId();
}
$queryOptions = array(
'option_status' => Categorypeer::STATUS_ACTIVE,
'option_category_ids' => $categoryIds
);
$categories = CategoryQuery::create()
->filterActiveCategoriesByOptions($queryOptions)
->find();
$builder->add('category_ids', 'model', array(
'label' => 'Category',
'mapped' => false,
'class' => 'Deal\MainBundle\Model\Category',
'query' => CategoryQuery::create()
->filterActiveCategoriesByOptions()
->orderByName(),
'property' => 'name',
'empty_value' => 'Select categories',
'empty_data' => null,
'required' => true,
'multiple' => true,
'data' => $categories,
'constraints' => array(
new NotBlank(array(
'message' => 'Your message can not be blank! Ouch!'
)),
)
));
Thank you
You should set required => true for this type from the parent form when calling (that's the form type that uses your BlogPostType):
$formBuilder->add('whatever', BlogPostType::class, ['required' => true]);
Also, you could set required = true by default for BlogPostType:
class BlogPostType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'required' => true
]);
}
}
You can do it with constraints key. Like this:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('message', TextareaType::class, array(
'required' => true,
'constraints' => array(
new NotBlank(array(
'message' => 'Your message can not be blank! Ouch!'
)),
)
))
;
}
do not forget to add uses:
use Symfony\Component\Validator\Constraints\NotBlank;

stuck on symfony2 for contact form

I have problem with a contact form in symfony2 here is the code what i've done and what error do i get
<?php
// src/Aleksandar/IntelMarketingBundle/Resources/views/ContactType.php
namespace Aleksandar\IntelMarketingBundle\Resources\views;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Collection;
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text', array(
'attr' => array(
'placeholder' => 'What\'s your name?',
'pattern' => '.{2,}' //minlength
)
))
->add('email', 'email', array(
'attr' => array(
'placeholder' => 'So I can get back to you.'
)
))
->add('subject', 'text', array(
'attr' => array(
'placeholder' => 'The subject of your message.',
'pattern' => '.{3,}' //minlength
)
))
->add('message', 'textarea', array(
'attr' => array(
'cols' => 20,
'rows' => 2,
'placeholder' => 'And your message to me...'
)
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$collectionConstraint = new Collection(array(
'name' => array(
new NotBlank(array('message' => 'Name should not be blank.')),
new Length(array('min' => 2))
),
'email' => array(
new NotBlank(array('message' => 'Email should not be blank.')),
new Email(array('message' => 'Invalid email address.'))
),
'subject' => array(
new NotBlank(array('message' => 'Subject should not be blank.')),
new Length(array('min' => 3))
),
'message' => array(
new NotBlank(array('message' => 'Message should not be blank.')),
new Length(array('min' => 5))
)
));
$resolver->setDefaults(array(
'constraints' => $collectionConstraint
));
}
public function getName()
{
return 'contact';
}
}
?>
This is the code for the contact form which will be rendered in the view
no here is the code from my controller
<?php
namespace Aleksandar\IntelMarketingBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
/**
* #Route("/contact", _name="contact")
* #Template()
*/
public function contactAction()
{
$form = $this->createForm(new ContactType());
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$message = \Swift_Message::newInstance()
->setSubject($form->get('subject')->getData())
->setFrom($form->get('email')->getData())
->setTo('info#intelmarketing.es')
->setBody(
$this->renderView(
'AleksandarIntelMarketingBundle::contact.html.php',
array(
'ip' => $request->getClientIp(),
'name' => $form->get('name')->getData(),
'message' => $form->get('message')->getData()
)
)
);
$this->get('mailer')->send($message);
$request->getSession()->getFlashBag()->add('success', 'Your email has been sent! Thanks!');
return $this->redirect($this->generateUrl('contact'));
}
}
return array(
'form' => $form->createView()
);
}
}
and here is the rooting
aleksandar_intel_marketing_contactpage:
pattern: /contact
defaults: { _controller: AleksandarIntelMarketingBundle:Default:contact }
now when i try to open the page its says the fallowing:
"[Semantical Error] The annotation "#Route" in method
Aleksandar\IntelMarketingBundle\Controller\DefaultController::contactAction()
was never imported. Did you maybe forget to add a "use" statement for
this annotation? 500 Internal Server Error - AnnotationException "
If any one knows what might be the problem please let me know
As the error message states, you are missing a use statement on top of your controller file.
Simply add:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
on top of your class DefaultController.
You can then replace your routing with:
aleksandar_intel_marketing:
resource: "#AleksandarIntelMarketingBundle/Controller/DefaultController.php"
type: annotation
This way, you are using the #Route annotation instead of the default yml way to declare your routes.
http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html

add class to subset of symfony checkboxes

I've made a form in Symfony that is not bound to an entity. One of the form elements is a set of checkboxes, fieldtypes. Which fieldtype checkboxes show should depend on the value of the searchby field. I want to add a class to each checkbox to use as a show/hide hook for the javascript I will add later. But so far as I can see, the choices form element only allows for an overall class to be applied.
Here's the relevant bit of my form:
$form = $this->createFormBuilder()
->add('searchby', 'choice', array(
'label' => 'browse.label.searchby',
'choices'=>array(
'name' => 'browse.searchby.name',
'location' => 'browse.searchby.location',
'classification' => 'browse.searchby.classification'
),
'required' => true,
'multiple' => false,
'expanded' => true,
))
->add('fieldtypes', 'choice', array(
'label' => 'browse.label.fieldtypes',
'choices' => array(
'extensionAttribute12' => 'person.label.position.fr',
'title' => 'person.label.position.eng',
'l' => 'person.label.city',
'st' => 'person.label.province',
'co' => 'person.label.country',
'givenname' => 'person.label.firstname',
'sn' => 'person.label.lastname',
'name' => 'person.label.fullname',
),
'required' => true,
'multiple' => true,
'expanded' => true
));
If I want to add the class 'searchbyname' to the radiobuttons created from $options['choices']['givenname'], $options['choices']['sn'], $options['choices']['name'], how would I go about it?
After seeing that choices could be declared like this:
'choices' => array(
'classification' => array(
'extensionAttribute12' => 'person.label.position.fr',
'title' => 'person.label.position.eng',
),
'location' => array(
'l' => 'person.label.city',
'st' => 'person.label.province',
'co' => 'person.label.country',
),
'name' => array(
'givenname' => 'person.label.firstname',
'sn' => 'person.label.lastname',
'name' => 'person.label.fullname',
)
),
where the key to each sub array would be used as an <optgroup> label in a <select>; and after attempting to modify the Twig template (which was every bit a painful as #Cerad said it would be), I tried extending the ChoiceType class by creating a form type extension.
My solution is inefficient since I'm modifying the child views after they are created and because I had to include all the code from ChoiceType::finishView. I can't see how a child view is created. There is a line in ChoiceType::buildForm that reads $remainingViews = $options['choice_list']->getRemainingViews();, but since $options['choices'] was input as an array, I don't know what class getRemainingViews() is being called from.
At any rate, here it is:
<?php
namespace Expertise\DefaultBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ChoiceTypeExtension extends AbstractTypeExtension
{
/**
* #return string The name of the type being extended
*/
public function getExtendedType()
{
return 'choice';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setOptional(array('optgroup_as_class'));
$resolver->setDefaults(array('optgroup_as_class'=>false));
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($options['expanded']) {
// Radio buttons should have the same name as the parent
$childName = $view->vars['full_name'];
// Checkboxes should append "[]" to allow multiple selection
if ($options['multiple']) {
$childName .= '[]';
}
foreach ($view as $childView) {
$childView->vars['full_name'] = $childName;
if($options['optgroup_as_class']){
foreach($options['choices'] as $optclass => $choices){
if(!is_array($choices)) continue;
foreach($choices as $value => $label){
if($childView->vars['value'] == $value && $childView->vars['label'] == $label) {
$childView->vars['attr']['class'] = $optclass;
break 2;
}
}
}
}
}
}
}
}
Add it as a service, use the "optgroup" choices format and set optgroup_as_class to true.
I'd love to see a more efficient method.

Symfony2 choice field not working

I asked a question here How to use Repository custom functions in a FormType but nobody anwsered, so i did a little digging and advanced a little but i still get this error:
Notice: Object of class Proxies\__CG__\Kpr\CentarZdravljaBundle\Entity\Category
could not be converted to int in /home/kprhr/public_html/CZ_Symfony/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php line 457
Now this is how my CategoryType looks like:
<?php
namespace Kpr\CentarZdravljaBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Bridge\Doctrine\RegistryInterface;
class CategoryType extends AbstractType
{
private $doctrine;
public function __construct(RegistryInterface $doctrine)
{
$this->doctrine = $doctrine;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Kpr\CentarZdravljaBundle\Entity\Category',
'catID' => null,
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$someId = $builder->getData()->getId();
$param = ($someId) ? $someId : 0;
$catID = $options['catID'];
$builder->add('name', 'text', array('attr' => array('class' => 'span6')));
$builder->add('file', 'file', array('image_path' => 'webPath', 'required' => false));
$builder->add('parent', 'choice', array(
'choices' => $this->getAllChildren($catID),
'required' => false,
'attr' => array('data-placeholder' => '--Izaberite Opciju--'),
));
$builder->add('tags', 'tag_selector', array(
'required' => false,
));
$builder->add('status', 'choice', array(
'choices' => array('1' => 'Aktivna', '0' => 'Neaktivna'),
'required' => true,
));
$builder->add('queue', 'text', array('attr' => array('class' => 'span3')));
}
private function getAllChildren($catID)
{
$choices = array();
$children = $this->doctrine->getRepository('KprCentarZdravljaBundle:Category')->findByParenting($catID);
foreach ($children as $child) {
$choices[$child->getId()] = $child->getName();
}
return $choices;
}
public function getName()
{
return 'category';
}
}
I am accessing the CategoryRepository function findByParenting($parent) from the CategoryType and I am getting the array populated with accurate data back from the function getAllChildren($catID) but the error is there, i think that Symfony framework is expecting an entity field instead of choice field, but dont know how to fix it.
I also changet the formCreate call in the controller giving $this->getDoctrine() as an argument to CategoryType():
$form = $this->createForm(new CategoryType($this->getDoctrine()), $cat, array('catID' => $id));
Ok i managed to resolve the dilemma. The answer was easy all I had to do is change
$builder->add('parent', 'choice', array(
'choices' => $this->getAllChildren($catID),
'required' => false,
'attr' => array('data-placeholder' => '--Izaberite Opciju--'),
));
to this:
$builder->add('parent', 'entity', array(
'class' => 'KprCentarZdravljaBundle:Category',
'choices' => $this->getAllChildren($catID),
'property' => 'name',
'required' => false,
'attr' => array('data-placeholder' => '--Izaberite Opciju--'),
));
And change the getAllChildren(..) function so that it returns objects
private function getAllChildren($catID)
{
$choices = array();
$children = $this->doctrine->getRepository('KprCentarZdravljaBundle:Category')->findByParenting($catID);
foreach ($children as $child) {
$choices[$child->getId()] = $child->getName();
}
return $choices;
}
I changed it to:
private function getAllChildren($catID)
{
$children = $this->doctrine->getRepository('KprCentarZdravljaBundle:Category')->findByParenting($catID)
return $children;
}
Lots of thanks to user redbirdo for pointing out the choices option on an entity field.
It seems like you are doing something too much complicated.
You are on the right way when you write Symfony framework is expecting an entity field instead of choice field.
To do this, replace:
$builder->add('parent', 'choice', array(
'choices' => $this->getAllChildren($catID),
'required' => false,
'attr' => array('data-placeholder' => '--Izaberite Opciju--'),
));
by:
$builder->add('users', 'entity', array(
'class' => 'KprCentarZdravljaBundle:Category',
'property' => 'name',
'query_builder' => function(EntityRepository $er) use($catID) {
return $er->findByParenting($catID);
},
'required' => false,
'empty_value' => '--Izaberite Opciju--'
));
(and you don't need getAllChildren($catID) anymore unless used somewhere else)
http://symfony.com/doc/current/reference/forms/types/entity.html

Symfony empty date field is not required with single_text widget

I created form with date field using single_text widget.
I'm choose this one over choice because I'm using Bootstrap datepicker
Problem I have is that field is always populated with current date instead of being empty.
According to this it should work if i set required to false but id does not.
I tried setting empty_value to empty string same for data but in first case nothing happened probably because this option is for choice fields. In second case I'm getting exception "Expected argument of type "\DateTime", "string" given"
I tried using DataTransformer but didn't make any difference. I found out that for data fields value always goes through DateTimeToLocalizedStringTransformer and if I understand it correctly it is returning empty string if empty value so problem is somewhere further after datatransformers.
One more thing I tried is to set value using attr array and it worked unfortunately the side effect was that populating form with some values doesn't affect date field.
Is there any way to set default value of data field as empty with single_text widget?
Here goes a code
<?php
namespace Psw\AdminBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\EntityRepository;
use Psw\AdminBundle\Form\DataTransformer\EmptyDateTransformer;
use Psw\AdminBundle\Form\DataTransformer\EmptyDateViewTransformer;
class OrdersFilterType extends AbstractType
{
private $admin;
public function __construct($admin=false) {
$this->admin = $admin;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('client', 'entity', array(
'class' => 'PswAdminBundle:User',
'required' => false,
'multiple' => false,
'label' => 'orders.client',
'empty_value' => 'orders.allclients',
'query_builder' => function(EntityRepository $er) {
$qb = $er->createQueryBuilder('u');
return $qb->where($qb->expr()->like('u.roles', '?0'))
->setParameters(array('%ROLE_CLIENT%'));
}
));
if($this->admin) {
$builder->add('staff', 'entity', array(
'class' => 'PswAdminBundle:User',
'required' => false,
'multiple' => false,
'label' => 'orders.staff',
'empty_value' => 'orders.allStaff',
'query_builder' => function(EntityRepository $er) {
$qb = $er->createQueryBuilder('u');
return $qb->where($qb->expr()->like('u.roles', '?0'))
->orWhere($qb->expr()->like('u.roles', '?1'))
->orWhere($qb->expr()->like('u.roles', '?2'))
->setParameters(array('%ROLE_STAFF%','%ROLE_ADMIN%','%ROLE_SUPER_ADMIN%'));
}
));
}
$builder->add('start', 'date', array(
'label' => 'orders.start',
'widget' => 'single_text',
'required' => false,
))
->add('end', 'date', array(
'label' => 'orders.end',
'widget' => 'single_text',
'required' => false,
))
->add('min', null, array(
'label' => 'orders.min',
'required' => false,
))
->add('max', null, array(
'label' => 'orders.max',
'required' => false,
));
}
public function getDefaultOptions(array $options)
{
$options = parent::getDefaultOptions($options);
$options['csrf_protection'] = false;
return $options;
}
public function getName()
{
return 'psw_adminbundle_ordersfiltertype';
}
}

Resources