Kontynuując wątek Symfony (Prosty system uwierzytelniania – logowanie) oraz bardzo przyjaznego systemu uwierzytelniania użytkowników, przyszedł czas na tworzenie nowego konta. Sama rejestracja konta jest już gotowa w DataFixtures, wystarczy wykorzystać ten mechanizm i dodać do niego obsługę formularza.
Czego potrzebujemy?
Bazując na poprzednim artykule (oraz repozytorium github) mamy już szkielet naszego systemu logowania. Teraz chcemy dodać kolejny element – rejestracja użytkownika. Do tego celu będziemy potrzebować przede wszystkim systemu formularzy. Instalujemy odpowiedni komponent poleceniem:
composer require symfony/form
Komponent zostanie automatycznie dodany do naszego projektu. Następnym krokiem jest delikatna modyfikacja naszej encji. Zakładamy, że adres e-mail oraz nazwa użytkownika, muszą być unikalne. W tym celu przechodzimy do pliku naszej encji i dodajemy parametr unique=true do obu wyżej wymienionych pól.
/** * @ORM\Column(type="string", length=128, unique=true) */ private $username; /** * @ORM\Column(type="string", length=128, unique=true) */ private $email;
Po zmianie encji, zaaplikować zmiany do naszej schemy. Do tego celu użyjemy komendy doctrine:schema:update z parametrem –force (zalecam przed wykonaniem tego polecenia ZAWSZE upewnić się, że wszystko jest ok przy pomocy flagi –dump-sql).
doctrine:schema:update --dump sql doctrine:schema:update --force
Od teraz nasza baza nie pozwoli nam na zapisanie zduplikowanych wartości w polach username oraz email. Wszystko już przygotowane, więc zabieramy się za implementowanie odpowiedniego kodu.
Formularz
Na początek tworzymy nowy plik w katalogu src/Form o nazwie UserType.php. Klasa formularza powinna dziedziczyć po klasie Symfony\Component\Form\AbstractType. Wewnątrz implementujemy jedną metodę buildForm przyjmująca te same parametry co parent.
Wewnątrz metody buildForm tworzymy strukturę naszego formularza. Potrzebujemy 4 pól: username, email oraz password i password repeat. Do tego celu użyjemy interfejsu FromBuilderInterface (pierwszy parametr metody) przypisany do $builder. Wywołanie metody add() na doda nam pole. Add przyjmuje 3 parametry – w naszym przypadku to nazwa pola, typ oraz listę dodatkowych opcji.
Kod, który stworzy nam potrzebną strukturę znajduje się poniżej.
<?php declare(strict_types=1); namespace App\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('username', TextType::class) ->add('email', EmailType::class) ->add('password', RepeatedType::class, [ 'type' => PasswordType::class, 'first_options' => ['label' => 'Password'], 'second_options' => ['label' => 'Repeat password'] ]); } }
Obsługa formularza
Obsługę formularza wykonujemy bezpośrednio po stronie naszego kontrolera. W src/Controller tworzymy nowy plik o nazwie RegistrationController.php. Wewnątrz deklarujemy metodę akcji o nazwie register przyjmująca kilka parametrów:
- Symfony\Component\HttpFoundation\Request $request
- Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface $passwordEncoder
- Doctrine\ORM\EntityManagerInterface $entityManager
- Symfony\Component\HttpFoundation\Session\Session $session
Pierwszym z nich jest obiekt Request z pakietu HttpFoundation, który pozwoli nam w łatwy sposób obsłużyć formularz. Kolejnym jest $passwordEncoder – obiekt umożliwiający nam zakodowanie naszego plain password. Trzecim parametrem jest $entityManager, który pozwoli nam na zapisanie nowej encji do bazy danych. Ostatnim obiektem jest $session z pakietu HttpFoundation. Pozwala on na tworzenie flash message, aby powiadomić użytkownika czy jego żądanie zostało przetworzone poprawnie.
Na początku metody dodajemy linijki odpowiedzialne za stworzenie instancji naszego formularza. Do tego celu użyjemy metody createForm dostępnego w naszym kontrolerze. Jako parametr przekazujemy nazwę klasy formularza (w naszym przypadku UserType::class).
Następnie, na wcześniej stworzonym obiekcie formularza, wywołujemy metodę handleRequest, która przyjmuje jako parametr nasz obiekt $request. W ten sposób wszystkie dane przekazane przez użytkownika w polach formularza zostaną poprawnie obsłużone i przypisane.
Kolejnym krokiem jest walidacja formularza. Sprawdzamy czy nasz formularz został wysłane (isSubmitted()) oraz czy jest poprawny (isValid()) – o samej walidacji napiszę w osobnym poście. Jeśli wszystko jest poprawne, przystępujemy do obsługi żądania użytkownika. Aby pobrać dane z formularza, wywołujemy metodę $form->get(‘{nazwa_pola}’)->getData(). W ten sposób możemy pobrać wartość każdego pola. Tworzymy nową instancję użytkownika przy pomocy new User() jako parametry przekazując wartości pola username oraz email.
$user = new User($form->get('username')->getData(), $form->get('email')->getData()); $password = $passwordEncoder->encodePassword($user, $form->get('password')->getData());
Mając już obiekt użytkownika, naszym następnym krokiem będzie zakodowanie hasła. Do tego celu użyjemy $passwordEncoder i metody encodePassword (w identyczny sposób jak w DataFixtures). Przypisujemy hasło do użytkownika i zapisujemy encję przy pomocy $entityManager’a.
$user->setPassword($password); $entityManager->persist($user); $entityManager->flush();
Z racji tego, że na chwilę obecną nie walidujemy naszego formularza, możemy mieć sytuację, w której dojdzie do duplikacji wartości w polach username lub email. Aby obsłużyć taką sytuację, flush() umieszczamy w try-catch’u i obsługujemy błąd (np. dodajemy komunikat).
Należy pamiętać również, aby po zapisie encji, wykonać przekierowanie http, po to, żeby uniknąć ewentualnego odświeżenia strony i ponownego wysłania formularza.
Ostatnim elementem jest wywołanie naszej templatki z formularzem i przekazaniem do niej widoku formularza.
<?php declare(strict_types=1); namespace App\Controller; use App\Entity\User; use App\Form\UserType; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class RegistrationController extends Controller { /** * @Route("/register", name="registration") */ public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, EntityManagerInterface $entityManager, Session $session) { $form = $this->createForm(UserType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $user = new User($form->get('username')->getData(), $form->get('email')->getData()); $password = $passwordEncoder->encodePassword($user, $form->get('password')->getData()); $user->setPassword($password); try { $entityManager->persist($user); $entityManager->flush(); $session->getFlashBag()->add('success', sprintf('Account %s has been created!', $user->getUsername())); return $this->redirectToRoute('home'); } catch (UniqueConstraintViolationException $exception) { $session->getFlashBag()->add('danger', 'Email and username has to be unique'); } } return $this->render('User/register.html.twig', [ 'form' => $form->createView() ]); } }
Wyświetlenie formularza
Bazując na tym samym szablonie, który mamy w formularzu logowania (patrz pierwsza część), tworzymy widok dla formularza rejestracji.
Plik o nazwie register.html.twig w katalogu templates/User o następującej zawartości
{% extends 'base.html.twig' %} {% block body %} <div class="row"> <div class="col-lg-12"> <h1 class="mt-5">Create new account</h1> <div class="col-lg-6"> {{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }} {{ form_label(form.username) }} {{ form_widget(form.username, {'attr': {'class': 'form-control'}}) }} {{ form_label(form.email) }} {{ form_widget(form.email, {'attr': {'class': 'form-control'}}) }} {{ form_label(form.password.first) }} {{ form_widget(form.password.first, {'attr': {'class': 'form-control'}}) }} {{ form_label(form.password.second) }} {{ form_widget(form.password.second, {'attr': {'class': 'form-control'}}) }} <br> <input type="submit" value="Create" class="btn btn-success"> {{ form_end(form) }} </div> </div> </div> {% endblock %}
Kosmetyka
Ostatnim krokiem jest dodanie kilku elementów. Pierwszym z nich, jest przycisk do rejestracji w głównym menu. Dodatkowo w głównym szablonie base.html.twig, bezpośrednio nad blokiem body, wstawiamy następujący fragment:
<div class="col-md-12"> {% for type, messages in app.session.flashbag.all() %} {% for message in messages %} <div class="alert alert-{{type}}">{{message}}</div> {% endfor %} {% endfor %} </div>
Ten fragment, pozwoli pobrać nam wszystkie wiadomości flash, które dodaliśmy w celu poinformowania użytkownika co dzieje się z jego żądaniem.
Podsumowanie
Jest to drugi artykuł z serii “system logowania w Symfony bez użycia FOSa“. Mam nadzieję, że widzicie jak proste jest stworzenie logowania bez dorzucania dużych i niepotrzebnych paczek.
Całość możesz znaleźć na githubie.
Uruchom przykład na Dockerze
Przykład opisany w tym artykule możesz pobrać z linku umieszczonego powyżej. Sam przykład uruchomić można bez konieczności instalowania PHP czy MySQL w systemie. Wystarczy, że uruchomisz go przy pomocy Doker’a. Jeśli nie wiesz jak to zrobić zapraszam po więcej informacji do artykułów Docker – pierwsze kroki [część pierwsza] i [część druga]
Instrukcja uruchomienia znajduje się w pliku README.