Чат веб-разработчиков
Система активации пользователей по email на PHP

Система активации пользователей по email на PHP

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

Прежде чем начать, вам нужно настроить OpenServer для отправки писем.

После того, как вы это сделали, можно приступать к написанию кода. Первое, что нам нужно – создать новую табличку, в которой мы будем хранить коды для активации пользователей.
Называем её «users_activation_codes», и указываем, что нам требуются три столбца:

  • id – это просто id записи в таблице;
  • user_id – id пользователя;
  • code – код для активации этого пользователя.

Таблица с кодами активации

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

src/MyProject/Models/Users/UserActivationService.php

Код доступен только после покупки курса ООП в PHP.

И теперь нам нужно сделать ещё один сервис, который будет предназначен для отправки email-сообщений.

src/MyProject/Services/EmailSender.php

Код доступен только после покупки курса ООП в PHP.

Здесь вам все функции уже знакомы – мы использовали похожий функционал для рендеринга шаблонов. Теперь у нас появится еще один тип шаблонов – специально для email-ов.

Давайте создадим наш первый шаблон, который будет предназначен для писем активации.

templates/mail/userActivation.php

Код доступен только после покупки курса ООП в PHP.

Ну и, собственно, остаётся только взять эти компоненты и собрать воедино. Делаем это в контроллере пользователя в экшене с регистрацией.

src/MyProject/Controllers/UsersController.php

Код доступен только после покупки курса ООП в PHP.

Теперь пробуем зарегистрироваться на свою почту в нашей системе.
Регистрация в системе на реальную почту

И после этого смотрим в базу данных.
Пользователи:
Зарегистрированный пользователь

Коды активации:
Код активации

Как видим, все успешно отработало, и кроме того, нам пришло письмо на почту!
Письмо с активацией

После перехода по ссылке мы видим, что такой страницы не существует.
Отсутствие страницы с активацией

Еще бы, ведь мы не добавляли для нее соответствующий роутинг. Добавляем его.

src/routes.php

Код доступен только после покупки курса ООП в PHP.

И добавляем соответствующий экшен в контроллере:

src/MyProject/Controllers/UsersController.php

Код доступен только после покупки курса ООП в PHP.

И добавляем у модели пользователя метод activate().

src/MyProject/Models/Users/User.php

Код доступен только после покупки курса ООП в PHP.

Снова пробуем обновить страничку для активации.

Успешная активация пользователя по email

Видим заветное “OK!”.

Проверяем, что в базе наш пользователь теперь подтвержден.
Активированный пользователь

Успех! Теперь осталось довести систему до ума – создать нормальные шаблоны для странички активации и обрабатывать возможные ошибки. Это вам предоставляется сделать самостоятельно в домашнем задании.

Читайте также
Комментарии


Sparky
Sparky

UsersController

    public function activate(int $userId, string $activationCode)
    {
        try
        {
            $user = User::getById($userId);
        } catch (UserActivationException $e)
        {
            $this->view->renderHtml('mail/userActivationError.php', [
                'error' => $e->getMessage()
            ],422);
            return;
        }

        try
        {
            $isCodeValid = UserActivationService::checkActivationCode($user, $activationCode);
        } catch (UserActivationException $e)
        {
            $this->view->renderHtml('mail/userActivationError.php', [
                'error' => $e->getMessage()
            ],422);
            return;
        }

        if ($isCodeValid) {
            $user->activate();
            echo 'OK!';
            UserActivationService::deleteActivationCode($userId, $activationCode);
        }
    }

UserActivationService

public static function checkActivationCode(User $user, string $code): bool
    {
        $db = Db::getInstance();
        $result = $db->query(
            'SELECT * FROM `' . self::TABLE_NAME . '` WHERE `user_id` = :user_id AND `code` = :code',
            [
                'user_id' => $user->getId(),
                'code' => $code
            ]
        );

        if (!$result)
        {
            throw new UserActivationException('Ошибка активации. Проверочный код не валидный.');
        }

        return !empty($result);
    }

    public static function deleteActivationCode(int $userId, string $activationCode): void
    {
        $db = Db::getInstance();
        $db->query(
            'DELETE FROM `' . self::TABLE_NAME . '` WHERE `user_id` = :userId AND `code` = :activationCode;', [
                ':userId' => $userId,
                ':activationCode' => $activationCode,
            ]
        );
    }

Так же для проверки на "если в ссылку активации подставить несуществующего пользователя" в методе getById бросаю исключение при получении пустого массива после запроса в БД. \
Все верно?

ivashkevich
ivashkevich
    public function activate(int $userId, string $activationCode)
    {
        try {
            $user = User::getById($userId);
        } catch (UserActivationException $e)

Неправильно - в процессе получения пользователя по id никак не должно бросаться исключение с типом UserActivationException. UserActivationException - только для ошибок, связанных с активацией. Само по себе получение пользователя из базы никак не связано с активацией.

echo 'OK!';

а где шаблон для успешного случая?

if (!$result)
        {
            throw new UserActivationException('Ошибка активации. Проверочный код не валидный.');
        }

        return !empty($result);

конкретно здесь !$result и !empty($result) - дадут просто противоположные значения. Не имеет смыла здесь кидать исключение - достаточно только вернуть true или false. Этот метод должен сказать, является ли код валидным или нет. То есть если он невалидный - это не исключительная ситуация, а вполне себе штатная. Поэтому исключение здесь не нужно.

Таким образом, в контроллере вам не нужно ничего ловить, нужно просто проверять пользователя на !== null и то что метод проверки кода вернул true.

Sparky
Sparky

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

ivashkevich
ivashkevich

Ничего, я тоже поначалу не понимал, даже когда устраивался на работу - это придет с опытом.

Kirill.K
Kirill.K

UsersActivationService:

    public static function existCode($userId): bool
    {
        $db = Db::getInstance();
        $code = $db->query(
            'SELECT code FROM ' . self::TABLE_NAME . ' WHERE user_id = :user_id',
            [
                'user_id' => $userId
            ]

        );
        return !empty($code);
    }

    public static function deleteActivationCode(int $userId): void
    {
        $db = Db::getInstance();
        $db->query(
            'DELETE FROM ' . self::TABLE_NAME . ' WHERE user_id = :user_id',
            [
                'user_id' => $userId
            ]
        );
    }

Функцию delete провожу без сверки с кодом, так как мы запускаем её непосредственно после проверки кода на валидность, или это открывает возможность появления каких-либо ошибок?

UsrsController:

public function activate(int $userId, string $activationCode)
    {
        try {
            $user = User::getById($userId);
            if ($user === null) {
                throw new ActivateException('Нет такого пользователя');
            }
            if ($user->getIsConfirmed() == 1) {
                throw new ActivateException('Пользователь уже активирован');
            }

            if (!UserActivationService::existCode($userId)) {
                throw new ActivateException('Не создан код активации');
            }

            $isCodeValid = UserActivationService::checkActivationCode($user, $activationCode);
            if ($isCodeValid) {
                $user->activate();
                UserActivationService::deleteActivationCode($userId);
                echo 'OK!';
            } else {
                echo 'Код активации не верен';
            }
        } catch (ActivateException $e) {
            $this->view->renderHtml('errors/noId.php', ['error' => $e->getMessage()]);
            return;
        }
    }

Я пытался учесть замечание Sparky, про то, что Activation-исключение бросается в процессе получения пользователя, но ума не приложу, как сделать правильно... нужно бросать другое исключение? или выносить catch в index?
Ещё не сделал шаблоны, ибо это не сложно, но времяёмко, а уже поздно и пора спать) Поэтому проторопился и сделал просто вывод месаджами(

ivashkevich
ivashkevich

1) Удаление кода без проверки кода в модели - норм. Ошибки вряд ли тут будут.
2) Логику, отвечающую за проверку существования кода можно перенести внутрь UserActivationService::checkActivationCode(). Если кода нет - просто возвращать false, исключение тут не нужно.
3) Исключения вы бросаете в нужном месте. Не надо ничего усложнять, у Вас все просто и понятно.
4) Когда код активации неверен - тоже можно бросить исключение.
5) Структуру кода можно переделать так, что сначала проверяются все исключительные ситуации и бросаются исключения, а затем, если все хорошо, просто работает код для успешного исхода. Суть такая:

if (что-то плохо) {
    бросаем исключение
}

if (что-то другое плохо) {
    бросаем исключение
}

если дошли до сюда, то просто рисуем шаблон, никаких if-ов уже не нужно.