Регистрация и авторизация пользователей на ООП на PHP

Описание простого алгоритма авторизации и регистрации пользователей в объектно-ориентированном стиле на PHP, актуального в 2024 году

Дата публикации: 12.01.2024

Пример алгоритма авторизации и регистрации

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

  1. Процедурный стиль меняем на объектно-ориентированный.
  2. Подключение к СУБД будем осуществлять через созданный для этих целей класс.
  3. Доступ к информации о пользователе будем получать через объекты.

Если кому-то лень читать весь текст статьи, и просто нужно знать, что делает весь этот, для вас есть короткий путь:

1. Классы и объекты для работы сценария

Все наши пользовательские функции мы подключали из файла bootstrap.php через выражение require_once('bootstrap.php');. Теперь таким же образом будем подключать и наши классы. С одним нюансом. Мы будем подключать не каждый класс, который далее создадим, а лишь один — класс ядра Core. А уже с его помощью автоматически будут подключаться классы, которые мы будем вызывать.

Реализуем этот алгоритм мы с помощью функции spl_autoload_register(). И смотрим на код нового файла bootstrap.php и код файла classes/core/core.php.

Файл bootstrap.php


            <?php
            // Создаем константу, в которой будет храниться путь к директории сайта на сервере
            define('SITE_FOLDER', dirname(__FILE__) . DIRECTORY_SEPARATOR);

            /**
            * Создаем константу, которая предотвратит прямой доступ к файлам наших классов через веб-сервер
            * Выражением defined('MYSITE') || exit('Прямой доступ к файлу запрещен') мы этот доступ и ограничим
            */
            define('MYSITE', TRUE);

            // Для запрета выполнения ini_set в сценариях нужно установить в TRUE
            define('DENY_INI_SET', FALSE);

            if (!defined('DENY_INI_SET') || !DENY_INI_SET)
            {
                ini_set('display_errors', 1);
            }

            // Подключаем класс ядра
            require_once(SITE_FOLDER . "classes" . DIRECTORY_SEPARATOR . "core" . DIRECTORY_SEPARATOR . "core.php");

            // Инициализируем ядро системы
            Core::init();
            ?>
        

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

Файл classes/core/core.php


            <?php
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            class Core 
            {
                // Статус инициализации ядра системы
                static private $_init = FALSE;

                // Путь к месту хранения файлов с определением пользовательских классов
                static public $classesPath = NULL;
                
                /**
                * Возвращает стаус инициализации ядра системы
                * @return boolean TRUE | FALSE
                */
                static public function isInit()
                {
                    return static::$_init;
                }

                /**
                * Инициализирует ядро системы
                */
                static public function init()
                {
                    // Если ядро уже было инициализировано, ничего не делаем
                    if (static::isInit())
                    {
                        return TRUE;
                    }

                    // Устанавливаем путь к месту хранения файлов с определениями пользовательских классов на сервере
                    static::setClassesPath();

                    // Установим кодировку UTF-8
                    mb_internal_encoding('UTF-8');
                }

                /**
                * Устанавливает пути к месту хранения файлов с определениями пользовательских классов на сервере
                */
                static public function setClassesPath()
                {
                    static::$classesPath = SITE_FOLDER . "classes" . DIRECTORY_SEPARATOR;
                }
            }
            ?>
        

Здесь также из комментариев внутри файла всё должно быть понятно и очевидно. Теперь, почему бы сразу же не подключиться к СУБД при инициализации ядра? В конец статического метода Core::init() допишем код Core_Database::instance()->connect();.

Почему именно так? Потому что класс подключения к СУБД сделаем статическим, чтобы не плодить множество подключений. Выражение Core_Database даёт понять интерпретатору PHP, что объявление класса нужно искать в файле classes/core/database.php. Но если всё сделать так, и попробовать просмотреть страницу, мы получим ошибку: Fatal error: Uncaught Error: Class "Core_Database" not found in....

И это логично. Интерпретатор, пока что, не в курсе, где искать этот класс. Для этого нам и понадобится функция spl_autoload_register(). Дополняем код статического метода Core::init(), и дописываем в него частный статический метод Core::_autoload() и ещё кое-чего. Подключение к СУБД заключаем в блок try catch.


            <?php
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            class Core 
            {
                /**** .......Дополняем код класса Core ....... */
                
                // Путь к месту хранения файлов с определением пользовательских классов
                static public $classesPath = NULL;

                // Свойство, в котором будет храниться информация об автозагруженных классах
                static private $_autoloadCache = [];
                
                /**
                * Автозагрузка определений классов
                */
                private static function _autoload($class)
                {
                    // Если ранее класс уже был загружен, ничего не делаем
                    if (isset(static::$_autoloadCache[$class]))
                    {
                        return static::$_autoloadCache[$class];
                    }
                    
                    $return = FALSE;
                    
                    // Получаем путь к файлу на сервере с определением класса
                    $sPath = static::$classesPath . static::getClassPath($class);

                    // Если такой файл на диске есть, подключаем его
                    if (is_file($sPath))
                    {
                        include($sPath);

                        $return = TRUE;
                    }

                    static::$_autoloadCache[$class] = $return;

                    return $return;
                }

                /*** ................  */
                static public function init()
                {
                /*** .................. */
                    
                // Инициализируем наши пользовательские классы
                    static::registerCallbackFunction();

                    /** .................... */

                    try {
                        // Устанавливаем соединение с СУБД
                        Core_Database::instance()->connect();
                    }
                    // В случае ошибки останавливаем работу сценария
                    catch (Exception $e)
                    {
                        echo "<p>{$e->getMessage()}</p>";

                        die();
                    }
                }

                /** ..................... */

                /**
                * Регистрирует реализации пользовательских классов
                */
                static public function registerCallbackFunction()
                {
                    spl_autoload_register(array('Core', '_autoload'));
                }

                /**
                * Получает путь к файлу на сервере с определением класса
                */
                static public function getClassPath($class) : string
                {
                    // Разделяем имя искомого класса на составляющие
                    $aClassName = explode("_", strtolower($class));

                    // Последний элемент полученного массива будет являться именем файла
                    $sFileName = array_pop($aClassName);

                    // Собираем путь к файлу
                    // Если имя класса было передано без символа разделителя _
                    $sPath = empty($aClassName) 
                                ? $sFileName . DIRECTORY_SEPARATOR 
                                : implode(DIRECTORY_SEPARATOR, $aClassName) . DIRECTORY_SEPARATOR; 

                    // Добавляем имя файла
                    $sPath .= $sFileName . ".php";
                    
                    // Возвращаем путь к файлу
                    return $sPath;
                }
            }
            ?>
        

Теперь код класса Core выглядит так. Но мы всё равно имеем ошибку: Fatal error: Uncaught Error: Class "Core_Database" not found in.... Конечно же, ведь нам нужно создать файл classes/database.php, и объявить в нем класс Core_Database.

2. Класс Core_Database для работы с СУБД

Но сначала давайте посмотрим на немного измененный файл database.php с параметрами подключения к СУБД, который теперь перенесен в каталог classes/core/config/.


            <?php
            return [
                'pdo' => [
                    'host' => 'localhost',
                    'user' => 'demo',
                    'password' => 'fy)s@6!9cJ*g!xvZ',
                    'dbname' => 'demo_auth_reg_1'
                ]
            ];
            ?>
        

Здесь в верхнем уровне массива имеется только ключ pdo. Для подключения к СУБД MySQL через PDO и указаны параметры. Если бы мы хотели работать через mysqli или иной драйвер, нужно было бы аналогично указать для него конфигурацию. Теперь смотрим на код класса Core_Database.


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Абстраактый класс
            * Обеспечивает подключение к СУБД
            * Класс является абстрактным, так как оставляет пользователю право определять, через какой
            * модуль будет реализовано взаимодействие с СУБД
            * 
            * Реализация такого взаимодействия должна быть написана в дочерних классах
            * Например, вызов Core_Database::instance('mysql') вернет экземпляр класса Core_Database_Mysql
            */
            abstract class Core_DataBase
            {
                // Экземпляр класса
                static protected $_instance = [];

                // Параметры подключение к СУБД
                protected $_config = [];

                // Здесь будет храниться последний выполненный запрос к БД
                protected $_lastQuery = NULL;

                /** Абстрактные методы не имеют реализации. Они должны быть реализованы в дочерних классах */

                // Подключение к СУБД
                abstract public function connect();

                // Отключение от СУБД
                abstract public function disconnect();

                // Установка кодировки соединения
                abstract public function setCharset($charset);

                // Экранирование данных
                abstract public function escape($unescapedString);

                /**
                * Защищенный конструктор класса, который невозможно вызвать откуда-либо, кроме как из самого класса
                * Получает параметры подключения к СУБД
                */
                protected function __construct(array $config)
                {
                    $this->setConfig($config);
                }

                /**
                * Возвращает и при необходимости создает экзепляр класса
                * @return object Core_Database
                */
                static public function instance(string $name = 'pdo')
                {
                    // Если экземпляр класса не был создан
                    if (empty(static::$_instance[$name]))
                    {
                        // Получаем параметры подключения к СУБД
                        $aConfig = Core::$config->get('core_database', array());
                        
                        if (!isset($aConfig[$name]))
                        {
                            throw new Exception('Для запрошенного типа подключения к СУБД нет конфигурации');
                        }

                        // Определяем, какой именно класс будем использовать
                        // Он будет именоваться Core_Database_{$name}, например Core_Database_Pdo или Core_Database_Mysql
                        $driver = __CLASS__ . "_" . ucfirst($name);
                        
                        static::$_instance[$name] = new $driver($aConfig[$name]);
                    }

                    // Возвращем вызову экземпляр класса для работы с СУБД
                    return static::$_instance[$name];
                }

                public function setConfig(array $config)
                {
                    $this->_config = $config + [
                        'host' => 'localhost',
                        'user' => '',
                        'password' => '',
                        'dbname' => NULL,
                        'charset' => 'utf8'
                    ];

                    return $this;
                }
            }
            ?>
        

Класс является абстрактным. Основные методы взаимодействия с СУБД будут реализованы в дочерних классах. В этой статье речь пойдет о работе через PDO, а значит — Core_Database_Pdo.

Конструктор класса Core_Database нельзя вызывать напрямую. Вызывается статический метод Core_Database::instance(), которому можно передать наименование драйвера для взаимодействия с СУБД. В случае отсутствия конфигурации для такого драйвера будет создано исключение, которое остановит работу сценария. По умолчанию используется драйвер PDO.

Здесь же в методе Core_Database::inctance() мы видим вызов статического метода Core_Config::instance(). А этот класс ранее не упоминался, так как не был нужен. Именно он получает параметры конфигурации, хранящиеся на сервере в отдельных файлах. В этом случае запрашиваются параметры конфигурации для подключения к СУБД.

Посмотрим, как выглядит класс Core_Config.


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Возвращает параметры конфигурации чего-либо
            */
            class Core_Config
            {
                // Экземпляр класса
                static protected $_instance = NULL;

                // Загруженные параметры
                protected $_values = [];

                /**
                * Возвращает последний компонент имени из указанного пути
                * https://www.php.net/manual/ru/function.basename
                * @param string $name
                * @return string
                */
                protected function _correctName($name) : string
                {
                    return basename(strtolower($name));
                }

                /**
                * Возвращает и при необходимости создает экзепляр класса
                * @return object Core_Config
                */
                static public function instance()
                {
                    // Если экземпляр класса ранее уже был создан, его и возвращаем
                    if (!is_null(static::$_instance))
                    {
                        return static::$_instance;
                    }

                    // Создаем экземпляр класса, и сохраняем его здесь же
                    static::$_instance = new static();

                    // Возвращем вызову экземпляр класса
                    return static::$_instance;
                }
                
                /**
                * Получает параметры чего-либо из файла на сервере по запрошенному имени файла, к примеру Core_Database подключит classes/core/config/database.php
                * @param string $name
                * @return mixed Config | NULL
                */
                public function get($name)
                {
                    $name = $this->_correctName($name);

                    // Если ранее не запрашивались параметры с таким именем
                    if (!isset($this->_values[$name]))
                    {
                        // Получаем путь к нужному файлу
                        $sPath = $this->getPath($name);

                        // Если такой путь существует
                        $this->_values[$name] = is_file($sPath)
                            ? require_once($sPath)
                            : NULL;
                    }

                    return $this->_values[$name];
                }

                /**
                * Получает путь к файлу с параметрами
                * @param string $name
                * @return string
                */
                public function getPath($name)
                {
                    // Разбираем строку с переданным именем на составляющие
                    $aConfig = explode('_', $name);

                    // Последним элементом будет имя файла
                    $sFileName = array_pop($aConfig);

                    // Собираем путь к файлу
                    $path = Core::$classesPath;
                    $path .= implode(DIRECTORY_SEPARATOR, $aConfig) . DIRECTORY_SEPARATOR;
                    $path .= 'config' . DIRECTORY_SEPARATOR . $sFileName . '.php';
                    
                    return $path;
                }
            }
            ?>
        

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

В итоге Core_Database вернет экземпляр класса для работы с СУБД через запрошенный драйвер (в нашем случае это драйвер PDO по умолчанию). Пришла пора посмотреть на код класса Core_Database_Pdo.

3. Работа с СУБД через PDO


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            class Core_Database_Pdo extends Core_DataBase
            {
                public function connect()
                {

                }
                
                public function disconnect()
                {

                }
                
                public function setCharset()
                {

                }
                
                public function escape()
                {

                }
            }
            ?>
        

Пока что здесь просто объявлены методы, которые в родительском классе Core_Database указаны абстрактными. Для начала, обеспечим подключение к СУБД.


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            class Core_Database_Pdo extends Core_DataBase
            {
                /** Результат выполнения запроса
                * @var resource | NULL
                */
                protected $_result = NULL;

                /**
                * Возвращает активное подключение к СУБД
                * @return resource
                */
                public function getConnection()
                {
                    $this->connect();

                    return $this->_connection;
                }

                /**
                * Подключается к СУБД
                * @return boolean TRUE | FALSE
                */
                public function connect()
                {
                    // Если подключение уже выполнено, ничего не делаем
                    if ($this->_connection)
                    {
                        return TRUE;
                    }
                    $this->_config += array(
                        'driverName' => 'mysql',
                        'attr' => array(
                            PDO::ATTR_PERSISTENT => FALSE
                        )
                    );

                    // Подключаемся к СУБД
                    try {
                        // Адрес сервера может быть задан со значением порта
                        $aHost = explode(":", $this->_config['host']);
                        
                        // Формируем строку источника подключения к СУБД
                        $dsn = "{$this->_config['driverName']}:host={$aHost[0]}";

                        // Если был указан порт
                        !empty($aHost[1])
                            && $dsn .= ";port={$aHost[1]}";

                        // Указываем имя БД
                        !is_null($this->_config['dbname'])
                            && $dsn .= ";dbname={$this->_config['dbname']}";
                        
                        // Кодировка
                        $dsn .= ";charset={$this->_config['charset']}";

                        // Подключаемся, и сохраняем подключение в экземпляре класса
                        $this->_connection = new PDO(
                            $dsn,
                            $this->_config['user'],
                            $this->_config['password'],
                            $this->_config['attr']
                        );
                        
                        // В случае ошибок будет брошено исключение
                        $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                        
                    }
                    catch (PDOException $e)
                    {
                        throw new Exception("<p><strong>Ошибка при подключении к СУБД:</strong> {$e->getMessage()}</p>");
                    }
                    
                    // Если ничего плохого не произошло
                    return TRUE;
                }
                
                /**
                * Закрывает соединение с СУБД
                * @return self
                */
                public function disconnect()
                {
                    $this->_connection = NULL;

                    return $this;
                }
                
                /**
                * Устанавливает кодировку соединения клиента и сервера
                * @param string $charset указанное наименование кодировки, которое примет СУБД
                */
                public function setCharset($charset)
                {
                    $this->connect();

                    $this->_connection->exec('SET NAMES ' . $this->quote($charset));

                    return $this;
                }
                
                /**
                * Экранирование строки для использования в SQL-запросах
                * @param string $unescapedString неэкранированная строка
                * @return string Экранированная строка
                */
                public function escape($unescapedString) : string
                {
                    $this->connect();

                    $unescapedString = addcslashes(strval($unescapedString), "\000\032");

                    return $this->_connection->quote($unescapedString);
                }
            }
            ?>
        

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


            
                print "<pre>";
                var_dump(Core_Database::instance()->getConnection()->query("SELECT * FROM `users`")->rowCount());
                print "</pre>'";
            
        

...то вы увидите количество записией в таблице: int(2), к примеру.

Сколько бы раз вы не вызывали Core_Database::instance(), вам всегда в ответ вернется один и тот же экземпляр соединения с СУБД для того драйвера, что вы указали.

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

4. Работа с пользователями сайта

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


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Класс пользователя клиентского раздела сайта
            * Объект класса может создаваться как для существующих пользователей, так и для несуществующих с целью их регистрации
            */
            class User
            {
                /**
                * Информация о пользователе (логин, пароль, и т.п.)
                * @var array
                */
                protected $_data = [];

                /**
                * Разрешенные для чтения поля таблицы пользователей
                * @var array
                */
                protected $_allowedProperties = [
                    'login',
                    'email',
                    'registration_date',
                    'active'
                ];

                /**
                * Запрещенные для чтения поля таблицы пользователей
                * @var array
                */
                protected $_forbiddenProperties = [
                    'password',
                    'deleted'
                ];

                /**
                * Конструктор класса
                * @param int $id = 0
                */
                public function __construct(int $id = 0)
                {

                }

                /**
                * Получает информацию об авторизованном пользователе
                * @return mixed self | NULL если пользователь не авторизован
                */
                public function getCurrent()
                {
                    $return = NULL;

                    /**
                    * Информация о пользователе, если он авторизован, хранится в сессии
                    * Поэтому нужно просто проверить, имеется ли там нужная информация
                    * Если в сессии её нет, значит пользователь не авторизован
                    */
                    (!empty($_SESSION['login']) 
                        && !empty($_SESSION['email']) 
                        && !empty($_SESSION['password']))
                            && $this->_getUserData();


                    // Возвращаем результат вызову
                    return $return;
                }
            }
            ?>
        

Этого кода достаточно для того, чтобы примерно понять, как необходимо взаимодействовать с экземплярами класса User, и как это будет работать. Если сейчас выполнить следующий код на страницах сайта:


            
                print "<pre>";
                var_dump((new User())->getCurrent());
                print "</pre>'";
            
        

...то ответ будет получен такой: NULL.

То есть, еслли мы напишем что-то вроде:


            
                print "<p>Вы " . ((is_null((new User())->getCurrent())) ? " не" : "") . " авторизованы на сайте</p>";
            
        

...то получим на странице текст: Вы не авторизованы на сайте. И уже основываясь на этом можем управлять доступом пользователей к ресурсам сайта. На данном этапе код класса User далеко не оптимален. И предназначение его не совсем такое, каким должно бы быть. Совершенствовать этот класс будем в будущем. А сейчас, пока что, обеспечим авторизацию и регистрацию через объектно-ориентированный стиль.

Итак, пользователь не авторизован. Отправляем его на страницу авторизации, там он заполняет форму, обработку которой мы писали в файле /users.php, и теперь мы эту обработку изменим. Начнем с регистрации пользователя. Для начала, посмотрим, как теперь выглядит страница /users.php.


            <?php
            // Подключаем файл с основными функциями
            require_once('bootstrap.php');

            // Здесь будет храниться результат обработки форм
            $aFormHandlerResult = [];

            // Если была заполнена форма
            // Здесь вместо прежних $_POST['sign-in'] и $_POST['sign-up'] мы используем константы класса USER
            // Это позволяет избежать ошибок в коде разных сценариев и страниц, когда дело касается одних и тех же сущностей
            if (!empty($_POST[User::ACTION_SIGNIN]) || !empty($_POST[User::ACTION_SIGNUP]))
            {
                // Создаем объект класса User
                $oUser = new User();
                
                // Обрабатываем пользовательские данные
                // Теперь здесь мы не определяем авторизуется или регистрируется пользователь.
                // Просто вызываем метод User::processUserData(), который это и определит
                $aFormHandlerResult = $oUser->processUserData($_POST);
            }

            // Создаем объект класса User авторизованного пользователя
            // Если пользователь не авторизован, новый объект будет иметь значение NULL
            $oCurrentUser = (new User())->getCurrent();

            // Если пользователь желает разлогиниться
            if (!empty($_GET['action']) && $_GET['action'] == USER::ACTION_LOGOUT && !is_null($oCurrentUser))
            {
                // Завершаем сеанс пользователя
                $oCurrentUser->unsetCurrent();
            }

            // Если пользователь вводил данные, покажем их ему
            $sLogin = (!empty($aFormHandlerResult['data']['login'])) ? htmlspecialchars_decode($aFormHandlerResult['data']['login']) : "";
            $sEmail = (!empty($aFormHandlerResult['data']['email'])) ? htmlspecialchars_decode($aFormHandlerResult['data']['email']) : "";

            // Остальной код страницы остался практически тем же
            ?>
            <!DOCTYPE html>
            <html lang="ru">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Личный кабинет пользователя. Авторизация/регистрация. PHP+MySQL+JavaScript,jQuery</title>
                <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
                <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
                <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>
                <script>
                    (() => {
                        'use strict'
                        document.addEventListener('DOMContentLoaded', (event) => {
                            // Fetch all the forms we want to apply custom Bootstrap validation styles to
                            const forms = document.querySelectorAll('.needs-validation');

                            if (forms)
                            {
                                // Loop over them and prevent submission
                                Array.from(forms).forEach(form => {
                                    form.addEventListener('submit', event => {
                                    if (!form.checkValidity()) {
                                        event.preventDefault();
                                        event.stopPropagation();
                                    }

                                    form.classList.add('was-validated')
                                    }, false);
                                });
                            }
                        });
                    })();
                </script>
            </head>
            <body>
                <div class="container-md container-fluid">
                    <h1 class="my-3">Личный кабинет пользователя</h1>
                    <p>
                        Сейчас вы <?php echo (!is_null($oCurrentUser)) ? "авторизованы" : "не авторизованы";?> на сайте. <br />
                        <?php
                        if (!is_null($oCurrentUser))
                        {
                            echo "<p>Ваш логин: <strong>" . $oCurrentUser()->login . "</strong>.</p>";
                            echo "<p>Вы можете <a href='/users.php?action=exit'>выйти</a> из системы.</p>";
                        }
                        // Если пользователь вышел из системы
                        elseif (!empty($_GET['action']))
                        {
                            if ($_GET['action'] == USER::ACTION_LOGOUT)
                            {
                                echo "Вы успешно вышли из системы";
                            }
                        }

                        // Перенаправим пользователя на главную страницу при успешной авторизации/регистрации
                        if (!empty($aFormHandlerResult['success']) && $aFormHandlerResult['success'] === TRUE)
                        {
                        ?>
                            <script>
                                setTimeout(() => {
                                    window.location.href="/";
                                }, 3000);
                            </script>
                        <?php
                        }
                        ?>
                    </p>
                    <?php
                    // Блок с формами авторизации/регистрации показываем только неавторизованным пользователям
                    if (!!is_null($oCurrentUser))
                    {
                    ?>
                        <ul class="nav nav-tabs my-3" id="user-action-tabs" role="tablist">
                            <li class="nav-item" role="presentation">
                                <button class="nav-link <?php print (empty($aFormHandlerResult) || $aFormHandlerResult['type'] == User::ACTION_SIGNIN) ? "active" : "";?>" id="user-auth-tab" data-bs-toggle="tab" data-bs-target="#user-auth-tab-pane" type="button" role="tab" aria-controls="user-auth-tab-pane" aria-selected="true">Авторизация</button>
                            </li>
                            <li class="nav-item" role="presentation">
                                <button class="nav-link <?php print (!empty($aFormHandlerResult) && $aFormHandlerResult['type'] == User::ACTION_SIGNUP) ? "active" : "";?>" id="user-reg-tab" data-bs-toggle="tab" data-bs-target="#user-reg-tab-pane" type="button" role="tab" aria-controls="user-reg-tab-pane" aria-selected="false">Регистрация</button>
                            </li>
                        </ul>
                        <div class="tab-content bg-light" id="user-action-tabs-content">
                            <div class="tab-pane fade px-3 <?php print (empty($aFormHandlerResult) || $aFormHandlerResult['type'] == User::ACTION_SIGNIN) ? "show active" : "";?>" id="user-auth-tab-pane" role="tabpanel" aria-labelledby="user-auth-tab-pane" tabindex="0">
                                <div class="row">
                                    <div class="col-xxl-8 col-md-10 rounded text-dark p-3">
                                        <!-- Блок для сообщений о результате обработки формы -->
                                        <?php
                                        // Если была обработана форма
                                        if (!empty($aFormHandlerResult) && $aFormHandlerResult['type'] == User::ACTION_SIGNIN)
                                        {
                                            $sClass = match($aFormHandlerResult['success']) {
                                                        TRUE => "my-3 alert alert-success",
                                                        FALSE => "my-3 alert alert-danger"
                                            };

                                            ?>
                                            <div class="<?=$sClass?>">
                                                <?=$aFormHandlerResult['message'];?>
                                            </div>
                                            <?php
                                        }
                                        ?>
                                        <h3 class="my-3">Авторизация пользователя</h3>
                                        <form id="form-auth" class="needs-validation" name="form-auth" action="/users.php" method="post" autocomplete="off" novalidate>
                                            <div class="my-3">
                                                <label for="auth-login">Логин или электропочта:</label>
                                                <input type="text" id="auth-login" name="login" class="form-control" placeholder="Ваши логин или электропочта" required value="<?php print (empty($aFormHandlerResult) || $aFormHandlerResult['type'] == User::ACTION_SIGNIN) ? $sLogin : "";?>" />
                                                <div class="error invalid-feedback" id="auth-login_error"></div>
                                                <div class="help form-text" id="auth-login_help">Напишите логин или адрес электропочты, указанные вами при регистрации на сайте</div>
                                            </div>
                                            <div class="my-3">
                                                <label for="auth-password">Пароль:</label>
                                                <input type="password" id="auth-password" name="password" class="form-control" placeholder="Напишите ваш пароль" required />
                                                <div class="error invalid-feedback" id="auth-password_error"></div>
                                                <div class="help form-text" id="auth-password_help">Напишите пароль, указанный вами при регистрации на сайте</div>
                                            </div>
                                            <div class="my-3">
                                                <input type="submit" class="btn btn-primary" id="auth-submit" name="<?=USER::ACTION_SIGNIN;?>" value="Войти" />
                                            </div>
                                        </form>
                                    </div>
                                </div>
                            </div>
                            <div class="tab-pane fade px-3 <?php print (!empty($aFormHandlerResult) && $aFormHandlerResult['type'] == User::ACTION_SIGNUP) ? "show active" : "";?>" id="user-reg-tab-pane" role="tabpanel" aria-labelledby="user-reg-tab-pane" tabindex="0">
                                <div class="row">
                                    <div class="col-xxl-8 col-md-10 rounded text-dark p-3">
                                        <!-- Блок для сообщений о результате обработки формы -->
                                        <?php
                                        // Если была обработана форма
                                        if (!empty($aFormHandlerResult) && $aFormHandlerResult['type'] == User::ACTION_SIGNUP)
                                        {
                                            $sClass = match($aFormHandlerResult['success']) {
                                                        TRUE => "my-3 alert alert-success",
                                                        FALSE => "my-3 alert alert-danger"
                                            };
                                        
                                            ?>
                                            <div class="<?=$sClass?>">
                                                <?=$aFormHandlerResult['message'];?>
                                            </div>
                                            <?php
                                        }
                                        ?>
                                        <h3 class="my-3">Регистрация пользователя</h3>
                                        <form id="form-reg" class="needs-validation" name="form-reg" action="/users.php" method="post" autocomplete="off" novalidate>
                                            <div class="row gy-2 mb-3">
                                                <div class="col-md">
                                                    <label for="reg-login">Логин:</label>
                                                    <input type="text" id="reg-login" name="login" class="form-control" placeholder="Ваш логин для регистрации" required value="<?php print (!empty($aFormHandlerResult) && $aFormHandlerResult['type'] == User::ACTION_SIGNUP) ? $sLogin : "";?>" />
                                                    <div class="error invalid-feedback" id="reg-login_error">Логин введен неверно</div>
                                                    <div class="help form-text" id="reg-login_help">Напишите логин для регистрации на сайте</div>
                                                </div>
                                                <div class="col-md">
                                                    <label for="reg-email">Электропочта:</label>
                                                    <input type="email" id="reg-email" name="email" class="form-control" placeholder="Ваш адрес электропочты" required value="<?php print (!empty($aFormHandlerResult) && $aFormHandlerResult['type'] == User::ACTION_SIGNUP) ? $sEmail : "";?>" />
                                                    <div class="error invalid-feedback" id="reg-email_error"></div>
                                                    <div class="help form-text" id="reg-email_help">Напишите ваш действующий адрес электропочты для регистрации на сайте</div>
                                                </div>
                                            </div>
                                            <div class="row gy-2 mb-3">
                                                <div class="col-md">
                                                    <label for="reg-password">Пароль:</label>
                                                    <input type="password" id="reg-password" name="password" class="form-control" placeholder="Напишите ваш пароль" required />
                                                    <div class="error invalid-feedback" id="reg-password_error"></div>
                                                    <div class="help form-text" id="reg-password_help">Напишите пароль, для регистрации на сайте</div>
                                                </div>
                                                <div class="col-md">
                                                    <label for="reg-password2">Подтверждение пароля:</label>
                                                    <input type="password" id="reg-password2" name="password2" class="form-control" placeholder="Повторите ваш пароль" required />
                                                    <div class="error invalid-feedback" id="reg-password2_error"></div>
                                                    <div class="help form-text" id="reg-password2_help">Повторите пароль для его подтверждения и исключения ошибки</div>
                                                </div>
                                            </div>
                                            <div class="my-3 d-flex">
                                                <input type="submit" class="btn btn-success me-3" id="reg-submit" name="<?=USER::ACTION_SIGNUP;?>" value="Зарегистрироваться" />
                                                <input type="reset" class="btn btn-danger" id="reg-reset" name="reset" value="Очистить" />
                                            </div>
                                        </form>
                                    </div>
                                </div>
                            </div>
                        </div>
                    <?php
                    }
                    ?>  
                </div>
            </body>
            </html>
        

Заметно изменилась лишь часть сценария, которая выполнялась до кода HTML-страницы. В комментариях всё описано. Теперь давайте посмотрим, что делает метод User::processUserData(), да и на сам класс User.

5. Класс User


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Класс пользователя клиентского раздела сайта
            * Объект класса может создаваться как для существующих пользователей, так и для несуществующих с целью их регистрации
            */
            class User
            {
                /**
                * Информация о пользователе (логин, пароль, и т.п.)
                * @var array
                */
                protected $_data = [];

                /**
                * Разрешенные для чтения и записи поля таблицы пользователей
                * @var array
                */
                protected $_allowedProperties = [
                    'id',
                    'login',
                    'email',
                    'registration_date',
                    'active'
                ];

                /**
                * Запрещенные для чтения и записи поля таблицы пользователей
                * @var array
                */
                protected $_forbiddenProperties = [
                    'password',
                    'deleted'
                ];

                /**
                * Имена полей таблицы в БД
                * @var array
                */
                protected $_columnNames = [];

                /**
                * Строка для действия регистрации
                * @param const 
                */
                public const ACTION_SIGNUP = 'sign-up';
                
                /**
                * Строка для действия регистрации
                * @param const 
                */
                public const ACTION_SIGNIN = 'sign-in';

                /**
                * Строка для действия выхода из системы
                * @param const 
                */
                public const ACTION_LOGOUT = 'exit';

                /**
                * Получает данные о пользователе сайте
                */
                protected function _getUserData()
                {

                }

                /**
                * Конструктор класса
                * @param int $id = 0
                */
                public function __construct(int $id = 0)
                {
                    // Сразу же из базы данных получаем перечень имен полей таблицы
                    $this->getColumnNames();
                }

                /**
                * Получает перечень имен полей таблицы из БД
                * @return self
                */
                public function getColumnNames()
                {
                    $oCore_Database = Core_Database::instance();
                    
                    $this->_columnNames = $oCore_Database->getColumnNames('users');

                    return $this;
                }

                /**
                * Получает информацию об авторизованном пользователе
                * @return mixed self | NULL если пользователь не авторизован
                */
                public function getCurrent()
                {
                    $return = NULL;

                    /**
                    * Информация о пользователе, если он авторизован, хранится в сессии
                    * Поэтому нужно просто проверить, имеется ли там нужная информация
                    * Если в сессии её нет, значит пользователь не авторизован
                    */
                    (!empty($_SESSION['login']) 
                        && !empty($_SESSION['email']) 
                        && !empty($_SESSION['password']))
                            && $this->_getUserData();

                    // Возвращаем результат вызову
                    return $return;
                }

                /**
                * Обрабатывает данные, которыми пользователь заполнил форму
                * @param array $post
                */
                public function processUserData(array $post)
                {
                    $aReturn = [
                        'success' => FALSE,
                        'message' => "При обработке формы произошла ошибка",
                        'data' => [],
                        'type' => static::ACTION_SIGNIN
                    ];
                    
                    // Если не передан массив на обработку, останавливаем работу сценария
                    if (empty($post))
                    {
                        die("<p>Для обработки пользовательских данных формы должен быть передан массив</p>");
                    }
                    
                    // Если в массиве отсутствуют данные о типе заполненной формы, останавливаем работу сценария
                    if (empty($post[static::ACTION_SIGNIN]) && empty($post[static::ACTION_SIGNUP]))
                    {
                        die("<p>Метод <code>User::processUserData()</code> должен вызываться только для обработки данных из форм авторизации или регистрации</p>");
                    }

                    // Флаг регистрации нового пользователя
                    $bRegistrationUser = !empty($post[static::ACTION_SIGNUP]);

                    // Логин и пароль у нас должны иметься в обоих случаях
                    $sLogin = strval(htmlspecialchars(trim($_POST['login'])));
                    $sPassword = strval(htmlspecialchars(trim($_POST['password'])));

                    // А вот электропочта и повтор пароля будут только в случае регистрации
                    if ($bRegistrationUser)
                    {
                        $aReturn['type'] = static::ACTION_SIGNUP;

                        $sEmail = strval(htmlspecialchars(trim($_POST['email'])));
                        $sPassword2 = strval(htmlspecialchars(trim($_POST['password2'])));

                        // Проверяем данные на ошибки
                        if ($this->validateEmail($sEmail))
                        {
                            // Логин и пароли не могут быть пустыми
                            if (empty($sLogin))
                            {
                                $aReturn['message'] = "Поле логина не было заполнено";
                                $aReturn['data'] = $post;
                            }
                            elseif (empty($sPassword))
                            {
                                $aReturn['message'] = "Поле пароля не было заполнено";
                                $aReturn['data'] = $post;
                            }
                            // Пароли должны быть идентичны
                            elseif ($sPassword !== $sPassword2)
                            {
                                $aReturn['message'] = "Введенные пароли не совпадают";
                                $aReturn['data'] = $post;
                            }
                            // Если логин не уникален
                            elseif ($this->isValueExist($sLogin, 'login'))
                            {
                                $aReturn['message'] = "Указанный вами логин ранее уже был зарегистрирован";
                                $aReturn['data'] = $post;
                            }
                            // Если email не уникален
                            elseif ($this->isValueExist($sEmail, 'email'))
                            {
                                $aReturn['message'] = "Указанный вами email ранее уже был зарегистрирован";
                                $aReturn['data'] = $post;
                            }
                            // Если все проверки прошли успешно, можно регистрировать пользователя
                            else
                            {
                                /**
                                * Согласно документации к PHP, мы для подготовки пароля пользователя к сохранению в БД
                                * будем использовать функцию password_hash() https://www.php.net/manual/ru/function.password-hash
                                * Причем, согласно рекомендации, начиная с версии PHP 8.0.0 не нужно указывать соль для пароля. Значит, и не будем
                                */
                                // Хэшируем пароль
                                $sPassword = password_hash($sPassword, PASSWORD_BCRYPT);
                                
                                $this->login = $sLogin;
                                $this->password = $sPassword;
                                $this->email = $sEmail;
                                $this->save();

                                if (Core_Database::instance()->lastInsertId())
                                {
                                    $aReturn['success'] = TRUE;
                                    $aReturn['message'] = "Пользователь с логином <strong>{$sLogin}</strong> и email <strong>{$sEmail}</strong> успешно зарегистрирован.";
                                    $aReturn['data']['user_id'] = Core_Database::instance()->lastInsertId();
                                }
                            }
                        }
                        else
                        {
                            $aReturn['message'] = "Указанное значение адреса электропочты не соответствует формату";
                            $aReturn['data'] = $post;
                        }
                    }

                    return $aReturn;
                }

                /**
                * Проверяет правильность адреса электронной почты
                * @param string $email
                * @return TRUE | FALSE
                */
                public function validateEmail(string $email) : bool
                {
                    return filter_var($email, FILTER_VALIDATE_EMAIL);
                }

                /**
                * Проверяет уникальность логина в системе
                * @param string $value
                * @param string $field
                * @return TRUE | FALSE
                */
                public function isValueExist($value, $field) : bool
                {
                    // Подключаемся к СУБД
                    $oCore_Database = Core_Database::instance();
                    $oCore_Database->clearSelect()
                        ->clearWhere()
                        ->select()
                        ->from('users')
                        ->where($field, '=', $value)
                        ->where('deleted', '=', 0);

                    // Выполняем запрос
                    try {
                        $stmt = $oCore_Database->execute();
                    }
                    catch (PDOException $e)
                    {
                        die("<p><strong>При выполнении запроса произошла ошибка:</strong> {$e->getMessage()}</p>");
                    }
                    
                    // Если логин уникален, в результате запроса не должно быть строк
                    return $oCore_Database->getRowCount() !== 0;
                }

                /**
                * Магический метод для установки значений необъявленных свойств класса
                * @param string $property
                * @param mixed $value
                */
                public function __set(string $property, $value)
                {
                    $this->_data[$property] = $value;
                }

                /**
                * Сохраняет информацию о пользователе
                * @return object self
                */
                public function save()
                {
                    $oCore_Database = Core_Database::instance();
                    $oCore_Database->insert('users')
                        ->fields(['login', 'password', 'email'])
                        ->values([
                            $this->_data['login'], 
                            $this->_data['password'], 
                            $this->_data['email']
                        ]);
                    
                    $stmt = $oCore_Database->execute();

                    return $this;
                }
            }
            ?>
        

Подробнее о реализации класса User.

  1. В массиве $_data будет (может) храниться информация о пользователе: логин, пароль, email и любая иная, которая имеется.
  2. $_allowedProperties перечисляет перечень полей таблицы `users`, доступ к которым будет разрешен из кода php-сценария.
  3. $_forbiddenProperties, соответственно, перечисляет перечень полей таблицы `users`, доступ к которым будет, наоборот, запрещен из кода php-сценария.
  4. Далее идут константы, которые нам повстречались в коде страницы /users.php.
  5. Метод _getUserData() пока пустой, его мы заполним при разговоре об авторизаии пользователя.
  6. Конструктор класса сейчас лишь получает данные об именах полей таблицы `users`.
  7. Метод getCurrent() получает информацию об авторизованном пользователе, либо возвращает NULL, если такового нет.
  8. Метод processUserData() обрабатывает данные форм авторизации и регистрации.
  9. Метод validateEmail() проверяет, является ли переданное ему значение корректным адресом электропочты, а метод isValueExist() ищет в БД заданное значение, в целях определения его уникальности. Ранее у нас было две функции: isLoginExist() и isEmailExist(). Теперь у нас один общий метод.
  10. Магический метод __set() для установки значений необъявленных свойств.
  11. Метод save() сохраняет информацию о пользователе в БД. При регистрации нового, к примеру.
  12. Если выражение Core_Database::instance()->lastInsertId() вернет значение идентификатора для новой записи в таблице `users`, пользователь зарегистрирован успешно.

Отдельно рассмотрим метод User::processUserData().


            <?php
            public function processUserData(array $post)
            {
                $aReturn = [
                    'success' => FALSE,
                    'message' => "При обработке формы произошла ошибка",
                    'data' => [],
                    'type' => static::ACTION_SIGNIN
                ];
                
                // Если не передан массив на обработку, останавливаем работу сценария
                if (empty($post))
                {
                    die("<p>Для обработки пользовательских данных формы должен быть передан массив</p>");
                }
                
                // Если в массиве отсутствуют данные о типе заполненной формы, останавливаем работу сценария
                if (empty($post[static::ACTION_SIGNIN]) && empty($post[static::ACTION_SIGNUP]))
                {
                    die("<p>Метод <code>User::processUserData()</code> должен вызываться только для обработки данных из форм авторизации или регистрации</p>");
                }

                // Флаг регистрации нового пользователя
                $bRegistrationUser = !empty($post[static::ACTION_SIGNUP]);

                // Логин и пароль у нас должны иметься в обоих случаях
                $sLogin = strval(htmlspecialchars(trim($_POST['login'])));
                $sPassword = strval(htmlspecialchars(trim($_POST['password'])));

                // А вот электропочта и повтор пароля будут только в случае регистрации
                if ($bRegistrationUser)
                {
                    $aReturn['type'] = static::ACTION_SIGNUP;

                    $sEmail = strval(htmlspecialchars(trim($_POST['email'])));
                    $sPassword2 = strval(htmlspecialchars(trim($_POST['password2'])));

                    // Проверяем данные на ошибки
                    if ($this->validateEmail($sEmail))
                    {
                        // Логин и пароли не могут быть пустыми
                        if (empty($sLogin))
                        {
                            $aReturn['message'] = "Поле логина не было заполнено";
                            $aReturn['data'] = $post;
                        }
                        elseif (empty($sPassword))
                        {
                            $aReturn['message'] = "Поле пароля не было заполнено";
                            $aReturn['data'] = $post;
                        }
                        // Пароли должны быть идентичны
                        elseif ($sPassword !== $sPassword2)
                        {
                            $aReturn['message'] = "Введенные пароли не совпадают";
                            $aReturn['data'] = $post;
                        }
                        // Если логин не уникален
                        elseif ($this->isValueExist($sLogin, 'login'))
                        {
                            $aReturn['message'] = "Указанный вами логин ранее уже был зарегистрирован";
                            $aReturn['data'] = $post;
                        }
                        // Если email не уникален
                        elseif ($this->isValueExist($sEmail, 'email'))
                        {
                            $aReturn['message'] = "Указанный вами email ранее уже был зарегистрирован";
                            $aReturn['data'] = $post;
                        }
                        // Если все проверки прошли успешно, можно регистрировать пользователя
                        else
                        {
                            /**
                            * Согласно документации к PHP, мы для подготовки пароля пользователя к сохранению в БД
                            * будем использовать функцию password_hash() https://www.php.net/manual/ru/function.password-hash
                            * Причем, согласно рекомендации, начиная с версии PHP 8.0.0 не нужно указывать соль для пароля. Значит, и не будем
                            */
                            // Хэшируем пароль
                            $sPassword = password_hash($sPassword, PASSWORD_BCRYPT);
                            
                            $this->login = $sLogin;
                            $this->password = $sPassword;
                            $this->email = $sEmail;
                            $this->save();

                            if (Core_Database::instance()->lastInsertId())
                            {
                                $aReturn['success'] = TRUE;
                                $aReturn['message'] = "Пользователь с логином <strong>{$sLogin}</strong> и email <strong>{$sEmail}</strong> успешно зарегистрирован.";
                                $aReturn['data']['user_id'] = Core_Database::instance()->lastInsertId();
                            }
                        }
                    }
                    else
                    {
                        $aReturn['message'] = "Указанное значение адреса электропочты не соответствует формату";
                        $aReturn['data'] = $post;
                    }
                }

                return $aReturn;
            }

            /** ............ */
            public function save()
            {
                $oCore_Database = Core_Database::instance();
                $oCore_Database->insert('users')
                    ->fields(['login', 'password', 'email'])
                    ->values([
                        $this->_data['login'], 
                        $this->_data['password'], 
                        $this->_data['email']
                    ]);
                
                $stmt = $oCore_Database->execute();

                return $this;
            }
            ?>
        
  1. $bRegistrationUser = !empty($post[static::ACTION_SIGNUP]) — мы определяем, авторизуется пользователь или регистрируется. Сейчас рассматриваем только процедуру регистрации.
  2. Далее проходим все проверки, и если они пройдены, если логин и email уникальны, переходим к сохранению информации о пользователе в методе User::save().
  3. В этом методе мы через выражение $oCore_Database = Core_Database::instance() получаем доступ к объекту подключения к СУБД. Это не сам PDO, а наш класс Core_Database_Pdo, который реализует взаимодействие с MySQL посредством PDO. Ему мы и сообщаем через последовательный вызов методов insert(), fields() и values() перечень полей и значений, которые мы хотим в полях сохранить.
  4. Выражение $oCore_Database->execute() выполняется отдельно, так как оно возвращет уже иной объект — не Core_Database_Pdo, а PDOStatement. И всё. Пользователь должен быть сохранен в БД таблице `users`.

Всем этим внутри метода User::save() мы заменили часть кода, которая раньше у нас была внутри тела функции userRegistration().

            
                function userRegistration(array $data) : array
                {
                    // ................

                    $dbh = dbConnect();

                    // Создаем тело SQL-запроса
                    $stmt = $dbh->prepare("INSERT INTO `users` (`login`, `email`, `password`) VALUES (:login, :email, :password)");

                    /**
                    * Согласно документации к PHP, мы для подготовки пароля пользователя к сохранению в БД
                    * будем использовать функцию password_hash() https://www.php.net/manual/ru/function.password-hash
                    * Причем, согласно рекомендации, начиная с версии PHP 8.0.0 не нужно указывать соль для пароля. Значит, и не будем
                    */
                    // Хэшируем пароль
                    $sPassword = password_hash($sPassword, PASSWORD_BCRYPT);

                    // Подготавливаем запрос
                    $stmt->bindParam(':login', $sLogin);
                    $stmt->bindParam(':email', $sEmail);
                    $stmt->bindParam(':password', $sPassword);

                    // Выполняем запрос
                    $stmt->execute();

                    // ....................
                }
            
        

Да, сейчас не очевидно, что же такого мы получили в виде выгоды, написав такую реализацию. Но это именно пока. В тот момент, пока мы говорим лишь о регистрации и авторизации. К тому же, сам класс PDO в PHP значительно упрощает взаимодействие с СУБД.

Очевидно, что изменения произошли и в классах Core_Database и Core_Database_Pdo. Посмотрим на них сейчас.

6. Классы Core_Database и Core_Database_Pdo


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Абстраактый класс
            * Обеспечивает подключение к СУБД
            * Класс является абстрактным, так как оставляет пользователю право определять, через какой
            * модуль будет реализовано взаимодействие с СУБД
            * 
            * Реализация такого взаимодействия должна быть написана в дочерних классах
            * Например, вызов Core_Database::instance('mysql') вернет экземпляр класса Core_Database_Mysql
            */
            abstract class Core_DataBase
            {
                // Экземпляр класса
                static protected $_instance = [];

                // Параметры подключение к СУБД
                protected $_config = [];

                // Здесь будет храниться последний выполненный запрос к БД
                protected $_lastQuery = NULL;

                /**
                * Подключение к СУБД
                * @var resource
                */
                protected $_connection = NULL;

                /**
                * Число строк в результате запросе
                * @var int
                */
                protected $_lastQueryRows = NULL;

                /**
                * Перечень полей для запроса SELECT
                * @var array
                */
                protected $_select = [];

                /**
                * Имя таблицы для запроса SELECT
                * @var string
                */
                protected $_from = NULL;
                
                /**
                * Перечень полей для запроса INSERT
                * @var array
                */
                protected $_fields = [];
                
                /**
                * Перечень значений для запроса INSERT
                * @var array
                */
                protected $_values = [];

                /**
                * Имя таблицы для запроса INSERT
                * @var string
                */
                protected $_tableName = NULL;

                /**
                * Перечень условий для оператора WHERE
                * @var array
                */
                protected $_where = [];
                
                /**
                * Тип SQL-запроса:
                * 0 - SELECT
                * 1 - INSERT
                * 2 - UPDATE
                * 3 - DELETE
                */
                protected $_queryType = NULL;
                
                /** Абстрактные методы не имеют реализации. Они должны быть реализованы в дочерних классах */

                // Подключение к СУБД
                abstract public function connect();

                // Отключение от СУБД
                abstract public function disconnect();

                // Установка кодировки соединения
                abstract public function setCharset($charset);

                // Экранирование данных
                abstract public function escape($unescapedString);

                // Представление результата в виде объекта
                abstract public function asObject();

                // Представление результата в виде ассоциативного массива
                abstract public function asAssoc();

                // Установка SQL-запроса
                abstract public function query($query);

                // Выполнение запроса
                abstract public function result();

                // Имена методов говорят сами за себя
                abstract function select();
                abstract function insert(string $tableName);
                abstract function from(string $from);
                abstract function fields();
                abstract function values();
                abstract function where(string $field, string $condition, $value);
                abstract function execute();
                abstract function lastInsertId();

                /**
                * Защищенный конструктор класса, который невозможно вызвать откуда-либо, кроме как из самого класса
                * Получает параметры подключения к СУБД
                */
                protected function __construct(array $config)
                {
                    $this->setConfig($config);
                }

                /**
                * Возвращает и при необходимости создает экзепляр класса
                * @return object Core_Database
                */
                static public function instance(string $name = 'pdo')
                {
                    // Если экземпляр класса не был создан
                    if (empty(static::$_instance[$name]))
                    {
                        // Получаем параметры подключения к СУБД
                        $aConfig = Core::$config->get('core_database', array());
                        
                        if (!isset($aConfig[$name]))
                        {
                            throw new Exception('Для запрошенного типа подключения к СУБД нет конфигурации');
                        }

                        // Определяем, какой именно класс будем использовать
                        // Он будет именоваться Core_Database_{$name}, например Core_Database_Pdo или Core_Database_Mysql
                        $driver = __CLASS__ . "_" . ucfirst($name);
                        
                        self::$_instance[$name] = new $driver($aConfig[$name]);
                    }

                    // Возвращем вызову экземпляр класса для работы с СУБД
                    return static::$_instance[$name];
                }

                public function setConfig(array $config)
                {
                    $this->_config = $config + [
                        'host' => 'localhost',
                        'user' => '',
                        'password' => '',
                        'dbname' => NULL,
                        'charset' => 'utf8'
                    ];

                    return $this;
                }

                /**
                * Получает перечень полей таблицы из БД
                * @param string $tableName имя таблицы
                * @param string $likeCondition значение для применения оператора LIKE
                * @return array
                */
                public function getColumnNames(string $tableName, string $likeCondition = NULL) : array
                {
                    // Подключаемся к СУБД
                    $this->connect();

                    // Составляем строку запроса
                    $sQuery = "SHOW COLUMNS FROM " . $this->quoteColumnNames($tableName);

                    // Если есть значения для условия оператора LIKE
                    if (!is_null($likeCondition))
                    {
                        $sQuery .= ' LIKE ' . $this->quote($likeCondition);
                    }

                    // Выполняем запрос, результат получаем в виде ассоциативного массива
                    $result = $this->query($sQuery)->asAssoc()->result();

                    $return = [];
                    
                    // Собираем информацию о столбцах
                    foreach ($result as $row)
                    {
                        $column['name'] = $row['Field'];
                        $column['columntype'] = $row['Type'];
                        $column['null'] = ($row['Null'] == 'YES');
                        $column['key'] = $row['Key'];
                        $column['default'] = $row['Default'];
                        $column['extra'] = $row['Extra'];
                        
                        $return[$column['name']] = $column;
                    }
                    
                    // Возвращаем вызову результат
                    return $return;
                }

                /**
                * Экранирует имена полей или таблиц для применения в строке SQL-запроса
                * @param string $value
                * @return string
                */
                public function quoteColumnNames(string $value) : string
                {
                    return preg_replace('/(?<=^|\.)(\w+)(?=$|\.)/ui', '`$1`', $value);
                }

                /**
                * Возвращает строку последнего выполненного запроса
                * @return string | NULL
                */
                public function getLastQuery()
                {
                    return $this->_lastQuery;
                }

                /**
                * Возвращает число строк из последнего результата запроса
                */
                public function getRowCount()
                {
                    return $this->_lastQueryRows;
                }

                /**
                * Устанавливает тип запроса SELECT, INSERT и т.п.
                * @param integer $queryType
                * @return object self
                */
                public function setQueryType(int $queryType)
                {
                    $this->_queryType = $queryType;

                    return $this;
                }

                /**
                * Возвращает тип запроса
                * @return integer
                */
                public function getQueryType()
                {
                    return $this->_queryType;
                }
            }
            ?>
        

Файл снабжен обширными комментариями, которые должны донести суть описанного. Перейдем к классу Core_Database_Pdo.


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            class Core_Database_Pdo extends Core_DataBase
            {
                /** Результат выполнения запроса
                * @var resource | NULL
                */
                protected $_result = NULL;

                /**
                * Представление результата запроса в виде ассоциативного массива либо объекта
                */
                protected $_fetchType = PDO::FETCH_OBJ;

                /**
                * Возвращает активное подключение к СУБД
                * @return resource
                */
                public function getConnection()
                {
                    $this->connect();

                    return $this->_connection;
                }

                /**
                * Подключается к СУБД
                * @return boolean TRUE | FALSE
                */
                public function connect()
                {
                    // Если подключение уже выполнено, ничего не делаем
                    if ($this->_connection)
                    {
                        return TRUE;
                    }
                    $this->_config += array(
                        'driverName' => 'mysql',
                        'attr' => array(
                            PDO::ATTR_PERSISTENT => FALSE
                        )
                    );

                    // Подключаемся к СУБД
                    try {
                        // Адрес сервера может быть задан со значением порта
                        $aHost = explode(":", $this->_config['host']);
                        
                        // Формируем строку источника подключения к СУБД
                        $dsn = "{$this->_config['driverName']}:host={$aHost[0]}";

                        // Если был указан порт
                        !empty($aHost[1])
                            && $dsn .= ";port={$aHost[1]}";

                        // Указываем имя БД
                        !is_null($this->_config['dbname'])
                            && $dsn .= ";dbname={$this->_config['dbname']}";
                        
                        // Кодировка
                        $dsn .= ";charset={$this->_config['charset']}";

                        // Подключаемся, и сохраняем подключение в экземпляре класса
                        $this->_connection = new PDO(
                            $dsn,
                            $this->_config['user'],
                            $this->_config['password'],
                            $this->_config['attr']
                        );
                        
                        // В случае ошибок будет брошено исключение
                        $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                        
                    }
                    catch (PDOException $e)
                    {
                        throw new Exception("<p><strong>Ошибка при подключении к СУБД:</strong> {$e->getMessage()}</p>");
                    }
                    
                    // Если ничего плохого не произошло
                    return TRUE;
                }
                
                /**
                * Закрывает соединение с СУБД
                * @return self
                */
                public function disconnect()
                {
                    $this->_connection = NULL;

                    return $this;
                }
                
                /**
                * Устанавливает кодировку соединения клиента и сервера
                * @param string $charset указанное наименование кодировки, которое примет СУБД
                */
                public function setCharset($charset)
                {
                    $this->connect();

                    $this->_connection->exec('SET NAMES ' . $this->quote($charset));

                    return $this;
                }
                
                /**
                * Экранирование строки для использования в SQL-запросах
                * @param string $unescapedString неэкранированная строка
                * @return string Экранированная строка
                */
                public function escape($unescapedString) : string
                {
                    $this->connect();

                    $unescapedString = addcslashes(strval($unescapedString), "\000\032");

                    return $this->_connection->quote($unescapedString);
                }

                /**
                * Возвращает результат работы метода PDO::quote()
                * @return string
                */
                public function quote(string $value) : string
                {
                    return $this->_connection->quote($value);
                }

                /**
                * Возвращает идентификатор последней вставленной записи в БД, если такой имеется
                * @return integer|string|NULL
                */
                public function lastInsertId()
                {
                    return $this->getConnection()->lastInsertId();
                }

                /**
                * Устанавливает строку запроса, который будет выполнен позднее
                * @param string $query
                * @return object self
                */
                public function query($query)
                {
                    // Переданную строку запроса сохраняем, чтобы её потом можно было просмотреть
                    $this->_lastQuery = $query;

                    // По умолчанию устанавливаем, что результат запроса хотим получать в виде объекта
                    $this->_fetchType = PDO::FETCH_OBJ;

                    return $this;
                }

                /**
                * Устанавливает тип представления данных в результате запроса в виде объекта
                * @return object self
                */
                public function asObject()
                {
                    $this->_fetchType = PDO::FETCH_OBJ;

                    return $this;
                }

                /**
                * Устанавливает тип представления данных в результате запроса в виде ассоциативного массива
                * @return object self
                */
                public function asAssoc()
                {
                    $this->_fetchType = PDO::FETCH_ASSOC;

                    return $this;
                }

                /**
                * Выполняет запрос SELECT, возвращает результат выполнения
                * @return object PDOStatement
                */
                public function result() : PDOStatement
                {
                    // Результат выполнения запроса сохраняем внутри объекта
                    $this->_result = $this->_connection->query($this->_lastQuery, $this->_fetchType);

                    // Определяем количество строк в результате запроса, сохраняем внутри объекта
                    $this->_lastQueryRows = $this->_result->rowCount();

                    return $this->_result;
                }

                /**
                * Устанавливает перечень полей для запроса SELECT
                * @param string|array $data = "*"
                * @return object self
                */
                public function select($data = "*")
                {
                    // Устанавливаем в объекте тип выполняемого запроса как SELECT
                    $this->getQueryType() != 0 && $this->setQueryType(0);

                    // Если методу не был передан перечень полей, очищаем все возможно установленные ранее поля
                    if ($data == "*")
                    {
                        $this->clearSelect();
                    }

                    // Сохраняем поля
                    try {
                        // Если перечень полей был передан в виде строки
                        if (is_string($data))
                        {
                            // Добавляем их к массиву в объекте
                            $this->_select[] = $data;
                        }
                        // Если был передан массив, его нужно интерпретировать как указание имени поля и его псевдонима в запросе
                        elseif (is_array($data))
                        {
                            // Если в переданном массиве не два элемента, это ошибка
                            if (count($data) != 2)
                            {
                                throw new Exception("<p>При передаче массива в качестве аргумента методу " . __METHOD__ . "() число элементов этого массива должно быть равным двум</p>");
                            }
                            // Если элементы переданного массива не являются строками, это ошибка
                            elseif (!is_string($data[0]) || !is_string($data[1]))
                            {
                                throw new Exception("<p>При передаче массива в качестве аргумента методу " . __METHOD__ . "() его элементы должны быть строками</p>");
                            }
                            // Если ошибок нет, сохраняем поля в массиве внутри объекта
                            else
                            {
                                // Имена полей экранируем
                                $this->_select[] = $this->quoteColumnNames($data[0]) . " AS " . $this->quoteColumnNames($data[1]);
                            }
                        }
                    }
                    catch (Exception $e)
                    {
                        print $e->getMessage();

                        die();
                    }
                    
                    return $this;
                }

                /**
                * Очищает перечень полей для оператора SELECT
                * @return object self
                */
                public function clearSelect()
                {
                    $this->_select = [];

                    return $this;
                }

                /**
                * Устанавливает имя таблицы для оператора SELECT
                * @param string $from
                * @return object self
                */
                public function from(string $from)
                {
                    try {

                        if (!is_string($from))
                        {
                            throw new Exception("<p>Методу " . __METHOD__ . "() нужно передать имя таблицы для запроса</p>");
                        }
                        
                        // Экранируем данные
                        $this->_from = $this->quoteColumnNames($from);
                    }
                    catch (Exception $e)
                    {
                        print $e->getMessage();

                        die();
                    }

                    return $this;
                }

                /**
                * Сохраняет перечень условий для оператора WHERE в SQL-запросе
                * @param string $field
                * @param string $condition
                * @param string $value
                * @return object self
                */
                public function where(string $field, string $condition, $value)
                {
                    try {
                        if (empty($field) || empty($condition))
                        {
                            throw new Exception("<p>Методу " . __METHOD__ . "() обязательно нужно передать значения имени поля и оператора сравнения</p>");
                        }

                        // Экранируем имена полей и значения, которые будут переданы оператору WHERE
                        $this->_where[] = $this->quoteColumnNames($field) . " " . $condition . " " . $this->_connection->quote($value);
                    }
                    catch (Exception $e)
                    {
                        print $e->getMessage();

                        die();
                    }

                    return $this;
                }

                /**
                * Очищает массив условий отбора для оператора WHERE
                * @return object self
                */
                public function clearWhere()
                {
                    $this->_where = [];

                    return $this;
                }

                /**
                * Устанавливает имя таблицы для оператора INSERT
                * @param string $tableName
                * @return object self
                */
                public function insert(string $tableName)
                {
                    // Экранируем имя таблицы
                    $this->_tableName = $this->quoteColumnNames($tableName);

                    // Устанавливаем тип запроса INSERT
                    $this->_queryType = 1;

                    return $this;
                }

                /**
                * Устанавливает перечень полей для оператора INSERT
                * @return object self
                */
                public function fields()
                {
                    try {
                        // Если не было передано перечня полей
                        if (empty(func_get_args()))
                        {
                            throw new Exception("Метод " . __METHOD__ . "() нельзя вызывать без параметров. Нужно передать перечень полей либо в виде строки, либо в виде массива");
                        }

                        // Сохраняем перечень полей в переменную
                        $mFields = func_get_arg(0);

                        // Если передан массив
                        if (is_array($mFields))
                        {
                            // Просто сохраняем его
                            $this->_fields = $mFields;
                        }
                        // Если передана строка
                        elseif (is_string($mFields))
                        {
                            // Разбираем её, полученный массив сохраняем
                            $this->_fields = explode(',', $mFields);
                        }
                        // В ином случае будет ошибка
                        else
                        {
                            throw new Exception("Метод " . __METHOD__ . "() ожидает перечень полей либо в виде строки, либо в виде массива");
                        }
                    }
                    catch (Exception $e)
                    {
                        print $e->getMessage();

                        die();
                    }

                    return $this;
                }

                /**
                * Устанавливает перечень значений, которые будут переданы оператору INSERT
                * @return object self
                */
                public function values()
                {
                    try {
                        // Если значения не переданы, это ошибка
                        if (empty(func_get_args()))
                        {
                            throw new Exception("Метод " . __METHOD__ . "() нельзя вызывать без параметров. Нужно передать перечень значений либо в виде строки, либо в виде массива");
                        }

                        // Сохраняем переденные значения в переменную
                        $mValues = func_get_arg(0);

                        // Если был передан массив
                        if (is_array($mValues))
                        {
                            // Просто сохраняем его
                            $this->_values[] = $mValues;
                        }
                        // Если была передана строка
                        elseif (is_string($mValues))
                        {
                            // Разбираем её, полученный массив сохраняем в объекте
                            $this->_values[] = explode(',', $mValues);
                        }
                        // В ином случае будет ошибка
                        else
                        {
                            throw new Exception("Метод " . __METHOD__ . "() ожидает перечень значений либо в виде строки, либо в виде массива");
                        }
                    }
                    catch (Exception $e)
                    {
                        print $e->getMessage();

                        die();
                    }

                    return $this;
                }

                /**
                * Выполняет SQL-запрос к СУБД
                */
                public function execute() : PDOStatement | NULL
                {
                    // Результат запроса будет представлен в виде объекта
                    $this->_fetchType = PDO::FETCH_OBJ;

                    // Пустая строка для SQL-запроса
                    $sQuery = "";

                    // Строка оператора WHERE
                    $sWhere = " WHERE ";

                    // Сначала собираем строку для оператора WHERE
                    foreach ($this->_where as $index => $sWhereRow)
                    {
                        // Для каждого из сохраненного массива для оператора WHERE формируем строку
                        $sWhere .= (($index) ? " AND" : "") . " " . $sWhereRow;
                    }

                    // Создаем данные, которые вернем в ответ на вызов
                    $return = NULL;

                    // Пробуем выполнить запрос
                    try {

                        // В зависимости от типа запроса
                        switch ($this->getQueryType())
                        {
                            // SELECT
                            case 0:
                                $sQuery .= "SELECT " . implode(", ", $this->_select) . " FROM {$this->_from}" . $sWhere;

                                $return = $this->query($sQuery)->result();
                            break;
                            
                            // INSERT
                            case 1:
                                /**
                                * Здесь мы воспользуемся механизмом подготовки запроса от PDO
                                * https://www.php.net/manual/ru/pdo.prepared-statements.php
                                */
                                $sPseudoValues = "(";
                                $sFields = "(";

                                foreach ($this->_fields as $index => $sField)
                                {
                                    $sPseudoValues .= (($index) ? "," : "") . "?";
                                    $sFields .= (($index) ? "," : "") . $this->quoteColumnNames($sField);
                                }

                                $sPseudoValues .= ")";
                                $sFields .= ")";

                                $sQuery .= "INSERT INTO " . $this->_tableName . " " . $sFields . " VALUES " . $sPseudoValues;

                                $stmt = $this->getConnection()->prepare($sQuery);
                                
                                foreach ($this->_values as $aValues)
                                {
                                    for ($i = 1; $i <= count($aValues); ++$i)
                                    {
                                        $stmt->bindParam($i, $aValues[$i - 1]);
                                    }

                                    $stmt->execute();
                                }

                                $return = $stmt;

                            break;
                        }
                        
                        // Сохраняем строку запроса в объекте
                        $this->_lastQuery = $sQuery;

                    }
                    catch (PDOException $e)
                    {
                        throw new Exception($e->getMessage());
                    }

                    return $return;
                }
            }
            ?>
        

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

7. Авторизация пользователя через класс User

Класс User был немного дополнен. Отметим здесь лишь внесенные изменения


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Класс пользователя клиентского раздела сайта
            * Объект класса может создаваться как для существующих пользователей, так и для несуществующих с целью их регистрации
            */
            class User
            {
                // ..................
                
                /**
                * Результируйщий объект запроса к БД
                * @var stdClass|NULL
                */
                protected $_user = NULL;

                // ..................
                
                /**
                * Получает данные о пользователе сайте
                * @return mixed self|NULL
                */
                protected function _getUserData()
                {
                    $return = NULL;
                    
                    // Если внутри объекта нет информации о пользователе, пробуем получить её из сессии
                    if (is_null($this->_user))
                    {
                        if (!empty($_SESSION['password']))
                        {
                            $sPassword = strval($_SESSION['password']);
                            $sValue = strval((!empty($_SESSION['email'])) ? $_SESSION['email'] : $_SESSION['login'] );

                            $stmt = $this->getByLoginOrEmail($sValue);
                            
                            if (!is_null($stmt))
                            {
                                $this->_user = $stmt->fetch();

                                $return = $this;
                            }
                        }
                    }
                    else
                    {
                        $return = $this;
                    }

                    return $return;
                }

                // ..................

                /**
                * Получает информацию об авторизованном пользователе
                * @return mixed self | NULL если пользователь не авторизован
                */
                public function getCurrent()
                {
                    $return = NULL;

                    /**
                    * Информация о пользователе, если он авторизован, хранится в сессии
                    * Поэтому нужно просто проверить, имеется ли там нужная информация
                    * Если в сессии её нет, значит пользователь не авторизован
                    */
                    (!empty($_SESSION['login']) 
                        && !empty($_SESSION['email']) 
                        && !empty($_SESSION['password']))
                            && $return = $this->_getUserData();
                    
                    // Возвращаем результат вызову
                    return $return;
                }

                /**
                * Устанавливает в сесии параметры пользователя, прошедшего авторизацию
                * @return object self
                */
                public function setCurrent()
                {
                    $_SESSION['login'] = $this->_user->login;
                    $_SESSION['email'] = $this->_user->email;
                    $_SESSION['password'] = $this->_user->password;

                    return $this;
                }

                // ..................

                /**
                * Обрабатывает данные, которыми пользователь заполнил форму
                * @param array $post
                */
                public function processUserData(array $post)
                {
                    // ..................

                    // А вот электропочта и повтор пароля будут только в случае регистрации
                    if ($bRegistrationUser)
                    {
                        // ..................
                    }
                    // Если пользователь авторизуется
                    else
                    {
                        // Проверяем, не был ли пользователь ранее авторизован
                        if ($this->_checkCurrent($sLogin))
                        {
                            $aReturn['success'] = TRUE;
                            $aReturn['message'] = "Вы ранее уже авторизовались на сайте";
                        }
                        // Если авторизации не было
                        else 
                        {
                            // Если не передан пароль
                            if (empty($sPassword))
                            {
                                $aReturn['message'] = "Поле пароля не было заполнено";
                                $aReturn['data'] = $post;
                            }
                            else 
                            {
                                // Ищем соответствие переданной информации в БД
                                $stmt = $this->getByLoginOrEmail($sLogin);
                                
                                // Если были найдены записи
                                if (!is_null($stmt))
                                {
                                    // Получаем объект с данными о пользователе
                                    $oUser = $this->_user = Core_Database::instance()->result()->fetch();
                                    
                                    // Проверяем пароль пользователя
                                    // Если хэш пароля совпадает
                                    if ($this->checkPassword($sPassword, $oUser->password))
                                    {
                                        // Авторизуем пользователя
                                        $this->setCurrent();

                                        $aReturn['success'] = TRUE;
                                        $aReturn['message'] = "Вы успешно авторизовались на сайте";
                                        $aReturn['data'] = $post;
                                        $aReturn['data']['user_id'] = $oUser->id;
                                    }
                                    else
                                    {
                                        $aReturn['message'] = "Для учетной записи <strong>{$sLogin}</strong> указан неверный пароль";
                                        $aReturn['data'] = $post;
                                    }
                                }
                            }
                        }
                    }

                    return $aReturn;
                }
                
                /**
                * Ищет в БД запись по переданному значению полей login или email
                * @param string $value
                * @return object PDOStatement|NULL
                */
                public function getByLoginOrEmail(string $value) : PDOStatement | NULL
                {
                    // Определяем тип авторизации: по логину или адресу электропочты
                    $sType = NULL;
                    $sType = match($this->validateEmail($value)) {
                                TRUE => 'email',
                                FALSE => 'login'
                    };

                    // Выполняем запрос SELECT
                    $oCore_Database = Core_Database::instance();
                    $oCore_Database->select()
                        ->from('users')
                        ->where($sType, '=', $value)
                        ->where('deleted', '=', 0)
                        ->where('active', '=', 1);
                    
                    $stmt = $oCore_Database->execute();

                    // Если такой пользователь есть в БД, вернем объект с результатом запроса
                    return ($oCore_Database->getRowCount() > 0) ? $stmt : NULL;
                }

                /**
                * Проверяет пароль пользователя, совпадает ли он с хранимым в БД
                * @param string $password пароль пользователя
                * @param string $hash хэш пароля пользователя из БД
                * @return boolean TRUE|FALSE
                */
                public function checkPassword(string $password, string $hash) : bool
                {
                    /**
                    * Согласно документации к PHP, мы для подготовки пароля пользователя к сохранению в БД
                    * мы использовали функцию password_hash() https://www.php.net/manual/ru/function.password-hash
                    * Теперь для проверки пароля для авторизации нам нужно использовать функцию password_verify()
                    * https://www.php.net/manual/ru/function.password-verify.php
                    */
                    return password_verify($password, $hash);
                }

                // ..................
            }
            ?>
        

Сессию теперь стартуем при инициализации ядра сайта. Для этого в конец метода Core::init() небольшой кусок кода


            <?php
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            class Core 
            {
                // ....................

                static public function init()
                {
                    // ...............

                    // Запускаем сессию
                    if (session_status() !== PHP_SESSION_ACTIVE)
                    {
                        session_start();
                    }
                }

                // ....................
            }
            ?>
        

Теперь пользователь может и авторизоваться. У нас после авторизаии пользователь перенаправляется на стартовую страницу сайта /index.php. Давайте внесем в неё небольшие изменения относительно предыдущей версии.


            <?php
            // Подключаем файл с основными функциями
            require_once('bootstrap.php');

            // Создаем объект авторизованного пользователя
            $oCurrentUser = (new User())->getCurrent();
            ?>
            <!DOCTYPE html>
            <html lang="ru">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Авторизация/регистрация. PHP+MySQL+JavaScript,jQuery</title>
                <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
                <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
                <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>
            </head>
            <body>
                <div class="container-md container-fluid">
                    <h1 class="my-3">Добро пожаловать на сайт</h1>
                    <p class="h6">
                        Вы зашли на сайт примера авторизации и регистрации пользователя. 
                    </p>
                    <p>
                        Сейчас вы <?php echo (!is_null($oCurrentUser)) ? "авторизованы" : "не авторизованы";?> на сайте. <br />
                        <?php
                        if (!is_null($oCurrentUser))
                        {
                            // Для вывода даты регистрации пользователя значение из формата TIMESTAMP, 
                            // в котором оно хранится в MySQL, нужно преобразовать
                            $oDateTime = new DateTime($oCurrentUser->registration_date);
                            
                            echo "<p>Ваш логин: <strong>" . $oCurrentUser->login . "</strong>.</p>";
                            echo "<p>Ваша электропочта: <strong>" . $oCurrentUser->email . "</strong>.</p>";
                            echo "<p>Вы зарегистрировались: <strong>" . $oDateTime->format("d.m.Y") . "</strong>.</p>";
                            echo "<p>Вы можете <a href='/users.php?action=exit'>выйти</a> из системы.</p>";
                        }
                        else
                        {
                            ?>
                            <p>На этом сайте вам доступно:</p>
                            <ul class="list-unstyled">
                                <li>
                                    <a href="/users.php">Авторизация и регистрация</a>
                                </li>
                            </ul>
                            <?php
                        }
                        ?>
                    </p> 
                </div>
            </body>
            </html>
        

Для авторизованных пользователей мы добавили вывод информации о email и дате регистрации. Для того, чтобы выражение вроде $oCurrentUser->email возвращало что-либо кроме NULL, нужно добавить реализацию магического метода User::__get(), заодно реализуем метод User::unsetCurrent(), ведь нам ещё нужно обеспечить возможность выхода из системы для пользователя.


            <?php
            // Запрещаем прямой доступ к файлу
            defined('MYSITE') || exit('Прямой доступ к файлу запрещен');

            /**
            * Класс пользователя клиентского раздела сайта
            * Объект класса может создаваться как для существующих пользователей, так и для несуществующих с целью их регистрации
            */
            class User
            {
                // .....................

                /**
                * Завершает сеанс пользователя в системе
                * @return object self
                */
                public function unsetCurrent()
                {
                    // Уничтожение данных о пользователе в сессии
                    unset($_SESSION['login']);
                    unset($_SESSION['email']);
                    unset($_SESSION['password']);

                    // Обновляем страницу
                    header("Refresh:0;"); die();

                    return NULL;
                }

                // .....................

                /**
                * Магический метод для получения значения необъявленного свойства класса
                * Вернет значение из запрошенного поля таблицы, если оно разрешено в массиве $_allowedProperties
                * @return mixed string|NULL
                */
                public function __get(string $property) : string | NULL
                {
                    return (in_array($property, $this->_allowedProperties) ? $this->_user->$property : NULL);
                }
            }
            ?>
        

После выхода из системы и очистки сессии, страница обновляется. Сеанс пользователя завершен.

Заключение

На этом статью мы завершаем. Работа с авторизацией и регистрацией переведена с процедурного типа на объектно-ориентированный. Начинающему читателю выгода от такого перехода сейчас может быть не очень понятна, но понимание придёт.

«Программируйте на уровне интерфейса, а не на уровне реализации.»

Опытный читатель же обратит внимание, и, возможно, даже, наберётся наглости написать в комментариях о том, что стиль конечно объектно-ориентированный, но с ним что-то не так. Реализация какая-то то ли незаконченная, то ли беcцелевая какая-то, что ли. Это так. Но это тема для следующих статей.

Таблица `users` с прошлой статьи у нас не менялась.


            --
            -- Структура таблицы `users`
            --

            CREATE TABLE `users` (
            `id` int(10) UNSIGNED NOT NULL,
            `login` char(16) NOT NULL,
            `email` char(32) NOT NULL,
            `password` char(255) NOT NULL,
            `registration_date` timestamp NOT NULL DEFAULT current_timestamp(),
            `active` tinyint(1) UNSIGNED NOT NULL DEFAULT 1,
            `deleted` tinyint(1) UNSIGNED NOT NULL DEFAULT 0
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

            --
            -- Индексы сохранённых таблиц
            --

            --
            -- Индексы таблицы `users`
            --
            ALTER TABLE `users`
            ADD PRIMARY KEY (`id`),
            ADD KEY `active` (`active`),
            ADD KEY `deleted` (`deleted`);

            --
            -- AUTO_INCREMENT для сохранённых таблиц
            --

            --
            -- AUTO_INCREMENT для таблицы `users`
            --
            ALTER TABLE `users`
            MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
        

Просмотреть пример работы сценария можно здесь.

Исходный код описанных файлов доступен по ссылке.

  • Я опубликовал эту статью:12.01.2024
  • 1 040
  • Яндекс.Метрика

Меню сайта

Settings

Performance

CPU Load
60%
CPU Temparature
42°
RAM Usage
6,532 MB

Customer care

Reports

Projects

May 14, 2020

Upcoming events

12:00

Donec laoreet fringilla justo a pellentesque

13:20

Nunc quis massa nec enim

14:00

Praesent sit amet