My project is on Symfony 2.6.
I have an Entity Request with a collection of User contributers.
I have a form in order to add a contributer to a request. This form contains a data transformer in order to add the user by its email.
Everything is working just fine When I want to :
CREATE a new request with an email corresponding to :
An existing user.
A NOT existing user : an error message appears to inform user that this email doesn't match any existing user.
UPDATE an existing request by
UPDATING an already existing contributer with an email corresponding to
An existing user.
A NOT existing user : an error message appears to inform user that this email doesn't match any existing user.
ADDING a new contributer with an email corresponding to
An existing user.
BUT when I want to update an existing request by adding a new contributer with an email corresponding to a NOT existing userthen it crashes : Catchable Fatal Error: Argument 1 passed to AppBundle\Entity\Request::addContributer() must be an instance of AppBundle\Entity\User, null given, called in C:\Users\Utilisateur\Workspace\repositories\tvjp\web-app-ws\vendor\symfony\symfony\src\Symfony\Component\PropertyAccess\PropertyAccessor.php on line 512 and defined
Here is a workaround to handle this problem : In my Request entity, I modify my addContributer method in order to accep null User :
This code doesn't work :
public function addContributer(User $contributer){
$this->contributers[] = $contributer;
return $this;
}
This code works :
public function addContributer(User $contributer = null){
if($contributer != null){
$this->contributers[] = $contributer;
}
return $this;
}
But I think this a really dirty workaround. Any ideas of what am I missing here ?
For information, here are parts of my code.
the datatransformer
class UserByEmailTransformer implements DataTransformerInterface{
private $entityManager;
public function __construct(EntityManager $entityManager){
$this->entityManager = $entityManager;
}
public function reverseTransform($email) {
if (!$email) { return new User(); }
$user = $this->entityManager->getRepository('AppBundle:user')->findOneBy(array('email' => $email));
if ($user === null) {
throw new TransformationFailedException('Il n\'existe aucun utilisateur '.$email.'.');
}
return $user;
}
public function transform($user) {
if ($user === null) { return ''; }
return $user->getEmail();
}
}
The user selector
class UserSelectorType extends AbstractType{
private $entityManager;
public function __construct(EntityManager $entityManager){
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options){
$transformer = new UserByEmailTransformer($this->entityManager);
$builder->addModelTransformer($transformer);
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$defaults = array();
$resolver->setDefaults($defaults);
}
public function getParent(){
return 'email';
}
public function getName(){
return 'userSelector';
}
}
The service
user_selector_form_type:
class: AppBundle\Form\Selector\UserSelectorType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type, alias: userSelector }
The form
class RequestType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options){
$isAddForm = $options['isAddForm'];
$builder->add('contributers','collection',array(
'label' => 'request.form.contributers',
'type' => 'userSelector',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
'options' => array('label' => false,'invalid_message' => 'request.contributer.404',)));
if ($isAddForm){
$builder->add('save', 'submit', array('label' => 'request.form.add.submit'));
}else{
$builder->add('save', 'submit', array('label' => 'request.form.edit.submit'));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$defaults = array(
'data_class' => 'AppBundle\Entity\Request',
'intention' => 'request',
'translation_domain' => 'request',
'isAddForm' => ''
);
$resolver->setDefaults($defaults);
}
public function getName(){
return 'request';
}
}
In my code I need to download from database some data and put it to the form, but I don't know how to get doctrone outside Controller class.
I tried create new service, but it didn't work (I think I can't use in this case __controller(), am I right?). I tried also transfer instance of the controller to the parameters of buildForm() method but I got message: FatalErrorException: Compile Error: Declaration of MyBundle\Form\Type\TemplateType::buildForm() must be compatible with that of Symfony\Component\Form\FormTypeInterface::buildForm() ).
This is my code:
class TemplateType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name', 'text')
// ...
->add('description', 'textarea');
}
public function getName() {
return 'template';
}
}
How can I use inside buildForm() doctrine?
In order to send data from doctrine to your form, you need to do this into your controller:
public function doSomethingWithOneObjectAction( $id )
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository( 'AcmeBundle:ObjectEntity' )->find( $id );
if ( ! $entity) {
throw $this->createNotFoundException( 'Unable to find Object entity.' );
}
$form = $this->createForm(
new TemplateType(),
$entity
);
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
If you want to access a service from container inside your form type, you need first to register it as an service and inject into it the services you need. Something like this
I am learning to use Symfony2 and in the documentation I have read, all entities being used with Symfony forms have empty constructors, or none at all. (examples)
http://symfony.com/doc/current/book/index.html Chapter 12
http://symfony.com/doc/current/cookbook/doctrine/registration_form.html
I have parametrized constructors in order to require certain information at time of creation. It seems that Symfony's approach is to leave that enforcement to the validation process, essentially relying on metadata assertions and database constraints to ensure that the object is properly initialized, forgoing constructor constraints to ensure state.
Consider:
Class Employee {
private $id;
private $first;
private $last;
public function __construct($first, $last)
{ .... }
}
...
class DefaultController extends Controller
{
public function newAction(Request $request)
{
$employee = new Employee(); // Obviously not going to work, KABOOM!
$form = $this->createFormBuilder($employee)
->add('last', 'text')
->add('first', 'text')
->add('save', 'submit')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
Should I not be using constructor arguments to do this?
Thanks
EDIT : Answered Below
Found a solution:
Looking into the API for the Controllers "createForm()" method I found something that is not obvious from the examples. It seems that the second argument is not necessarily an object:
**Parameters**
string|FormTypeInterface $type The built type of the form
mixed $data The initial data for the form
array $options Options for the form
So rather than pass in an instance of the Entity, you can simply pass in an Array with the appropriate field values:
$data = array(
'first' => 'John',
'last' => 'Doe',
);
$form = $this->createFormBuilder($data)
->add('first','text')
->add('last', 'text')
->getForm();
Another option (which may be better), is to create an empty data set as a default option in your Form Class.
Explanations here and here
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first');
$builder->add('last');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => new Employee('John', 'Doe'),
));
}
//......
}
class EmployeeFormController extends Controller
{
public function newAction(Request $request)
{
$form = $this->createForm(new EmployeeType());
}
//.........
}
Hope this saves others the head scratching.
I want to separate form validation logic:
public function contactAction()
{
$form = $this->createForm(new ContactType());
$request = $this->get('request');
if ($request->isMethod('POST')) {
$form->submit($request);
if ($form->isValid()) {
$mailer = $this->get('mailer');
// .. setup a message and send it
return $this->redirect($this->generateUrl('_demo'));
}
}
return array('form' => $form->createView());
}
I want to translate into 2 separate actions:
public function contactAction()
{
$form = $this->createForm(new ContactType());
return array('form' => $form->createView());
}
public function contactSendAction()
{
$form = $this->createForm(new ContactType());
$request = $this->get('request');
if ($request->isMethod('POST')) {
$form->submit($request);
if ($form->isValid()) {
$mailer = $this->get('mailer');
// .. setup a message and send it using
return $this->redirect($this->generateUrl('_demo'));
}
}
// errors found - go back
return $this->redirect($this->generateUrl('contact'));
}
The problem is that when errors exist in the form - after form validation and redirect the do NOT showed in the contactAction. (probably they already will be forgotten after redirection - errors context will be lost)
If you check out how the code generated by the CRUD generator handles this you will see that a failed form validation does not return a redirect but instead uses the same view as the GET method. So in your example you would just:
return $this->render("YourBundle:Contact:contact.html.twig", array('form' => $form->createView()))
rather than return the redirect. This means you do not lose the form errors as you do in a redirect. Something else the CRUD generator adds is the Method requirement which means you could specify that the ContactSendAction requires the POST method and thus not need the extra if($request->isMethod('POST')){ statement.
You can also just return an array if you specify the template elsewhere, for example you could use the #Template annotation and then just
return array('form' => $form->createView())
This seems to work for me in Symfony 2.8:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller {
public function templateAction()
{
$form = $this->createForm(new MyFormType(), $myBoundInstance);
if ($session->has('previousRequest')) {
$form = $this->createForm(new MyFormType());
$form->handleRequest($session->get('previousRequest'));
$session->remove('previousRequest');
}
return array(
'form' => $form->createView(),
);
}
public function processingAction(Request $request)
{
$form = $this->createForm(new MyFormType(), $myBoundInstance);
$form->handleRequest($request);
if ($form->isValid()) {
// do some stuff
// ...
return redirectToNextPage();
}
$session->set('previousRequest', $request);
// handle errors
// ...
return redirectToPreviousPage();
}
}
Please note that redirectToNextPage and redirectToPreviousPage, as well as MyFormType, are pseudo code. You would have to replace these bits with your own logic.
I am new to Symfony2 but read about it very much.
First of all, I am using symfony 2.1.7. And FOSUserBundle for user settings. I have already override fos_user-login template, with username and password. But I want to add a captcha for log in. I have seen GregwarCaptchaBundle, and according to document, new field should be added to FormType. And my question comes: Where is the symfony or FOSUserBundle login form type, that i can add this new field, or override it? There exists ChangePasswordFormType, ProfileFormType... etc. but no LoginFOrmType. May be it is so obvious but i did not get the point, Any help is welcomed please
QUESTION IS EDITED WITH A SOLUTION SOMEHOW
Take a look at the comments below that Patt helped me.
I have created a new form type with _username, _password and captcha fields. When naming for username and password begins with an underscore is enough for 'login_check' routing and Symfony authentication. However Symfony uses a listener for login process.
Which is UsernamePasswordFormAuthenticationListenerclass. Although i've added captcha field in the Form type, it is always ignored during login process.(It is rendered on the page, but the field is never validated, it is simply ignored.)
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username', 'email', array('label' => 'form.username', 'translation_domain' => 'FOSUserBundle')) // TODO: user can login with email by inhibit the user to enter username
->add('_password', 'password', array(
'label' => 'form.current_password',
'translation_domain' => 'FOSUserBundle',
'mapped' => false,
'constraints' => new UserPassword()))
->add('captcha', 'captcha');
}
As i mentioned above UsernamePasswordFormAuthenticationListener class gets the form input values and then redirects you:
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'captcha' => 'captcha',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfProvider = $csrfProvider;
}
captcha field is added.
protected function attemptAuthentication(Request $request)
{
if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) {
if (null !== $this->logger) {
$this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod()));
}
return null;
}
if (null !== $this->csrfProvider) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
// check here the captcha value
$userCaptcha = $request->get($this->options['captcha'], null, true);
$dummy = $request->getSession()->get('gcb_captcha');
$sessionCaptcha = $dummy['phrase'];
// if captcha is not correct, throw exception
if ($userCaptcha !== $sessionCaptcha) {
throw new BadCredentialsException('Captcha is invalid');
}
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
}
Now, i have captcha on login screen.
Playing with symfony code is not a good way, i know. If i find out some way to override and call my own function, i'll post it.
ANOTHER USEFUL ANSWER
I found another answer that might be useful
[link]Is there any sort of "pre login" event or similar?
Following this solution, I have simply override UsernamePasswordFormAuthenticationListenerclass and override security listener security.authentication.listener.form.class parameter. Here goes the code:
namespace TCAT\StaffBundle\Listener;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener as BaseListener; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Log\LoggerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class StaffLoginFormListener extends BaseListener
{
private $csrfProvider;
/**
* {#inheritdoc}
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options
= array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'captcha' => 'captcha',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfProvider = $csrfProvider;
}
/**
* {#inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) {
if (null !== $this->logger) {
$this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod()));
}
return null;
}
if (null !== $this->csrfProvider) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
// throw new BadCredentialsException('Bad credentials');
$userCaptcha = $request->get($this->options['captcha'], null, true);
$dummy = $request->getSession()->get('gcb_captcha');
$sessionCaptcha = $dummy['phrase'];
if ($userCaptcha !== $sessionCaptcha) {
throw new BadCredentialsException('Captcha is invalid');
}
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
}
}
and add security.authentication.listener.form.class: TCAT\StaffBundle\Listener\StaffLoginFormListener line to the app/config/paramaters.yml
BTW i can check my captcha value. I hope it all work for you.
Adding Captcha to Symfony2 Login Page
I am not sure this is a great idea. But it's doable.
Where is the symfony or FOSUserBundle login form type?
There is no form type for the login. The form is directly embed in the template as you can see in login.html.twig.
How could you do it?
You could totally create one but you would have to customize the SecurityController so that you send your form to the template.
The procedure would be something like that:
1. Create your custom loginFormType (that's where you can add your captcha in the builder).
2. Override the SecurityController (you could take a look here to see something similar). You need to override the loginAction method so that you can pass the form to your template here.
3. Override login.html.twig to render the form passed from your controller
Edit: Answer to your comment
How can you access to your form in a controller that extends
ContainerAware?
I highly recommend this reading to see how you can move away from the base controller. Now, how can you do this?
Well, you have 2 options:
OPTION 1: EASY WAY
$form = $this->createForm(new LoginFormType(), null);
becomes:
$form = $this->get('form.factory')->create(new LoginFormType(), $null);
OPTION 2: REGISTER FORM AS A SERVICE
1. Create your formType (normal procedure): loginFormType
2. Define your form as a service acme_user.login.form. You have a great example here (In the 1.2 version of FOSUserBundle, both registration and profile forms were registered as services, so this gives you a perfect example of how it's done).
3. You can now use your form inside your controller extending ContainerAware. See here.
$form = $this->container->get('acme_user.login.form');
In response to : Playing with symfony code is not a good way, i know. If i find out some way to override and call my own function, i'll post it.
To override the "UsernamePasswordFormAuthenticationListenerclass" you must copy the listner file in your bundle and change the config.yml file to load th new one :
parameters:
security.authentication.listener.form.class: Acme\YourBundle\Security\UsernamePasswordFormAuthenticationListener
Also the namespace in the copied file must be changed to the correct one :
namespace Acme\YourBundle\Security;
The last thing is adding "AbstractAuthenticationListener" in the use part to be loaded correctly :
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;