Symfony Dynamic Form Collection Errors Not Associated to Fields - php

I have an issue with a Symfony Form Collection field. I have a parent form which has two fields which are form collections. Everything is working absolutely fine, except when I submit my form with invalid data. The errors for the form collection fields are output beneath the form on the page. I have been reading through the documentation for error_bubbling on these fields and realise that for CollectionType fields, it defaults to true. I have therefore set it to false on each field and still the errors are not mapped to the fields on the form.
The collection fields are able to be added dynamically to the page through javascript on the front end. What I have noticed, is that in my markup, before I have even submitted the form, there are two erroneous <div class=""form-group">'s added to the base of my markup which I am not outputting in my template. When the form is submitted and is not valid, then the errors are being output within these divs.
The code;
ItemFormType;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('shop', ShopType::class, [
'data_class' => Shop::class,
'label' => false,
])
->add('purchase', PurchaseType::class, [
'data_class' => Purchase::class,
'label' => false,
])
->add('missing_items', CollectionType::class, [
'entry_type' => MissingItemFormType::class,
'allow_add' => true,
'allow_delete' => true,
'label' => false,
'prototype' => true,
'error_bubbling' => false,
])
->add('replaced_items', CollectionType::class, [
'entry_type' => ReplacedItemFormType::class,
'allow_add' => true,
'allow_delete' => true,
'label' => false,
'prototype' => true,
'error_bubbling' => false,
])
->add('submit', SubmitType::class)
->getForm();
}
/**
* Get the form name.
*
* #return string
*/
public function getName(): string
{
return 'missing_form';
}
/**
* Set form options.
*
* #param OptionsResolver $resolver
*
* #return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
'error_bubbling' => false
]);
}
Controller;
/**
* #Route("/", name="homepage")
*
* #param ClaimMailer $mailer
* #param Request $request
*
* #return Response
*/
public function indexAction(ClaimMailer $mailer, Request $request): Response
{
$purchase = [
'shop' => new Shop(),
'purchase' => new Purchase(),
];
$form = $this->createForm(MissingFormType::class, $purchase);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->store($form, $purchase);
// Send confirmation email.
$mailer->send(
$purchase['purchase']->getEmail(),
$purchase['shop']->getName(),
$purchase['purchase']->getClaimReferenceNumber()
);
return $this->render('form/form_complete.html.twig', [
'purchase_id' => $purchase['purchase']->getPurchaseReferenceNumber(),
]);
}
return $this->render('form/purchase_form.html.twig', [
'form' => $form->createView(),
]);
}
/**
* Store form data.
*
* #param Form $form
* #param array$claim
*
* #return void
*/
public function store(Form $form, $purchase){}
The Template;
{% block _missing_form_missing_items_entry_row %}
{% for field in form %}
<td>
{{ form_row(field) }}
</td>
{% endfor %}
{% endblock %}
{% block _missing_form_replaced_items_entry_row %}
{% for field in form %}
<td>
{{ form_row(field) }}
</td>
{% endfor %}
{% endblock %}
{% block website_body %}
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-body">
{{ form_start(form) }}
<div class="row">
<div class="col-lg-6">
{{ form_row(form.shop.name) }}
{{ form_row(form.shop.accountNumber) }}
{{ form_row(form.shop.email) }}
{{ form_row(form.shop.addressLine1) }}
</div>
<div class="col-lg-6">
{{ form_row(form.shop.addressLine2) }}
{{ form_row(form.shop.town) }}
{{ form_row(form.shop.county) }}
{{ form_row(form.shop.postcode) }}
</div>
</div>
<div class="row">
<h3>Missing Items</h3>
<table class="table missing_items">
<tbody class="missing_items" data-prototype="{{ form_row(form.missing_items.vars.prototype)|e('html_attr') }}"></tbody>
</table>
</div>
<div class="row">
<div class="col-lg-6">
{{ form_row(form.purchase.receivedReplacement) }}
</div>
<table class="table replacement-items">
<tbody class="replacement_items" data-prototype="{{ form_row(form.replaced_items.vars.prototype)|e('html_attr') }}"></tbody>
</table>
</div>
<div class="row">
<div class="col-lg-6">
{{ form_row(form.submit) }}
</div>
</div>
{{ form_end(form) }}
{% embed 'form/components/terms_and_conditions.html.twig' %}{% endembed %}
</div>
</div>
</div>
</div>
{% endblock %}
Any assistance would be greatly appreciated! Tried everything to get the errors in the correct place.

So for anyone else out there, I have found the answer to this conundrum.
Basically, whilst my JS was rendering the form onto the page via the "prototype" attribute on my <tbody>, as far as Symfony was aware, I was not explicitly outputting the fields into my template. As a result, all of the errors for the item fields were being spat out when calling 'form_end(form)' at the end of the template.
As form_end(form) calls form_rest() behind the scenes, it basically outputs any of the fields for the form which have not been explicitly rendered - as a result the validation errors and their respective fields were being output at the end of the form on the page!
By explicitly outputting these fields within the template, the errors and associated fields were displayed correctly in the form as so;
<tbody class="missing_items" data-prototype="{{ form_row(form.missing_items.vars.prototype)|e('html_attr') }}">
{% for field in form.missing_items %}
<tr class="item">
<td>{{ form_row(field.quantity) }}</td>
<td>{{ form_row(field.description) }}</td>
<td>{{ form_row(field.invoiceNumber) }}</td>
<td>{{ form_row(field.invoiceDate) }}</td>
<td>{{ form_row(field.deliveryDate) }}</td>
</tr>
{% endfor %}
</tbody>
I do hope this helps somebody else out there who finds themselves in the same predicament!

Related

Template doesn't work in Symfony - how to connect a new function?

I'm creating an app and I want to enable users to change their data and password. I've created an email changing form and it works, but I've got a problem with password.
I've got a page localhost:8000/user/{id} and here the buttons: edit email and edit password. Edit email is working and when I click on editing password - I got a blank page on localhost:8000/user/{id}/change_password
(enter image description here enter image description here). Th PassType doesn't show there.
There are no syntax error on localhost:8000/user/{id} and localhost:8000/user/{id}/change_password.
I've added a function edit_pass to UserController, crated PassType (where is the form to change password) and make a template edit_password.html.twig.
I don't know where is the problem. I did the same thing as with other Forms which work. I've tried to clear the edit_password template (I've just left the base_html there and put some text) cause I've thought there is a problem in it, but it was still a blank page on localhost:8000/user/{id}/change_password.
PassType:
<?php
/*
* Password type.
*/
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class PassType.
*/
class PassType extends AbstractType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* #see FormTypeExtensionInterface::buildForm()
*
* #param FormBuilderInterface $builder The form builder
* #param array $options The options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'password',
RepeatedType::class,
[
'type' => PassType::class,
'required' => true,
'attr' => ['max_length' => 40],
'first_options' => ['label' => 'label.password'],
'second_options' => ['label' => 'label.repeat_password'],
]
);
}
/**
* Configures the options for this type.
*
* #param OptionsResolver $resolver The resolver for the options
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => User::class]);
}
/**
* Returns the prefix of the template block name for this type.
*
* The block prefix defaults to the underscored short class name with
* the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
*
* #return string The prefix of the template block name
*/
public function getBlockPrefix(): string
{
return 'user';
}
}
edit_password.html.twig
{% extends 'base.html.twig' %}
{% block title %}
{{ 'title_password_edit'|trans({'%id%': user.id|default('')}) }}
{% endblock %}
{% block body %}
<h1 class="display-4 d-flex justify-content-center">{{ 'title_password_edit'|trans({'%id%': user.id|default('')}) }}</h1>
{{ form_start(form, { method: 'PUT', action: url('password_edit', {id: user.id}) }) }}
{{ form_widget(form) }}
<div class="form-group row float-sm-right">
<input type="submit" value="{{ 'action_save'|trans }}" class="btn btn-primary" />
</div>
<div class="form-group row float-sm-left">
<a href="{{ url('user_index') }}" class="btn btn-link">
{{ 'action_back_to_list'|trans }}
</a>
</div>
{{ form_end(form) }}
{% endblock %}
and part of UserController whith edit_pass function
/**
* Edit Password.
*
* #param \Symfony\Component\HttpFoundation\Request $request HTTP request
* #param \App\Entity\User $user User entity
* #param \App\Repository\UserRepository $repository User repository
*
* #return \Symfony\Component\HttpFoundation\Response HTTP response
*
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*
* #Route(
* "/{id}/change_password",
* methods={"GET", "PUT"},
* requirements={"id": "[1-9]\d*"},
* name="password_edit",
* )
*/
public function edit_pass(Request $request, User $user, UserPasswordEncoderInterface $passwordEncoder, UserRepository $repository): Response
{
$form = $this->createForm(PassType::class, $user, ['method' => 'PUT']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('password')->getData()
)
);
$repository->save($user);
$this->addFlash('success', 'message.updated_successfully');
return $this->redirectToRoute('user_show', array('id' => $this->getUser()->getId()));
}
return $this->render(
'user/edit_password.html.twig',
[
'form' => $form->createView(),
'user' => $user,
]
);
}
I'm also adding the user/show.html.twig
{% extends 'base.html.twig' %}
{% block title %}
{{ 'label_detail_users'|trans({'%id%': user.id|default('')}) }}
{% endblock %}
{% block body %}
<h1 class="display-4 d-flex justify-content-center">{{ 'label_detail_users'|trans({'%id%': user.id|default('')}) }}</h1>
{% if users is defined and users|length %}
<table class="table table-striped">
<thead>
<tr>
<th>{{ 'label_user_id'|trans }}</th>
<th>{{ 'label_email'|trans }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ users.id }}</td>
<td>{{ users.email }}</td>
</tr>
</tbody>
</table>
<p>
<a class="btn btn-info" href="{{ url('user_edit', {id: users.id}) }}" title="{{ 'edit_email'|trans }}">
{{ 'edit_email'|trans }}
</a>
</p>
<p>
<a class="btn btn-info" href="{{ url('password_edit', {id: users.id}) }}" title="{{ 'edit_password'|trans }}">
{{ 'edit_password'|trans }}
</a>
</p>
<p>
<a class="btn btn-info" href="{{ url('user_index') }}" title="{{ 'action_back_to_list'|trans }}">
{{ 'action_back_to_list'|trans }}
</a>
</p>
{% else %}
<p>
{{ 'message_item_not_found'|trans }}
</p>
{% endif %}
{% endblock %}
You produced an endless loop - its PasswordType in your RepeatedType. PassType is the name of your whole Form - you see the problem?
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'password',
RepeatedType::class,
[
'type' => PasswordType::class,
'required' => true,
'attr' => ['max_length' => 40],
'first_options' => ['label' => 'label.password'],
'second_options' => ['label' => 'label.repeat_password'],
]
);
}
And may I ask why you limit the length of a password?

Custom form for user password in Symfony 4

In Easy Admin, I already have a list/edit form of users. I want to add an extra form to change password of any member user. (password, repeat password, submit)
In the documentation custom forms are told to be entity specific. For example, to create a custom product form, you create a custom controller:
easy_admin:
entities:
# ...
Product:
controller: AppBundle\Controller\ProductController
# ...
But this solution doesn't fit to my problem. I already set a user form and use that form.
I can set an event listener and manage saving the password but I'm stuck with adding this simple form.
Firstly, you need a controller to handle requests associated with your users (create one if you don't have it already)
/**
* #Route("/user/change-password", name="change_password")
*/
public function changePassword(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$changePasswordModel = new ChangePassword();
$form = $this->createForm(ChangePasswordType::class, $changePasswordModel);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$user = $entityManager->find(User::class, $this->getUser()->getId());
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('newPassword')->getData()
)
);
$entityManager->persist($user);
$entityManager->flush();
return $this->redirect('/?entity=User&action=show&id='. $this->getUser()->getId());
}
return $this->render('admin/theme/changePassword/change_password.html.twig', array(
'changePasswordForm' => $form->createView(),
));
}
Then I created a form type that looks like this:
class ChangePasswordType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('oldPassword', PasswordType::class, [
'required' => true,
'label' => 'Type your current password',
])
->add('newPassword', RepeatedType::class, [
'type' => PasswordType::class,
'invalid_message' => 'Passwords do not match.',
'first_options' => ['label' => 'Type your new password'],
'second_options' => ['label' => 'Retype your new password']
]);
}
public function setDefaultOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => ChangePassword::class,
));
}
public function getName()
{
return 'change_passwd';
}
}
I created a form model with custom validation, you can edit this however you like
class ChangePassword
{
/**
* #SecurityAssert\UserPassword(
* message = "Wrong value for your current password!"
* )
*/
public $oldPassword;
/**
* #Assert\Length(
* min = 6,
* minMessage = "Password must be at least 6 characters long!"
* )
*/
public $newPassword;
}
Lastly, extend your base twig with someting like this
{% extends 'base.html.twig' %}
{% block title %}Change password
{% endblock %}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('change-password') }}
{% endblock %}
{% block body %}
{{ parent() }}
<body id="{% block body_id %}{% endblock %}">
<div class="container-fluid h-100">
<div class="row justify-content-center align-items-center h-100">
<div class="col col-sm-8 col-md-8 col-lg-6 col-xl-4">
{{ form_start(changePasswordForm, {'attr':{'class':'form-signin'}}) }}
{{ form_row(changePasswordForm.oldPassword, {'attr': {'class':'form-control mb-2'} }) }}
{{ form_row(changePasswordForm.newPassword.first, {'attr': {'class':'form-control mb-2'} }) }}
{{ form_row(changePasswordForm.newPassword.second, {'attr': {'class':'form-control mb-2'} }) }}
<button class="btn btn-dark btn-lg btn-block mt-3" type="submit">Change password</button>
{{ form_end(changePasswordForm) }}
</div>
</div>
</div>
</body>
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('change-password') }}
{% endblock %}
I created a button on my show user action that takes you to /user/change-password and that is pretty much it.

Display custom message for checkbox (terms of service/privacy)

I am still new to Symfony and Php, so I am using the CheckboxType code from Symfony docs.
Currently no message is appearing if a user attempts to register and the checkbox is unchecked, but it will still prevent the user from making a account.
(1) I would like a error message to appear next to the checkbox in red stating the box must be checked in order to proceed. I would also like to customize this message.
Thank you!
Register.html
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Register!</h1>
{{ form_start(form) }}
{{ form_row(form.username) }}
{{ form_row(form.email) }}
{{ form_row(form.plainPassword.first, {
'label': 'Password'
}) }}
{{ form_row(form.plainPassword.second, {
'label': 'Repeat Password'
}) }}
Terms of service
{{ form_widget(form.termsAccepted) }}
cancel
<button type="submit" class="btn btn-primary" formnovalidate>
Register
</button>
<br></br>
<p>Privacy Policy
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}
RegistrationForm.php
class UserRegistrationForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('username', TextType::class)
->add('plainPassword', RepeatedType::class, ['type' => PasswordType::class])
->add('termsAccepted', CheckboxType::class, array(
'mapped' => false,
'constraints' => new IsTrue(),));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'validation_groups' => ['Default', 'Registration']
]);
}
}
The problem is that you are using:
{{ form_widget(form.termsAccepted) }}
for rendering the checkbox. This will only render the widget, whereas:
{{ form_row(form.termsAccepted) }}
as is used by all the user fields would contain the label, the widget and the error message. If you want to keep the widget, e.g. because using form_row messes up the template somehow you could render the errors individually using:
{{ form_errors(form.termsAccepted) }}
You might also want to check out the documentation on Form Customization.

Irregular Symfony exception: renderBlock() must be an instance of FormView, null given

We have a strange error in our Symfony 2 project. We use Symfony forms, and we don't have any problems in our development environment, but do receive messages of an error in production. An error we can't replicate, and never had. Everything works fine when we test (both in prod & dev) but we do receive notifications of this error happening.
We are notified of the error:
"exception":"[object] (Symfony\Component\Debug\Exception\FatalThrowableError(code: 0): Type error: Argument 1 passed to Symfony\Component\Form\FormRenderer::renderBlock() must be an instance of Symfony\Component\Form\FormView, null given
Most important code (Stripped for compactness & readability )
Formtype:
class WishlistType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rank', HiddenType::class)
->add('description', TextType::class)
// Image is deprecated and will be removed.
->add('image');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => Item::class,
]
);
}
}
Controller:
public function showAction(Request $request, $url)
{
/** #var Participant $participant */
$participant = $this->get('participant_repository')->findOneByUrl($url);
if ($participant === null) {
throw new NotFoundHttpException();
}
$wishlistForm = $this->createForm(
WishlistType::class,
$participant,
[
'action' => $this->generateUrl(
'wishlist_update',
['url' => $participant->getUrl()]
),
]
);
if (!$request->isXmlHttpRequest()) {
return [
'entry' => $participant,
'wishlistForm' => $wishlistForm->createView(),
];
}
}
Twig template:
{{ form_start(wishlistForm, {'attr': {'id': 'add_item_to_wishlist_form'}}) }}
{{ form_row(wishlistForm._token) }}
<table>
<!-- ADD SOMETHING TO YOUR WISHLIST -->
<thead>
<tr>
<th>#</th>
<th>{{ 'entry_show_valid.label.description'|trans }}</th>
<th>
<button type="button" class="btn btn-mini btn-success add-new-entry">
{{ 'entry_show_valid.btn.add_wishlist'|trans }}
</button>
</th>
</tr>
</thead>
<!-- OVERVIEW OF ITEMS IN WISHLIST, AND POSSIBILITY TO REMOVE SINGLE ITEMS-->
<tbody>
{% for item in wishlistForm.wishlistItems %}
<tr>
<td>{{ form_widget(item.rank) }}
<span>{{ item.rank.vars.value }}</span>
</td>
<td>
{{ form_widget(item.description, {'attr': {'class': 'wishlistitem-description'} }) }}
</td>
<td>
<button type="submit" >
<span>{{ 'entry_show_valid.btn.update_item'|trans }}</span>
</button>
<button type="button">
<span>{{ 'entry_show_valid.btn.remove_item'|trans }}</span>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ form_end(wishlistForm, {'render_rest': false}) }}
Form handling is done over Ajax, but request is handled as regular symfony form ($wishlistForm->handleRequest($request); etc...).
I don't understand how you can do this :
{% for item in wishlistForm.wishlistItems %}
because I don't see any 'wishlistItems' in your form declaration.
For me, this code has to fail at this line...

Symfony forms - password type

When I am trying to add password filed in my form type class:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text');
$builder->add('email', 'email');
$builder->add('password', 'password');
$builder->add('terms', 'checkbox', array(
'mapped' => false,
'constraints' => new NotBlank()
));
}
and then render this field in twig template {{ form_row(register.password) }} it appears like text filed. But I need it to be the password type <input type='password'...
Also form type is attached to entity with password attribute:
/**
* #var string
*
* #ORM\Column(name="password", type="string", length=32, nullable=false)
*/
private $password;
What is the reason?
The reason is that I use form theme:
{% block text_row %}
<div>
{{ block('form_label') }}
<div>
{{ block('form_widget_simple') }}
</div>
</div>
{% endblock %}
So I add new block:
{% block password_row %}
<div>
{{ block('form_label') }}
<div>
{{ block('password_widget') }}
</div>
</div>
{% endblock %}
And now all is OK
You should use Type to define field Type in $builder->add('child', 'type', ...)
$builder->add('username', TextType::class, array('label' => 'Username'))
->add('password', PasswordType::class, array('label' => 'Password'))
and in Twig
{{ form(form) }}
Not sure if this is the correct way, but did you try specifying a type to the field?
$builder->add('password', 'password', array('type'=>'password');

Categories