Deprecated method addValidation and class CallbackValidator in Symfony2 - php

I have a problem. I need to validate a field that is not in entity in form type class. Previously I used this code:
$builder->addValidator(new CallbackValidator(function(FormInterface $form){
if (!$form['t_and_c']->getData()) {
$form->addError(new FormError('Please accept the terms and conditions in order to registe'));
}
}))
But since Symfony 2.1 method addValidator and class CallbackValidator are deprecated. Does anyone know what I should use instead?

I've done it in this way:
add('t_and_c', 'checkbox', array(
'property_path' => false,
'constraints' => new True(array('message' => 'Please accept the terms and conditions in order to register')),
'label' => 'I agree'))

The interface FormValidatorInterface was deprecated and will be removed in Symfony 2.3.
If you implemented custom validators using this interface, you can
substitute them by event listeners listening to the
FormEvents::POST_BIND (or any other of the *BIND events). In case
you used the CallbackValidator class, you should now pass the callback
directly to addEventListener.
via https://github.com/symfony/symfony/blob/master/UPGRADE-2.1.md#deprecations

For anyone else looking for help changing their validators to event subscribers (as it is slightly different to normal subscribers) follow this:
Step 1
Change:
$builder->addValidator(new AddNameFieldValidator());
to
$builder->addEventSubscriber(new AddNameFieldSubscriber());
Step 2
Replace your validator class (and all the namespaces) to a subscriber class.
Your subscriber class should look like the following:
// src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace Acme\DemoBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AddNameFieldSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(FormEvents::POST_BIND => 'postBind');
}
public function postBind(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
$form->addError(new FormError('oh poop'))
}
}
You do not need to register the subscriber in a service file (yml or otherwise)
Reference:
http://symfony.com/doc/2.2/cookbook/form/dynamic_form_modification.html#adding-an-event-subscriber-to-a-form-class

Related

How to pass an instance to symfony form validator class?

I am trying to create a custom symfony form validator constraint. I created two class, one constraint and one validator and it works fine. But I need to pass doctrine entitymanager instance to validator class, as I am using them standalone and not framework, I don't have yaml configuration file. I created a constructor in validator class to have $em, and in controller I have:
->add('email', EmailType::class, [
'constraints' => [
new Assert\Email(['message' => 'invalid.email', 'mode' => 'strict', 'normalizer' => 'trim']),
new Assert\EmailExists($em),
],
]);
But I am not getting $em, in my validator class, what should I do? I also tried to have constructor in main constraint class, then in validator I had parent::construct(), still not working.
I did read this too How to configure dependencies on custom validator with Symfony Components? but instead of making the factory class, I used the current factor class and used this:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\ContainerConstraintValidatorFactory;
$container = new ContainerBuilder();
$container
->register('customEmailUniqueEntity', 'EmailExistsValidator')
->addArgument($em);
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->setConstraintValidatorFactory(
new ContainerConstraintValidatorFactory($container)
);
$validator = $validatorBuilder->getValidator();
$violations = $validator->validate('email address', [
new EmailExists()
]);
if (0 !== count($violations)) {
// there are errors, now you can show them
foreach ($violations as $violation) {
echo $violation->getMessage().'<br>';
}
}
With this code both dependency injection and validation works fine, but is there a trick to have this custom constraint as 'constraint' array argument within form builder rather than validating it manually?
->add('email', EmailType::class, [
'constraints' => [
new Assert\Email(['message' => 'invalid.email', 'mode' => 'strict', 'normalizer' => 'trim']),
new Assert\EmailExists($em),
],
]);
With code above I cannot pass $em to the constructor of my custom Validator. Any trick possible?
EDIT:
In order to inject doctrine EntityManager, in EmailExists class I had:
public function validatedBy()
{
return 'customEmailUniqueEntity';
//return \get_class($this).'Validator';
}
then I had:
$container = new ContainerBuilder();
$container
->register('customEmailUniqueEntity', 'EmailExistsValidator')
->addArgument($em);
because if I was returning validator class from validatedBy() I could not inject $em to the constructor of validator. With the answer below I used:
->addTag('validator.constraint_validator');
But now I am getting customEmailUniqueEntity class not found error, as if I return validator from validatedBy(), injection will not work, what should I do? I tried to return
public function validatedBy()
{
return 'EmailExists';
//return \get_class($this).'Validator';
}
but this one, of course I am getting initialize() error. Please advise.
EDIT2:
I added addTag to:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\ContainerConstraintValidatorFactory;
$container = new ContainerBuilder();
$container
->register('customEmailUniqueEntity', 'EmailExistsValidator')
->addArgument($em),
->addTag('validator.constraint_validator');
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->setConstraintValidatorFactory(
new ContainerConstraintValidatorFactory($container)
);
$validator = $validatorBuilder->getValidator();
$violations = $validator->validate('email address', [
new EmailExists()
]);
if (0 !== count($violations)) {
// there are errors, now you can show them
foreach ($violations as $violation) {
echo $violation->getMessage().'<br>';
}
}
and in constructor of EmailExistsValidator I have:
var_dump($em);
and I got $em object in validator, so $em is injected and adding addTag() did not cause any error. If I remove validatedBy() of EmailExists contsraint, injection will not be done. In that method I am doing
return `customEmailUniqueEntity;`
because if I return EmailExistsValidator, injection will not be done.
Now how to use validator.constraint_validator or EmailExists() as constraints array param of the form? if I use new EmailExists() I will get Two Few Aguments for validator class as $em wll not be injected this way. What to do?
Constraints are not validators.
Symfony will take a constraint and search for its validator by attaching Validator to the classname.
So in symfony you register your constraint by EmailExists but the class/service which actually does validation is EmailExistsValidator.
And this is also the place to inject EntityManagerInterface into it.
All information can be found here: Symfony - How to Create a custom Validation Constraint
Your customEmailUniqueEntity service will never be taken into account by the ContainerConstraintValidatorFactory when it determines the actual validator to be used for the EmailExists constraint.
In order to let the factory know which services are constraint validators you need to tag it with the validator.constraint_validator tag like this:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
$container = new ContainerBuilder();
$container
->register('customEmailUniqueEntity', 'EmailExistsValidator')
->addArgument($em)
->addTag('validator.constraint_validator');
$container->addCompilerPass(new AddConstraintValidatorPass());
$container->compile();

How to replace all 403 status codes with 404 in Symfony

In Symfony, when a user attempts to access a route which is forbidden for that specific user (according to the user roles), a page with response code 403 will be returned.
So the user can still see that there is a valid route there.
I would like to overwrite this behavior by replacing the status code 403 with 404, so the user will just see that there is no valid route when she/he is not allowed to access that resource.
How can I accomplish that?
This is doable, however almost undocumented. I'm aware of two ways but there might be even more:
Using access_denied_url configuration option. See security config reference. With this option you can set URL where the user is redirected when the user in unauthorized (I think it should work also with route name). See a similar question: Symfony2 Redirection for unauthorised page with access_denied_url
There're also "Entry Points" as mentioned in The Firewall and Authorization. However, no examples, no explanation how to use it.
I looks like this option expects a service name as can be seen in security config reference (search for entry_point option).
One possible solution, as partially explained here, can be the following:
1) Defining a new service controller in services.yml
exception_controller:
class: Path\To\MyBundle\Controller\MyExceptionController
arguments: ['#twig', '%kernel.debug%']
2) Creating the new class which overrides Symfony\Bundle\TwigBundle\Controller\ExceptionController:
namespace Path\To\MyBundle\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class MyExceptionController extends ExceptionController
{
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
{
$currentContent = $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1));
$showException = $request->attributes->get('showException', $this->debug); // As opposed to an additional parameter, this maintains BC
$code = $exception->getStatusCode();
if ($code == 403) {
$code = 404;
// other customizations ...
}
return new Response($this->twig->render(
(string) $this->findTemplate($request, $request->getRequestFormat(), $code, $showException),
array(
'status_code' => $code,
'status_text' => isset(Response::$statusTexts[$code]) ? Response::$statusTexts[$code] : '',
'exception' => $exception,
'logger' => $logger,
'currentContent' => $currentContent,
)
));
}
}
3) Setting the following in config.yml under twig:
twig:
exception_controller: 'exception_controller:showAction'
Even though my original goal was to prevent such an exception to be thrown at all with that code.
Another solution can be overwriting the AccessListener service of the Symfony Security component.
The generic procedure about how to override a service of a bundle is documented here. The following is the concrete example about this particular situation.
First of all let's create the class which overrides the AccessListener class:
<?php
namespace Path\To\My\Bundle\Services;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class OverrideAccessListener extends AccessListener
{
public function handle(GetResponseEvent $event)
{
try {
parent::handle($event);
} catch (AccessDeniedException $e) {
$request = $event->getRequest();
$message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo());
if ($referer = $request->headers->get('referer')) {
$message .= sprintf(' (from "%s")', $referer);
}
throw new NotFoundHttpException($message);
}
}
}
then we need to create a Compiler Pass in order to change the class attribute of the original service with the new class:
<?php
namespace Path\To\My\Bundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OverrideServiceCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.access_listener');
$definition->setClass('Path\To\My\Bundle\Services\OverrideAccessListener');
}
}
finally we need to register the Compiler Pass in the build method of the bundle:
<?php
namespace Path\To\My\Bundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Path\To\My\Bundle\DependencyInjection\Compiler\OverrideServiceCompilerPass;
class MyBundleName extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OverrideServiceCompilerPass());
}
}
Finally I found a simpler solution: using an access denied handler.
Unfortunately there is no much documentation about how to create an access denied handler, but it is very simple.
First create a class that implements the AccessDeniedHandlerInterface and set it as a service (for example naming it my_access_denied_handler_service).
In the handle method a Response should be created and returned (in my case I wanted a 404 response).
Then in the security.yml configuration file we have to set the access_denied_handler under the firewall:
...
firewalls:
my_firewall:
...
access_denied_handler: my_access_denied_handler_service
...
...

How can I add a custom Validator to Respect's Validation library

The awesome Respect Validation library comes with lots of built-in validators, like string(), alpha(), et cetera. I want to add self-defined validators to the library, eg., I want to be able to do this:
Validator::myCustomValidator()->assert( $input );
I just discovered that's it not very complicated, but I had to look at the library's source code to find out, so I'm posting this self-answered question here for future reference.
Define a validation class and an accompanying exception class in the proper namespace, and the Validation library will automagically use them to validate your data, as such:
myCustomValidator.php:
<?php
namespace Respect\Validation\Rules;
class myCustomValidator extends AbstractRule
{
public function validate($input)
{
return true; // Implement actual check here; eg: return is_string($input);
}
}
myCustomValidatorException.php:
<?php
namespace Respect\Validation\Exceptions;
class myCustomValidatorException extends ValidationException
{
public static $defaultTemplates = array(
self::MODE_DEFAULT => array(
self::STANDARD => '{{name}} must ... ', // eg: must be string
),
self::MODE_NEGATIVE => array(
self::STANDARD => '{{name}} must not ... ', // eg: must not be string
)
);
}
As long as these files are included in your project, Validator::myCustomValidator()->assert( $input ); should now work.
This obviously relies on naming conventions, so be sure to use the class name to call the self-defined validator and you should be set.

Symfony2 form not validate email and required constraint

I have a form like this made with Symfony2:
class UsuarioRegistroType extends AbstractType {
public function BuildForm(FormBuilderInterface $builder, array $options) {
$builder->add('email', 'email', array('label' => 'E-mail', 'required' => true))
....
Forms works fine, but if I write something like Email: asdf (not email address), I never get the error assosiated with this issue. Also, if I don't write anything, I don't get any error for required constraint.
Any idea with this issue?
Thanks :)
Required true don't validate anything. It just add a class required on the field on the form view. It's the html5 which validate that.
Try to add that on UsuarioRegistroType class :
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$collectionConstraint = new Collection(array(
'email' => array(
new NotBlank(array('message' => 'Email should not be blank.')),
new Email(array('message' => 'Invalid email address.'))
)
));
$resolver->setDefaults(array(
'constraints' => $collectionConstraint
));
}
don't forget the use statements :
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Collection;
You have just used HTML5 validation, many barowser do not support this. With this old version of different famous browser also not support HTML5 validation.
I think you should use annotation for validation from serverside. I think you know about annotation , which you can use in your Entity class. In your enity class property you may defined your required validation rules using annotaion in symfony2.
Example:
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* #Assert\Email(
* message = "The email '{{ value }}' is not a valid email.",
* checkMX = true
* )
*/
protected $email;
}
Helpful link from Symfony2 official documentation: Symfony2 Validation

Possible to create a factory to instantiate custom Form validators?

(using Zend Framework 2.2.4)
My validator factory, doesn't seem to "exist" at validation time. If I attempt to instantiate the validator from the controller in which the form is housed, it conversely works fine:
This works...
$mycustomvalidator = $this->getServiceLocator()
->get('ValidatorManager')
->get('LDP_PinAvailable');
Here's how things are set up otherwise in the code, I can't seem to find the problem, and was hopeful to avoid opening up ZF2 source to understand. By way of documentation, it seems right.
Module Config
public function getValidatorConfig()
{
return array(
'abstract_factories' => array(
'\LDP\Form\Validator\ValidatorAbstractFactory',
),
);
}
Factory Class
namespace LDP\Form\Validator;
use Zend\ServiceManager\AbstractFactoryInterface,
Zend\ServiceManager\ServiceLocatorInterface;
class ValidatorAbstractFactory implements AbstractFactoryInterface
{
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
{
return stristr($requestedName, 'LDP_PinAvailable') !== false;
}
public function createServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
{
// baked in for sake of conversation
$validator = new \LDP\Form\Validator\PinAvailable();
if( $validator instanceof DatabaseFormValidatorInterface )
$validator->setDatabase( $locator->get('mysql_slave') );
return $validator;
}
}
Custom Validator
namespace LDP\Form\Validator;
class PinAvailable extends \Zend\Validator\AbstractValidator implements DatabaseFormValidatorInterface
{
/**
* #var \Zend\Db\Sql\Sql
*/
private $database;
public function setDatabase( \Zend\Db\Sql\Sql $db )
{
$this->database = $db;
}
public function isValid( $value )
{
$DBA = $this->database->getAdapter();
// do the mixed database stuff here
return true;
}
}
Lastly, the form field validator config part of the array:
'pin' => array(
'required' => true,
'filters' => array(
array('name' => 'alnum'),
array('name' => 'stringtrim'),
),
'validators' => array(
array( 'name' => 'LDP_PinAvailable' )
),
),
),
Piecing it all together, the form loads, and when submitted, it does with the stack trace below:
2013-10-28T17:09:35-04:00 ERR (3): Exception:
1: Zend\Validator\ValidatorPluginManager::get was unable to fetch or create an instance for LDP_PinAvailable
Trace:
#0 /Users/Saeven/Documents/workspace/Application/vendor/zendframework/zendframework/library/Zend/ServiceManager/AbstractPluginManager.php(103): Zend\ServiceManager\ServiceManager->get('LDP_PinAvailabl...', true)
#1 /Users/Saeven/Documents/workspace/Application/vendor/zendframework/zendframework/library/Zend/Validator/ValidatorChain.php(82): Zend\ServiceManager\AbstractPluginManager->get('LDP_PinAvailabl...', Array)
The ValidatorPluginManager extends the Zend\ServiceManager\AbstractPluginManager. The AbstractPluginManager has a feature called "autoAddInvokableClass", which is enabled by default.
Basically, what this means, is that if the service name requested can't be resolved by the ValidatorPluginManager, it will then check if the name is a valid class name. If so, it will simply add it as an invokable class right there, on-demand, which of course means that it will never fall back to your abstract factory.
To circumvent this behavior, the easiest method is to simply make your abstract factory respond to service names that do not actually resolve to the actual class names.
See: AbstractPluginManager.php#L98-L100
Digging some more, I've found the problem. It distilled to these lines in Zend\Validator\ValidatorChain circa line 80:
public function plugin($name, array $options = null)
{
$plugins = $this->getPluginManager();
return $plugins->get($name, $options);
}
There was no plugin manager available in context.
It took about three seconds of Googling to find that I had to do this when I prepared the form in the controller:
$validators = $this->getServiceLocator()->get('ValidatorManager');
$chain = new ValidatorChain();
$chain->setPluginManager( $validators );
$form->getFormFactory()->getInputFilterFactory()->setDefaultValidatorChain( $chain );
Hopefully this helps someone else. You are able to use regular old classnames when setting it up this way, no need to warp the classnames.
In ZF3/Laminas, if a validator is registered as an invokable, you can call the validator in the getInputFilterSpecification() of your form, and no problem. If a validator is instantiated using a factory, you get into trouble. If I understand correctly, even if your form is registered like this
'form_elements' => [
'factories' => [
SomeForm::class => SomeFormFactory::class,
]
]
and your validator:
'validators' => [
'factories' => [
SomeValidator::class => SomeValidatorFactory::class,
]
]
you won't be instantiating the validator via factory. The reason is that the form factory (the one you get like $form->getFormFactory()) has an input filter factory and in there sits default validator chain. And this validator chain has no ValidatorManager attached. And without the ValidatorManager, the default chain cannot map the validator name to the validator factory.
To solve all this headache, in your controller factory do this:
$form->('FormElementManager')->get(SomeForm::class);
$form->getFormFactory()->getInputFilterFactory()
->getDefaultValidatorChain()->setPluginManager($container->get('ValidatorManager'));
and your troubles are over.

Categories