Custom form for user password in Symfony 4 - php

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.

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?

Symfony 4.1.4 isSubmitted() doesn't work on rendered form

So I have this simple form:
class CreditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('credits', EntityType::class, [
'class' => Product::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('product')
->orderBy('product.amount');
},
'expanded' => true,
'choice_label' => function ($key) {
return $key->amount.' credits voor maar '.$key->price;
}
]);
}
}
which I use in the following Controller:
public function credits(Request $request)
{
$form = $this->createForm(CreditType::class);
$form->handleRequest($request);
if ($form->isSubmitted()) {
throw new \Exception('It works!');
}
return $this->render(
'credits/credits_widget.html.twig',
[
'form' => $form->createView(),
]
);
}
And use this in my XXX.html.twig
{{ render(controller('App\\Controller\\DetailsController:credits')) }}
I have been working with symfony for 4 weeks now so not that long.
I want to call some functions when the form is submitted only nothing that I place in the if statement is working, it doesn't throw any exception at the moment. Am I not seeing something or is it not possible to do this when I render a form?
EDIT:
my credits_widget.html.twig
{% block credits %}
{{ form_start(form.credits) }}
{{ form_widget(form.credits) }}
{% if app.user %}
<button type="submit">Koop nu!</button>
{% else %}
<button type="button">Login en koop</button>
<button type="button">Registreer en koop</button>
{% endif %}
{{ form_end(form.credits) }}
{% endblock %}
The problem is that the form is rendered without action and when submitted, it is submitted by default to the root route from which it was rendered. So the credits action is not being called. You should change the action of the form like this:
public function credits(Request $request)
{
// set current url path as form's action
$form = $this->createForm(CreditType::class, null, , [
'action' => $request->getRequestUri()
]);
$form->handleRequest($request);
if ($form->isSubmitted()) {
throw new \Exception('It works!');
}
return $this->render(
'credits/credits_widget.html.twig',
[
'form' => $form->createView(),
]
);
}
EDIT: You are currently rendering subform form.credits when you should really render form as it is passed from your controller. Your credits_widget.html.twig template should look like this:
{% block credits %}
{{ form_start(form) }}
{{ form_widget(form) }}
{% if app.user %}
<button type="submit">Koop nu!</button>
{% else %}
<button type="button">Login en koop</button>
<button type="button">Registreer en koop</button>
{% endif %}
{{ form_end(form) }}
{% endblock %}

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.

Symfony - Using twig extension with form prevents errors from showing up

I'm trying to use twig extensions as widgets to render sections of the page.
I have this class EventWidget, which is a twig extension that defines a twig function called event_widget_create that's responsible for rendering the create page of my entity.
The problem is, There's no form errors printed inside the view, even though there're errors displayed when I do var_dump($form->getErrorsAsString()) inside the controller or the widget function.
There's something that I noticed, when clicking submit, the fields data disappears and I get the feeling that the page reloads.
Here's my widget extension code:
class EventWidget extends \Twig_Extension
{
/**
* #var Twig_Environment
*/
protected $env;
/**
* #var \Tsk\FEBundle\FormHandler\EventFormHandler
*/
protected $eventFormHandler;
function __construct(EventFormHandler $eventFormHandler)
{
$this->eventFormHandler = $eventFormHandler;
}
/**
* #param Twig_Environment $environment
*/
public function initRuntime(Twig_Environment $environment)
{
$this->env = $environment;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction("event_widget_create", [$this, "getEventWidgetCreate"]),
];
}
public function getEventWidgetCreate(FormInterface $form)
{
return $this->env->render("#Default/Partial/events_widget_create.html.twig",[
"form" => $form->createView()
]);
}
/**
* #return string
*/
public function getName()
{
return "event_widget";
}
}
And here's my controller:
/**
* #Route("/")
*
* #package Tsk\FEBundle\Controller
*/
class EventController extends Controller
{
/**
* #Route("/event/create", name="tsk_fe_event_create")
* #Template("#Default/event_create.html.twig")
* #Security("has_role('ROLE_ARTIST')")
*/
public function createEventAction(Request $request)
{
$handler = $this->get("tsk_fe.event_form.handler");
$form = $handler->create();
if ($request->getMethod() == "POST") {
if ($handler->process($form)) {
return new RedirectResponse($this->generateUrl("tsk_fe_default_index"));
}
}
return ["form" => $form];
}
}
And the form:
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add("title", "text", [
"error_bubbling" => true
])
->add("description", "textarea", [
"required" => false
])
->add("start", "thrace_datetimepicker", [
"label" => "From",
"error_bubbling" => true
])
->add("end", "thrace_datetimepicker", [
"label" => "To",
"error_bubbling" => true
])
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
"data_class" => "Tsk\FEBundle\Entity\Event",
]);
}
public function getName()
{
return "event";
}
}
And lastly the view:
This's the main view:
"#Default/event_create.html.twig"
{% extends "#TskFE/layout.html.twig" %}
{% block content %}
{{ event_widget_create(form)|raw }}
{% endblock content %}
And this's the partial view that's rendered by the widget:
"#Default/Partial/events_widget_create.html.twig":
{% form_theme form with ['ThraceFormBundle:Form:fields.html.twig'] %}
{{ form_start(form, {"action": url("tsk_fe_event_create"), 'attr': {'novalidate': 'novalidate'} }) }}
<div class="form-group">
{{ form_label(form.title) }}
{{ form_widget(form.title, {"attr":{"class": "form-control"} }) }}
{{ form_errors(form.title) }}
</div>
<div class="form-group">
{{ form_label(form.description) }}
{{ form_widget(form.description, {"attr":{"class": "form-control"} }) }}
{{ form_errors(form.description) }}
</div>
<div class="form-group">
{{ form_label(form.start) }}
{{ form_widget(form.start, {"attr":{"class": "form-control"} }) }}
{{ form_errors(form.start) }}
</div>
<div class="form-group">
{{ form_label(form.end) }}
{{ form_widget(form.end, {"attr":{"class": "form-control"} }) }}
{{ form_errors(form.end) }}
</div>
<input type="submit" value="Submit" class="btn btn-info"/>
Cancel
{{ form_rest(form) }}
{{ form_end(form) }}
It seems you have bubbled your errors by using 'error_bubbling' => true. Not sure if it is what you want because you render error individually by field.
Error bubbling means all errors are attached to the parent form instead of being attach to the related field. So, if it is what you want, you must add {{ form_errors(form) }} on top of your form in your template in order to get your errors displayed otherwise, just remove the error_bubbling option.

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