Пишем регистрацию на сайте на PHP

09.09.2018 в 21:13
18582
+1029

Ну вот мы с вами и закончили изучение основных возможностей объектно-ориентированного программирования. Теперь мы будем учиться применять все эти возможности в деле. А именно – напишем движок для блога на чистом PHP. И в сегодняшнем уроке мы узнаем как сделать регистрацию на сайте. Благодаря ей новые пользователи смогут регистрироваться на нашем сайте. Поехали.

В одном из прошлых уроков мы с вами уже создавали таблицу users, которая имеет следующий вид:

Именно её мы и будем использовать для регистрации и авторизации пользователей c помощью PHP и MySQL. Давайте первым делом составим список полей, которые мы должны будем принимать от пользователя для регистрации:

  • nickname – должен быть уникальным и содержать только символы латинского алфавита и цифры;
  • email – должен быть уникальным и быть корректным email-ом;
  • password – должен быть не менее 8 символов (будет захеширован и будет храниться в поле password_hash).

Все остальные поля будут заполняться значениями по-умолчанию на стороне сервера.

Создаём контроллер

Итак, давайте создадим контроллер для работы с пользователями.

src/MyProject/Controllers/UsersController.php

<?php

namespace MyProject\Controllers;

use MyProject\View\View;

class UsersController
{
    /** @var View */
    private $view;

    public function __construct()
    {
        $this->view = new View(__DIR__ . '/../../../templates');
    }

    public function signUp()
    {
        echo 'здесь будет код для регистрации пользователей';
    }
}

Добавляем роут

Теперь давайте пропишем роутинг для данной странички. Пусть это будет myproject.loc/users/register

src/routes.php

<?php

return [
    '~^articles/(\d+)$~' => [\MyProject\Controllers\ArticlesController::class, 'view'],
    '~^articles/(\d+)/edit$~' => [\MyProject\Controllers\ArticlesController::class, 'edit'],
    '~^articles/add$~' => [\MyProject\Controllers\ArticlesController::class, 'add'],
    '~^users/register$~' => [\MyProject\Controllers\UsersController::class, 'signUp'],
    '~^$~' => [\MyProject\Controllers\MainController::class, 'main'],
];

Проверим, что всё работает:

Создаём шаблон

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

templates/users/signUp.php

<?php include __DIR__ . '/../header.php'; ?>
    <div style="text-align: center;">
        <h1>Регистрация</h1>
        <form action="/users/register" method="post">
            <label>Nickname <input type="text" name="nickname"></label>
            <br><br>
            <label>Email <input type="text" name="email"></label>
            <br><br>
            <label>Пароль <input type="password" name="password"></label>
            <br><br>
            <input type="submit" value="Зарегистрироваться">
        </form>
    </div>
<?php include __DIR__ . '/../footer.php'; ?>

И теперь отрендерим этот шаблон в нашем контроллере:

src/MyProject/Controllers/UsersController.php

<?php

namespace MyProject\Controllers;

use MyProject\View\View;

class UsersController
{
    /** @var View */
    private $view;

    public function __construct()
    {
        $this->view = new View(__DIR__ . '/../../../templates');
    }

    public function signUp()
    {
        $this->view->renderHtml('users/signUp.php');
    }
}

Проверяем, что все работает

Пишем логику регистрации

Итак, заготовочку сделали, теперь – самое интересное. Нам нужно обработать данные, пришедшие от пользователя. Принимать данные из запроса мы будем внутри контроллера, однако вся логика по проверке этих данных будет находиться внутри модели пользователя. То есть принять данные из запроса – это всегда ответственность контроллера. Далее его задача – передать эти данные модели, чтобы она произвела с ними какие-то действия(проверила на валидность, сохранила в базу). Контроллеры не должны содержать в себе бизнес-логику (то есть то, как должны обрабатываться и храниться данные). За счет того, что такая логика всегда содержится в моделях, код становится проще реиспользовать (использовать в нескольких местах). Например, мы можем из разных контроллеров обращаться к одному и тому же коду, который хранится в модели. Итак, давайте создадим в модели пользователя статический метод, который будет принимать на вход массив с данными, пришедшими от пользователя, и будет пытаться создать нового пользователя и сохранить его в базе данных.

src/MyProject/Models/Users/User.php

...
public static function signUp(array $userData)
{
    var_dump($userData);
}

Именно этот код мы и будем вызывать в контроллере, если пришел POST-запрос.

src/MyProject/Controllers/UsersController.php

    public function signUp()
    {
        if (!empty($_POST)) {
            $user = User::signUp($_POST);
        }

        $this->view->renderHtml('users/signUp.php');
    }

Проверка входных данных на пустоту

Итак, приступаем к написанию логики в модели. Для начала стоит убедиться в том, что все необходимые данные были переданы в запросе. Если это не так – будем кидать исключение. Для такого рода ошибок заведем отдельное исключение:

src/MyProject/Exceptions/InvalidArgumentException.php

<?php

namespace MyProject\Exceptions;

class InvalidArgumentException extends \Exception
{
}

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

Итак, пишем проверки на то, что все данные были переданы:

src/MyProject/Models/Users/User.php

public static function signUp(array $userData)
{
    if (empty($userData['nickname'])) {
        throw new InvalidArgumentException('Не передан nickname');
    }

    if (empty($userData['email'])) {
        throw new InvalidArgumentException('Не передан email');
    }

    if (empty($userData['password'])) {
        throw new InvalidArgumentException('Не передан password');
    }
}

В контроллере теперь нужно научиться обрабатывать эти исключения:

src/MyProject/Controllers/UsersController.php

public function signUp()
{
    if (!empty($_POST)) {
        try {
            $user = User::signUp($_POST);
        } catch (InvalidArgumentException $e) {
            $this->view->renderHtml('users/signUp.php', ['error' => $e->getMessage()]);
            return;
        }
    }

    $this->view->renderHtml('users/signUp.php');
}

Видите, мы начали передавать в шаблон переменную error? Нужно бы уметь выводить её в шаблоне, если она не пустая:

templates/users/signUp.php

<?php include __DIR__ . '/../header.php'; ?>
    <div style="text-align: center;">
        <h1>Регистрация</h1>
        <?php if (!empty($error)): ?>
            <div style="background-color: red;padding: 5px;margin: 15px"><?= $error ?></div>
        <?php endif; ?>
        <form action="/users/register" method="post">
            <label>Nickname <input type="text" name="nickname"></label>
            <br><br>
            <label>Email <input type="text" name="email"></label>
            <br><br>
            <label>Пароль <input type="password" name="password"></label>
            <br><br>
            <input type="submit" value="Зарегистрироваться">
        </form>
    </div>
<?php include __DIR__ . '/../footer.php'; ?>

Теперь попробуем отправить форму регистрации с пустыми полями:

Увидим, что код упал на первой же проверке.

Попробуем заполнить теперь поле nickname и снова отправим запрос.

Как видим, теперь уже ошибка о том, что мы не заполнили email. Отлично, значит наш код действительно проверяет наличие полей. Однако данные, которые мы заполняли перед отправкой формы потеряны, что, согласитесь неудобно, так как придется возвращаться назад. Давайте будем выводить в шаблоне данные, которые были переданы в запросе. Для этого используется атрибут value у тегов input.

<?php include __DIR__ . '/../header.php'; ?>
    <div style="text-align: center;">
        <h1>Регистрация</h1>
        <?php if (!empty($error)): ?>
            <div style="background-color: red;padding: 5px;margin: 15px"><?= $error ?></div>
        <?php endif; ?>
        <form action="/users/register" method="post">
            <label>Nickname <input type="text" name="nickname" value="<?= $_POST['nickname'] ?? '' ?>"></label>
            <br><br>
            <label>Email <input type="text" name="email" value="<?= $_POST['email'] ?? '' ?>"></label>
            <br><br>
            <label>Пароль <input type="password" name="password" value="<?= $_POST['password'] ?? '' ?>"></label>
            <br><br>
            <input type="submit" value="Зарегистрироваться">
        </form>
    </div>
<?php include __DIR__ . '/../footer.php'; ?>

Попробуем снова отправить форму с заполненным полем nickname.

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

Проверка данных на валидность

Добавим также проверку на то, что длина пароля не менее восьми символов, а также что nickname содержит только допустимые символы, а email является корректным email-ом:

src/MyProject/Models/Users/User.php

public static function signUp(array $userData)
{
    if (empty($userData['nickname'])) {
        throw new InvalidArgumentException('Не передан nickname');
    }

    if (!preg_match('/^[a-zA-Z0-9]+$/', $userData['nickname'])) {
        throw new InvalidArgumentException('Nickname может состоять только из символов латинского алфавита и цифр');
    }

    if (empty($userData['email'])) {
        throw new InvalidArgumentException('Не передан email');
    }

    if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('Email некорректен');
    }

    if (empty($userData['password'])) {
        throw new InvalidArgumentException('Не передан password');
    }

    if (mb_strlen($userData['password']) < 8) {
        throw new InvalidArgumentException('Пароль должен быть не менее 8 символов');
    }
}

Поиск дубликатов

Теперь стоит проверить, что пользователя с такими email и nickname нет в базе. Для этого нам понадобится метод, позволяющий находить записи в базе по какому-то одному столбцу. Давайте добавим его в класс ActiveRecordEntity.

src/MyProject/Models/ActiveRecordEntity.php

public static function findOneByColumn(string $columnName, $value): ?self
{
    $db = Db::getInstance();
    $result = $db->query(
        'SELECT * FROM `' . static::getTableName() . '` WHERE `' . $columnName . '` = :value LIMIT 1;',
        [':value' => $value],
        static::class
    );
    if ($result === []) {
        return null;
    }
    return $result[0];
}

Этот метод будет принимать два параметра:

  1. имя столбца, по которому искать;
  2. значение, которое мы ищем в этом столбце.

Если ничего не найдено – вернётся null. Если же что-то нашлось – вернётся первая запись.
С помощью этого метода мы сможем искать пользователей по email и nickname:

src/MyProject/Models/Users/User.php

public static function signUp(array $userData)
{
    ...

    if (static::findOneByColumn('nickname', $userData['nickname']) !== null) {
        throw new InvalidArgumentException('Пользователь с таким nickname уже существует');
    }

    if (static::findOneByColumn('email', $userData['email']) !== null) {
        throw new InvalidArgumentException('Пользователь с таким email уже существует');
    }
}

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

public static function signUp(array $userData): User
{
    // ... тут все проверки

    $user = new User();
    $user->nickname = $userData['nickname'];
    $user->email = $userData['email'];
    $user->passwordHash = password_hash($userData['password'], PASSWORD_DEFAULT);
    $user->isConfirmed = false;
    $user->role = 'user';
    $user->authToken = sha1(random_bytes(100)) . sha1(random_bytes(100));
    $user->save();

    return $user;
}

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

В конце метода мы сохраняем этого нового пользователя в базу и возвращаем его из метода. Обратите внимание, что у метода появился тип возвращаемого значения: User – не забывайте их указывать.
Теперь в контроллере нужно получать результат этого метода и проверять, что нам вернулся только что созданный пользователь. Если всё ок – писать об этом на форме регистрации.

src/MyProject/Controllers/UsersController.php

public function signUp()
{
    if (!empty($_POST)) {
        try {
            $user = User::signUp($_POST);
        } catch (InvalidArgumentException $e) {
            $this->view->renderHtml('users/signUp.php', ['error' => $e->getMessage()]);
            return;
        }

        if ($user instanceof User) {
            $this->view->renderHtml('users/signUpSuccessful.php');
            return;
        }
    }

    $this->view->renderHtml('users/signUp.php');
}

Я решил не загромождать наш шаблон всем подряд и создал для успешной регистрации отдельный шаблончик - signUpSuccessful.php. Как видите, именно его я и рендерю в контроллере при успешном исходе.

templates/users/signUpSuccessful.php

<?php include __DIR__ . '/../header.php'; ?>
    <div style="text-align: center;">
        <h1>Регистрация прошла успешно!</h1>
        Ссылка для активации вашей учетной записи отправлена вам на email.
    </div>
<?php include __DIR__ . '/../footer.php'; ?>

Пробуем теперь заполнить нашу формочку корректными данными и отправляем запрос.

Заглянем теперь в базу и убедимся, что действительно появился новый пользователь

Вот мы и сделали регистрацию для нашего блога. В следующем уроке мы сделаем активацию пользователя по email-у.

Текущая версия проекта на github.

loader
09.09.2018 в 21:13
18582
+1029
Комментарии
Этот урок набрал набрал достаточно большое количество комментариев и дальнейшее его комментирование отключено. Если вы хотели убедиться в правильности выполнения ДЗ или у вас возник вопрос по уроку, посмотрите ранее добавленные комментарии, кликнув по кнопке ниже. Скорее всего вы найдете там то, что искали. Если это не помогло - задайте вопрос в чате в телеграме - https://t.me/php_zone
Логические задачи с собеседований