Создание формы регистрации и отправка писем на почту

24.02.2019 в 23:49
7210
+367

На прошлом уроке мы приступили с вами к созданию регистрации на сайте. Мы сделали не так много: создали сущность User и обновили базу. Сегодня мы начнем с того, что немного отредактируем код из прошлого урока, добавив туда еще два поля - confirmationCode и enabled - и конструктор. Первое поле нужно для кода подтверждения, который мы будем отправлять на почту, а второе - это булев тип, который принимает состояние false, если аккаунт еще не подтвержден, и true - когда подтвержден.

Итак, как мы уже сказали, нам нужны два новых поля для сущности User, давайте создадим их:

    /**
     * @var string
     *
     * @ORM\Column(type="string", nullable=true)
     */
    private $confirmationCode;

    /**
     * @var bool
     *
     * @ORM\Column(type="boolean")
     */
    private $enabled;

Также нам понадобятся геттеры и сеттеры для них, которые вы можете написать сами или позволить сгенерировать их за вас IDE:

   /**
     * @return string
     */
    public function getConfirmationCode(): string
    {
        return $this->confirmationCode;
    }

    /**
     * @param string $confirmationCode
     *
     * @return User
     */
    public function setConfirmationCode(string $confirmationCode): self
    {
        $this->confirmationCode = $confirmationCode;

        return $this;
    }

    /**
     * @return bool
     */
    public function getEnabled(): bool
    {
        return $this->enabled;
    }

    /**
     * @param bool $enabled
     *
     * @return User
     */
    public function setEnable(bool $enabled): self
    {
        $this->enabled = $enabled;

        return $this;
    }

Пока ничего необычного, правда? Поле confirmationCode будет содержать рандомную строку, которую мы будем отправлять на почту для подтверждения пользователем. Также после подтверждения поле enabled примет true и сделает человека полноправным пользователем нашего приложения. Не забудем сделать конструктор для сущности:

    public function __construct()
    {
        $this->roles = [self::ROLE_USER];
        $this->enabled = false;
    }

ROLE_USER - это константа, которую надо добавить в начале сущности User:

public const ROLE_USER = 'ROLE_USER';

Делайте так в любой ситуации, чтобы избежать магических переменных.

Теперь, кажется, мы закончили работу над нашей сущностью. По крайней мере, на данном этапе ее хватит для работы регистрации. Выполняем знакомые нам команды, чтобы доктрина применила изменения:

php bin/console doctrine:migrations:diff &&
php bin/console doctrine:migrations:migrate

Как вы уже знаете, многие настройки, которые дает нам Symfony, содержатся в файлах формата yaml. Любой пароль нужно шифровать, и Symfony предоставляет различные алгоритмы шифрования на выбор. Для этого вам нужно открыть файл config/security.yaml и после тега security уровнем ниже и чуть правее написать, какой алгоритм вы собираетесь использовать. В нашем случае это будет стандартный и надежный bcrypt. Так будет выглядеть файл:

security:
    encoders:
        App\Entity\User: bcrypt

Стандартная система безопасности фреймворка достаточно гибкая: она позволяет вам управлять данными, которые будут доставаться из сессии, а также данными, доставаемыми по разным признакам - например, из базы. Повторю код с новыми настройками:

security:
    encoders:
        App\Entity\User: bcrypt
    providers:
        database_users:
            entity:
                class: App\Entity\User,
                property: email

Здесь мы создали так называемый User провайдер, который знает, как доставать пользователя из базы по его email. Провайдер больше нужен для формы логина, но мы напишем его уже сейчас, а как он работает - узнаем в следующей статье.

Теперь нам нужно создать форму для регистрации пользователя:

<?php

declare(strict_types=1);

namespace App\Form;

use App\Entity\User;
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\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email', EmailType::class)
            ->add('plainPassword', RepeatedType::class, [
                'type' => PasswordType::class
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
           'data_class' => User::class
        ]);
    }
}

С формами мы уже работали, так что многое вам покажется знакомым. Мы используем EmailType, который требует от пользователя обязательного ввода электронного адреса, иначе форма не пройдет валидацию. RepeatedType - это парные поля, которые должны быть идентичны, чтобы валидация прошла успешно. Остальная конфигурация должна быть интуитивно понятна: поле типа PasswordType скроет ваш ввод.

Прежде чем писать регистрацию, предлагаю написать нужные нам сервисы: это Mailer и CodeGenerator - первый будет отправлять письма на почту, второй генерировать случайный хэш. В папке src/Service создайте класс Mailer.php. Symfony использует популярную и простую в использовании библиотеку Swift_Mailer, которая принимает объект Swift_Message, предварительно заполненный данными, и отправляет его на почту. Для создания сервиса Mailer нам нужны компоненты Swift_Mailer и Twig для рендеринга приветственного сообщения, которое отправится на почту пользователю. Инжектим их через конструктор и пишем наш метод sendConfirmation, который принимает в качестве аргумента сущность User. Вот так будет выглядеть наш класс:

<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\User;
use Swift_Mailer;
use Swift_Message;
use Twig_Environment;

class Mailer
{
    public const FROM_ADDRESS = '[email protected]';

    /**
     * @var Swift_Mailer
     */
    private $mailer;

    /**
     * @var Twig_Environment
     */
    private $twig;

    public function __construct(
        Swift_Mailer $mailer,
        Twig_Environment $twig

    )  {
        $this->mailer = $mailer;
        $this->twig = $twig;

    }

    /**
     * @param User $user
     *
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     */
    public function sendConfirmationMessage(User $user)
    {
        $messageBody = $this->twig->render('security/confirmation.html.twig', [
            'user' => $user
        ]);

        $message = new Swift_Message();
        $message
            ->setSubject('Вы успешно прошли регистрацию!')
            ->setFrom(self::FROM_ADDRESS)
            ->setTo($user->getEmail())
            ->setBody($messageBody, 'text/html');

        $this->mailer->send($message);
    }
}

Для начала рендерим форму, куда передаем юзера и возвращаем результат в переменную $messageBody. Дальше создаем объект класса Swift_Message и по цепочке заполняем данными: тема, откуда отправлено (это может быть корпоративный ящик вашего ресурса), кому отправляем (тут достаем ящик пользователя, который он только что ввел) и само тело сообщения - это будет красиво оформленный в html разметку текст со ссылкой для подтверждения аккаунта. И под конец отправляем сообщение методом send() класса Swift_Mailer. Как видим, у нас получился достаточно простой сервис, мы могли бы написать его даже стандартными функциями php, но зачем, когда есть готовый компонент Swift_Mailer.

Также нам нужно написать шаблон (security/confirmation.html.twig), который отправится на почту пользователю. Вы можете сделать его красивым, мне хватит такого:

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Подтверждение регистрации!</title>
</head>
<body>
<div class="container">
    <p>Добро пожаловать, {{ user.username }}!</p>
    <p>Чтобы завершить регистрацию, подтвердите <a href="{{ path('email_confirmation', {'code': user.confirmationCode }) }}">электронный адрес</a></p>
</div>
</body>
</html>

Теперь займемся другим сервисом - CodeGenerator. В этом классе будет всего один метод, генерирующий нам случайный код. Этот сервис вы можете написать сами, подойдет любая реализация. В любом случае вот готовый пример:

<?php

declare(strict_types=1);

namespace App\Service;

class CodeGenerator
{
    public const RANDOM_STRING = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

    /**
     * @return string
     */
    public function getConfirmationCode()
    {
        $stringLength = strlen(self::RANDOM_STRING);
        $code = '';

        for ($i = 0; $i < $stringLength; $i++) {
            $code .= self::RANDOM_STRING[rand(0, $stringLength - 1)];
        }

        return $code;
    }
}

Думаю, объяснять не нужно, здесь нет ничего от фреймворка, это чистый PHP.

Итак, займемся, наконец, классом RegisterController. Вы можете создать контроллер следующей командой:

php bin/console make:controller RegisterController. 

В этот раз я не буду использовать агрегацию, а покажу, как, используя наследование, работать с методами родительского класса для решения поставленной задачи. Вы можете полностью отказаться от наследования в пользу агрегации или композиции, неважно, в нашем случае это никак не влияет на работу приложения.

Что нам нужно передать в аргументы метода register()? Конечно, объект Request, наши сервисы Mailer и CodeGenerator, а также симфоневский компонент UserPasswordEncoderInterface, который зашифрует наш пароль: он принимает объект пользователя и plainPassword.

При регистрации происходит такой же порядок действий, как и при добавлении обычного поста: создаем объект сущности, резолвим форму и выполняем сохранение. Генерация случайного кода и хэширование пароля - это единственные особенности в данном случае. Также не забудем отправить сообщение на почту. Вот как в итоге будет выглядеть наш код:

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use App\Service\CodeGenerator;
use App\Service\Mailer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class RegisterController extends AbstractController
{
    /**
     * @Route("/register", name="register")
     */
    public function register(
        UserPasswordEncoderInterface $passwordEncoder,
        Request $request,
        CodeGenerator $codeGenerator,
        Mailer $mailer
    ) {
        $user = new User();
        $form = $this->createForm(
            UserType::class,
            $user
        );

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();

            $password = $passwordEncoder->encodePassword(
                $user,
                $user->getPlainPassword()
            );
            $user->setPassword($password);
            $user->setConfirmationCode($codeGenerator->getConfirmationCode());

            $em = $this->getDoctrine()->getManager();

            $em->persist($user);
            $em->flush();

            $mailer->sendConfirmationMessage($user);
        }

          return $this->render('security/register.html.twig', [
            'form' => $form->createView()
            ]);
    }

Не забудем реализовать шаблон security/register.html.twig:

{% extends 'base.html.twig' %}

{% block title %} Регистрация {% endblock %}

{% block body %}

    <div class="container">
        <h3 class="text-center">Регистрация</h3>
        {{ form_start(form) }}
        {{ form_row(form.email, {
            'label': ' '
        }) }}
        {{ form_row(form.plainPassword.first, {
            'label': ' ', 'class': 'form-control'
        }) }}
        {{ form_row(form.plainPassword.second, {
            'label': ' ', 'help': 'Введите пароль повторно'
        }) }}
        <button type="submit" class="btn btn-success" formnovalidate>Зарегистрироваться</button>
        {{ form_end(form) }}

    </div>
{% endblock %}

Уже сейчас вы можете пойти в браузер и посмотреть, что получилось. После регистрации можете посмотреть в дебаг бар и увидеть значок письма

Нажав на него, вы перейдете в профайлер Symfony и увидите письмо:

Так будет выглядеть письмо, отправленное на почту. Как я уже говорил, вы можете сделать его красивым.

Осталось написать action для подтверждения регистрации. Он будет доставать пользователя по его confirmationCode. Если такого пользователя нет, обрываем выполнение кода и возвращаем 404 (вы со своей стороны можете возвращать красивую страницу с такой же ошибкой). Если пользователь есть, ставим true в поле enable и '' в поле confirmationCode. Обновляем сущность и рендерим шаблон. Вот так будет выглядеть наш метод:

   /**
     * @Route("/confirm/{code}", name="email_confirmation")
     */
    public function confirmEmail(UserRepository $userRepository, string $code)
    {
        /** @var User $user */
        $user = $userRepository->findOneBy(['confirmationCode' => $code]);

        if ($user === null) {
          return new Response('404');
        }

        $user->setEnable(true);
        $user->setConfirmationCode('');

        $em = $this->getDoctrine()->getManager();

        $em->flush();

        return $this->render('security/account_confirm.html.twig', [
            'user' => $user,
        ]);
    }

Шаблон же будет выглядеть следующим образом:

{% extends 'base.html.twig' %}

{% block body %}
        <h5>Поздравляем!</h5>
        <p>Ваш аккаунт подтвержден, теперь вы можете
            <a href="{{ path('login') }}">войти</a></p>
{% endblock %}

Все, на данном этапе форма регистрации закончена. В следующем уроке мы сделаем вход/выход и кнопку "Запомнить меня".

loader
24.02.2019 в 23:49
7210
+367
Логические задачи с собеседований