Если информация была полезной для вас, вы можете поблагодарить за труды Юmoney: 41001164449086

Пишем PHP чат ООП MVC Ajax JavaScript

Сразу надо сказать что я не пытаюсь изобрести велосипед, или правильнее сказать чат, благо на сегодня их написано огромное количество. Скорее это проверка собственных навыков приобретённых после прохождения ряда курсов по PHP ООП, программированию, а также изучения JavaScript, помноженных на концепцию MVC. Всё написанное ниже является моим виденьем архитектуры построения web приложения, а именно чата, которое возможно поможет начинающим программистам.

Вступление. Структура приложения чата.

Итак, чат мы будем писать на PHP с использованием ООП в концепции MVC. При отправка сообщений и получение новых будет осуществляться динамически при помощи Ajax POST запросов.
Но прежде чем начать мне бы хотелось сразу показать структуру чата, архитектуру которая у нас выстроится в следующих частях.
Я не буду строить UML диаграммы, поскольку не все с ними знакомы, да и иллюстрация ниже более чем понятна:


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

Часть 1. MVC и файловая структура.

На самом деле самое сложное при написании любого приложения это создание правильной архитектуры. И здесь нам на помощь приходит концепция MVC, которая предлагает разделение приложения на части по функциональному признаку. Соответственно эти части будут находиться в разных файлах и папках организованных соответствующим образом. Именно поэтому мне хотелось бы заострить внимание на MVC чуть более подробнее.

Опишу концепцию MVC коротко, своими словами.
При таком подходе приложение разбивается на три части каждая из которых отвечает за свою сферу, а именно:

Model (Модель) – выполняет обработку данных, вычисления, и т.д.
View (Вид) – отвечает за вид, форму, в общем интерфейс отображаемый пользователю, на основании данных от модели
Controller(Контроллер) – получает данные от пользователя (от интерфейса) и  передаёт их соответствующим образом модели.

На картинке выше, модели показаны синем прямоугольником, виды - зелёным, а контроллеры жёлтым.

В соответствие с MVC, и некоторыми другими соображениями структура папок чата будет выглядеть следующим образом:

config – файлы конфигурации с параметрами подключения к БД, и т.д.
controllers  - контроллеры отвечающие за принятие решений по обработке данных от пользователя
css – файлы стилей
js – файлы JavaScript
lib – модели и библиотеки для работы с данными
templates – шаблоны, отвечающие за оформление внешнего вида  
index.php – ну и самый главный файл с которого начинается работа всего приложения.

Часть 2. Начало работы и автозагрузка файлов.

Поскольку я буду использовать PHP ООП подход, то за место скучного подключения нужных файлов через include (require), можно написать функцию которая будет подгружать нужные файлы при инстанцировании класса:

spl_autoload_register(function($class){
    $file = str_replace('\\', '/', $class) . '.php';
    if(file_exists($file)){
        require_once($file);
    }
});

Например:

$obj = new controllers\cMain;
$obj->request();

При попытке создать объект класса cMain из пространства имён (namespace) controllers (об этом чуть позже) мы автоматически выполним:

require_once controllers/cMain

Важно заметить, что функция, str_replace в функции автозагрузчика автоматически заменит '\’ на  ‘/’, таким образом происходит согласование пространства имён с файловой структурой приложения. Это очень важный момент.
Т.е создавая объект new controllers\cMain, мы фактически выполняем require_once controllers/cMain.

Красиво и изящно, а главное нет длинных имён классов, сложной функции автозагрузки, и  файловая структура согласована с пространством имён.

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

Мы только что разобрали работу файла index.php

Небольшое отступление.
Я думаю вы заметили приставку ‘c’ перед названием класса cMain, это говорит о том, что перед нами контроллер. Соответственно ‘m’ – будет означать модель. Для большей простоты и наглядности построения пользовательского интерфейса при помощи шаблонов я не буду использовать ООП, поэтому классов с ‘v’ у меня не будет.
Приставки в виде буквы в названии класса я сделал умышлено, для большей наглядности, и в действительности в этом нет никакой необходимости, поскольку пространство имён однозначно определяют принадлежность того или иного класса в концепции MVC.

Часть 3. Контроллеры.

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

Вот его код:

namespace controllers;
class cMain{
public function __construct(){       
    }
   
    public function request(){       
        if(!$_POST){
            $messages = new \lib\mMessagesCheck(null);
            $chat = new \lib\mChat($messages);
            $chat->show();
        }
        else {
            $messages = new \lib\mMessagesCheck($_POST);
            if (array_key_exists('transmit', $_POST))
                $messages->add();
            if (array_key_exists('receive', $_POST))
                $messages->showJson();
        }
    }
}

Как я говорил ранее, структура папок и пространство имён согласованны, по этому контроллер находится в пространстве имён (namespace) controllers, находясь при этом в папке controllers.

Выполняя в index.php единственный метод request(), мы проверяем наличие POST запросов.

Если POST запроса нет, то, это пользователь первый раз зашёл на страницу, и мы должны показать ему интерфейс, или проще говорят вывести собственно наш чат.
Для этого мы создаём объект сообщений класса mMessagesCheck, передавая в конструктор null (поскольку отсутствует POST запрос). После чего сам объект с сообщениями передаём в конструктор класса mChat, который и отвечает за построение пользовательского интерфейса.

Если же есть POST запрос, то это Ajax запросы, инициированные JavaScript составляющей чата.
Запросы POST могут быть двух видов 'transmit' и 'receive'. Для этого мы просто проверяем наличие соответствующих ключей в глобальном массиве $_POST. Названия 'transmit' и 'receive' выдуманные, но довольно неплохо отражают суть происходящего.

Первый запрос, 'transmit', говорит о необходимости добавить новое сообщение, для чего мы выполним в контроллере метод add().

Второй запрос 'receive', говорит о желании получить новые сообщения из БД. Здесь мы выполним метод showJson().

Забегая немного вперёд скажу, передавать и принимать сообщения (Ajax запросы), для динамического обновления страницы, мы будем в формате JSON. Не стоит пугаться этого названия, это всего лишь удобный формат представления данных и не более.

В обоих случаях нам потребуется создать объект с сообщениями new mMessagesCheck($_POST). Запрос POST мы передаём в конструктор, поскольку именно в нём будет содержатся информация о вновь добавляемом сообщении от пользователя в БД, либо о желании получить новые сообщения от БД к пользователю.

Крайне важно не возлагать работу моделей (обработку данных, вычисления) на контроллеры. Всё что должен сделать контроллер, это управлять процессом, но не более. В противном случае мы рискуем скатиться к чрезмерному разрастанию контроллера, или говоря научно к Fat Stupid Ugly Controllers (Толстые, тупые, уродливые контроллеры).

Ну да ладно, я отвлёкся, думаю, теперь настало самое время детально рассмотреть, классы модели с сообщениями mMessagesCheck и mMessages.

Часть 4. Модели mMessagesCheck и mMessages.

Сразу хочу сказать, что это самый сложный класс для понимания, поэтому, следует запастись терпением.

mMessagesCheck является прокси для класса mMessages, и оба они по паттерну GOF Proxy реализуют один и тот же интерфейс:

namespace lib;
interface iMessages{
    public function __construct($post);
    public function get();
    public function add();
    public function showJson();
}

В конструктор бы будем передавать POST запросы (если они есть).

Метод get() будет возвращать массив сообщений которые есть в БД, для дальнейшей обработки моделью класса mChat.
Метод add() будет добавлять новые сообщения в БД, руководствуясь тем что находиться в POST.
Метод  showJson() будет выводить JSON представление сообщений, на основании некоторых параметров, которые опять же находятся в POST.

mMessagesCheck осуществляет проверку данных полученных методом POST, и если они корректны, то передаёт их mMessages для дальнейшего взаимодействия с БД. Таким образом mMessagesCheck как бы защищает mMessages являясь кеширующим защитником. Но оба класса реализуют один и тот же интерфейс.

Итак mMessagesCheck, имеет следующий вид:

class mMessagesCheck implements iMessages{
    private $post;

    public function __construct($post){
        $this->post = $post;
    }

    public function add(){
        $correct = true;
        $mObj = json_decode($this->post['transmit']);
        if (!isset($mObj->user) or !isset($mObj->message))
            return;       
        $mObj->user = trim($mObj->user);
        $mObj->message = trim($mObj->message);
        $this->post['transmit'] = json_encode($mObj);        
        // Пробелы   
        if($mObj->user == '' || $mObj->message == '')
            $correct = false;
        //HTML
        if(preg_match("/[<\/][a-zA-Z]{1,10}[>]+/", $mObj->user) or preg_match("/[<\/][a-zA-Z]{1,10}[>]+/", $mObj->message))
            $correct = false;
        //SQL
    if(preg_match("/((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))/i", $mObj->user) or preg_match("/(\%27)|(\')|(\-\-)|(\%23)|(#)/i", $mObj->message))
        $correct = false;       
        if($correct){
            $real = new mMessages($this->post);
            $real->add();
        }
    }

    public function get(){
        $real = new mMessages($this->post);
        return $real->get();
    }

    public function showJson(){
        if ($this->post['receive'] != ''){
            $mObj = json_decode($this->post['receive']);
            if (preg_match("/^[\d\+]+$/", $mObj->last)){
                $real = new mMessages($this->post);
                $real->showJson();
            }
        }
    }
}

Начнём с самого простого, метода get().
Напомню, этот метод вызывается контроллером, если запрос исходит непосредственно от пользователя, т.е. не идёт речи о в взаимодействии данных от пользователя с БД. Поэтому этот метод абсолютно безопасен, и его можно переадресовать дальше объекту класса mMessages:

$real = new mMessages($this->post);
return $real->get();

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

Несколько сложнее работает метод showJson().
В нём проверяется что запрос не пустой, и если это так, то из последовательности символов переданных JavaScript, мы делаем полноценный объект:

$mObj = json_decode($this->post['receive']);

В этом объекте при помощи регулярного выражения мы проверяем на корректность одно единственное свойство last. В этом свойстве храниться порядковый номер последнего сообщения которое есть у пользователя. Если это только число, то всё в порядке, и мы также переадресовываем POST запрос объекту класса mMessages.

$real = new mMessages($this->post);
$real->showJson();

Здесь нам нет необходимости возвращать запрос, поскольку showJson(), вызывается для того чтобы вывести, показать данные. Именно эти данные увидит JavaScript при соответствующем запросе.

Метод add() работает практически аналогичным образом.
Он принимает POST запрос, делает из него объект. У этого объекта есть 2 свойства user и message, в которых содержится информации о имени пользователя и тексте сообщения которые отправляет пользователь.
Поскольку именно эти свойства (данные) мы будем записывать в БД, их необходимо хорошенько проверить на предмет всевозможных атак, что и делается регулярными выражениями строчками ниже.

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

Теперь настало самое время рассмотреть класс mMessages.
Именно он занимается взаимодействием (правда через посредников) с БД. Фактически, его основная задача, это подготовить POST данные для записи/получения данных в/из БД в виде SQL запросов и вернуть либо вывести результат.

Итак, собcтвенно код:

class mMessages implements iMessages{
    private $post;
       
    public function __construct($post){
        $this->post = $post;
        $dbConfig = new mConfigIni('config/db.ini');
        $this->my = new mMySQL($dbConfig->host, $dbConfig->db, $dbConfig->login, $dbConfig->pass);
    }
   
    public function get(){
        $query = "SELECT  * FROM `messages_oop`  ORDER BY `date_msg` DESC LIMIT 50;";
        $res = $this->my->request($query);
        while ($record = $res->fetch_assoc()){
            $arr[] = $record;
        }
        return array_reverse($arr);
    }
   
    public function showJson(){
        $mObj = json_decode($this->post['receive']);
        $t = "SELECT  * FROM `messages_oop` WHERE `id_msg` > '%d' ORDER BY `date_msg` DESC LIMIT 50;";
        $query = sprintf($t, $mObj->last);
        $res = $this->my->request($query);
        while ($record = $res->fetch_assoc()){
            $arr[] = $record;
        }
        if (isset($arr)){
            echo json_encode(array_reverse($arr)); //JSON_FORCE_OBJECT
        }
        else
            echo 'no';
    }
   
    public function add(){
        $mObj = json_decode($this->post['transmit']);
        $t = "INSERT INTO `messages_oop` (`date_msg`, `user`, `message`) VALUES (now(), '%s', '%s');";
        $query = sprintf($t, $mObj->user, $mObj->message);
        if ($this->my->request($query));
            echo 'success';
       
    }
}

Фактически каждый метод в этом классе формирует необходимый SQL запрос для БД, после чего он выполняется, после чего результат возвращается либо отображается. Детально рассматривать SQL запросы не вижу смысла, и хочу остановлюсь лишь на некоторых особенностях класса mMessages.

В конструкторе этого класса:

$dbConfig = new mConfigIni('config/db.ini');
$this->my = new mMySQL($dbConfig->host, $dbConfig->db, $dbConfig->login, $dbConfig->pass);

В переменной $dbConfig будет храниться объект со свойствами, параметрами которые необходимы для подключения непосредственно к БД. Эти свойства мы передадим в конструктор класса mMySQL, который имеет единственный метод request($query), целью которого является выполнение соответствующего SQL запроса $query и возвращение результата.

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

Первый класс mConfigIni, читает конфигурацию из файлов вида 'свойство=значение', и формирует объект с искомыми свойствами, которые позднее удобно использовать.

Второй класс mMySQL на основании данных конструктора осуществляет соединение с БД и выполняет запрос при вызове метода request($query).

Хочу обратить внимание на особенность метода add() класса mMessages.
В случае если добавление сообщения прошло успешно, мы отправим сообщение ‘success’.

if ($this->my->request($query));
            echo 'success';

Это сообщение будет принимать JS, понимая тем самым что сообщение было успешно добавлено в БД.

Почти также работает метод showJson() он берёт сообщения из БД, номера которых больше чем отправил JS (об этом чуть позже). Если таких сообщений нет, то выводиться ‘no’, если есть, то они выводятся в JSON формате для последующей интерпретацией JS:

if (isset($arr)){
    echo json_encode(array_reverse($arr)); 
}
else
    echo 'no';

Часть 5. Модели для создания интерфеса чата, mChat и mTemplate.

Здесь я предлагаю начать с последнего, а именно mTemplate, класс прост и изящен.

class mTemplate {
    private $html;
    public function __construct($file, $var = array()){
        foreach($var as $key => $item){
            $$key = $item;
        }
       
        ob_start();
            include $file;
        $this->html = ob_get_clean();
    }
   
    public function get(){
        return $this->html;
    }
   
    public function show(){
        echo $this->html;
    }
}

Целью данного класса является создание объектов на основании файлов с шаблонами.

Наиболее интересен здесь конструктор. Мы передаём ему 2 параметра, а именно файл с шаблоном на основании какого будет создан объект, и переменные с значениями в виде массива которые будем использовать внутри этого шаблона:

public function __construct($file, $var = array())

С первым параметром всё понятно, а вот назначение второго параметра в виде массива может несколько озадачить.

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

Проще всего понять это на примере.
Рассмотрим, что произойдёт в конструкторе класса при:

$obj = new mTemplate('templates/messages.php', array('messages'=> ‘Много сообщений’)

При выполнение цикла foreach показанного выше в переменную $messages будет записано значение ‘Много сообщений’. Далее мы стартуем буферезованный вывод, состоящий из include файла messages.php.
Для большей простоты, предположим что этот файл представляет из себя что-то вроде:

<div id="messages">$messages </div>

В этом файле при выполнении переменная $messages получит описанное выше значение. 

После чего мы останавливаем буферезацию вывода и записываем значение в свойство $this->html объекта.
Теперь в свойстве html объекта класса mTemplate содержится:

<div id="messages">Много сообщений</div>

Т.е. в свойстве готовый HTML код который можно либо использовать дальше, либо показать пользователю. От сюда следуют 2 основных метода:

get() – который вернёт готовый HTML код для последующего использования, например вставки в другой более общий шаблон.
show() – выведет готовый HTML, тем самым передав его браузеру пользователя.

Собственно, именно так и работает mChat:

namespace lib;
class mChat {
        private $messages;
        public function __construct($messages){
        $this->messages = $messages;
    }
   
    public function show(){
        $htmlMessages = new mTemplate('templates/messages.php', array('messages'=> $this->messages->get()));
        $htmlSend = new mTemplate('templates/send.php');
        $htmlPopup = new mTemplate('templates/popup.php');
        $htmlAll = new mTemplate('templates/main.php', array('templateMessages' => $htmlMessages->get(), 'templateSend' => $htmlSend->get(), 'templatePopup' => $htmlPopup->get()));
        $htmlAll->show();
    }
}

Этот класс принимает в конструктор единственный параметр – объект сообщений, который мы передали ранее в контроллере cMain.

Далее следует рассмотреть метод show().
В первой строке, мы записываем в переменную $htmlMessages объект с готовым HTML кодом, построенный на основании шаблона 'templates/messages.php' и массива сообщений (полученного в результате вызова метода messages->get()).
В messages.php находится шаблон для построения сообщений чата.
Теперь $htmlMessages содержится объект в свойстве html которого, что-то вроде:

<div id="messages">
        <div class="message" data-msg_num="498">
            <div class="message__user">123</div>
            <div class="message__time">2018-06-28 23:20:32</div>
            <div class="message__text">  </div>
        </div>
        <div class="message" data-msg_num="499">
            <div class="message__user">123</div>
            <div class="message__time">2018-06-28 23:21:55</div>
            <div class="message__text"> </div>
        </div>
… повторяется много раз…
</div>
<script src="/js/messages.js"></script>

Почему в переменную мы не записываем непосредственно HTML код, а объект с свойством, станет понятно чуть ниже.

Аналогично мы поступаем с шаблоном popup.php, который нужен для включения на страницу HTML кода для всплывающих сообщений. А также send.php в котором содержится шаблон для формы отправки данных, в чат.

Самое интересное происходит в предпоследней строке метода:

$htmlAll = new mTemplate('templates/main.php', array('templateMessages' => $htmlMessages->get(), 'templateSend' => $htmlSend->get(), 'templatePopup' => $htmlPopup->get()));

Здесь мы производим окончательную сборку.
В общий шаблон main.php мы включаем все составные части HTML. Именно для этого мы вызываем методы get(),  у соответствующих объектов. Таким образом мы передаём в шаблон main.php составные части HTML кода, но ещё не показываем его пользователю. А вот строка:

$htmlAll->show();

Делает именно это.

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

Схематично иерархию шаблонов можно представить так:

main.php(messages.php + send.php + popup.php)

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

Внимательный читатель, заметил, что в конце шаблона messages.php есть подключение внешнего скрипта <script src="/js/messages.js"></script>.
В некоторых шаблонах есть отсылки на JavaScript файлы которые потребуется для динамического обновления страницы, и отправки данных с новыми сообщениями на сервер в БД.

Об этом мы и поговорим далее.

Часть 6. JavaScript Ajax

На данный момент у нас есть полностью готовая HTML страница, с сообщениями которые есть БД, формой отправки и т.д. Но она полностью статична. Передавать свои сообщения и получать новые от сервера мы будем с использованием JavaScript.

Для этого в шаблонах ранее подключены 3 js файла:

jquery.min.js
send.js
messages.js

Первый это библиотека Jquery.
По большому счёту она потребуется нам лишь для более удобного формирования Ajax запросов, и для некоторой анимации. Всё это можно сделать и голыми средствами JavaScript, однако Jquery значительно упростит нашу JS составляющую.

Прежде чем переходить непосредственно к рассмотрению JS файлов, надо понимать что у нас в данный происходит в браузере.
А в нем у нас примерно такой HTML код полученный от сервера:


<body>
    <div id="wrapper">
        <!-- Messages -->
        <div id="messages">
        <div class="message" data-msg_num="498">
            <div class="message__user">123</div>
            <div class="message__time">2018-06-28 23:20:32</div>
            <div class="message__text">  </div>
        </div>
        <div class="message" data-msg_num="499">
            <div class="message__user">123</div>
            <div class="message__time">2018-06-28 23:21:55</div>
            <div class="message__text"> </div>
        </div>
    </div>
<script src="/js/messages.js"></script>

<!-- Send -->
<div id="send">
           <form action="index.php" method="POST"  name="send">
               Имя: <input name="user" type="textarea" size="25"><br>
               Сообщение: <input name="message" type="textarea" size="90">
              <button type="submit">Отправить</button>
          </form>
</div>
<script src="/js/send.js"></script>       

<!-- pop up info-->
            <div id="popup">
                <div class="popup__center">
                    <div class="popup__center-to-left">
                        <div class="popup__center-to-right">
                            <div class="popup__text">
                            </div>
                        </div>
                    </div>
                </div>
            </div>   

</div>
</body>
</html>

Теперь рассмотри 2 JS фала в которых и происходит Ajax магия.

Начнём с более простого send.js:

"use strict";
var elForm = document.forms.send;
var elUser = elForm.elements.user;
var elMessage = elForm.elements.message;
$(document).ready(function(){   
    elForm.onsubmit = function(e){
        if (checkSend(elUser.value, elMessage.value))
            send(elUser.value, elMessage.value);
        return false;
    };

});


function send(user, message){
    var send = Object.create(null);
    send.user = user;
    send.message = message;
    send = JSON.stringify(send);

    $.ajax({
        type: "POST",
        url: 'index.php',
        data : {transmit: send},
        success: function (reply) {
            if (reply == 'success'){
                elMessage.value = '';
            } else {
                //console.log(reply);
            }
        }
    });   
}

function checkSend(user, message) {
    var errors = [];
    user = user.trim();
    message = message.trim();
   
    if (!user)
        errors.push('Нет имени пользователя');
    if (!message)
        errors.push('Нет сообщения');

    //HTML
    var regexpHTML = /[<\/][a-zA-Z]{1,10}[>]+/;
    if (regexpHTML.test(user) || regexpHTML.test(message)){

        errors.push('Теги запрещены');
    }
       
    if (errors.length > 0){
        $('.popup__text').html(errors.join('<br>\n'));
        $('#popup').fadeIn(100);
        $('#popup').delay(1500).fadeOut(300);
        return false;
    }
    return true;
}

Первым делом мы навешиваем на нашу форму обработчик - анонимную функцию, события типа submit. Это не что иное как обычная функция, которая будет выполнена при попытке отправить сообщение.

В начале при помощи функции checkSend(user, message) мы будем проверять на стороне клиента корректность отправляемых данных. Если что-то не так, то выводить всплывающее сообщение.
Важно понимать что поскольку JS выполняется на стороне клиента, то он всегда может быть изменён, и не стоит полагаться на него в вопросах безопасности. Именно поэтому ранее мы при помощи регулярных выражение проверяли в mMessagesCheck принимаемые данные от пользователя.

Если данные корректны мы выполняем функцию send(user, message), аргументами которой также будут имя пользователя и текст сообщения.

В самом начале этой функции мы создаём пустой объект, свойствами которого будут сообщение и имя пользователя:

var send = Object.create(null);
send.user = user;
send.message = message;
send = JSON.stringify(send);

Последняя строка представляет объект send в виде JSON последовательности. Из этой самой последовательности мы и восстанавливали ранее полноценный объект на сервере выполняя json_decode(...).

Отправлять дынные на сервер мы будем в POST запросе сформированным при помощи Jquery.

        type: "POST",
        url: 'index.php',
        data : {transmit: send},

При этом в последней строке transmit, не что иное как ключ которые мы искали ранее в POST запросе, а send как уже было сказано ранее это JSON представление объекта.

При успешной отправке данных, именно отправке, но не добавлении в БД, будет выполнена callback функция, аргументом которой будет ответ сервера:

success: function (reply) {
    if (reply == 'success'){
                elMessage.value = '';

Помните, ранее при выполнении метода add() класса mMessages, мы генерировали сообщение 'success', именно это сообщение и есть ответ сервера.
И если ответ сервера  reply == 'success' то мы просто затираем поле формы с сообщением для ввода нового сообщения.

На этом отправка сообщения завершена. При этом не происходит полная перезагрузка страницы. Мы просто отправили наше сообщение методом POST используя JS на сервер. А там уже контроллер cMain разобрался, что с ним делать.

Чуть более сложнее обстоит дело с messages.js, по этому я оставил в нём больше комментариев:

"use strict";
var messages = document.getElementById('messages');
//Первое попавшийся элемент с сообщением для клонирования
var message = document.querySelector('.message').cloneNode(true);
//номер последнего сообщения берём из атрибута последнего узла
var lastMessageNum = messages.lastElementChild.getAttribute('data-msg_num') || 0;

$(document).ready(function(){
    setInterval(refreshMessages, 500);
});

function refreshMessages(){
    var send = Object.create(null);
    send.last = lastMessageNum;
    send = JSON.stringify(send);
    $.ajax({
        type: "POST",
        url: 'index.php',
        data : {receive: send},
        success: function (reply) {
            //если есть что обновлять
            if(reply != '' && reply != 'no'){
                try {
                    var arr = JSON.parse(reply);                    
                    //будем действовать только если порядковый номер первого принятого сообщения действительно больше последнего что есть у нас
                    //это нужно на тот случай если БД отвечает медленно, и пришло более одного ответа                
                    if (+arr[0].id_msg > lastMessageNum){
                        lastMessageNum = +arr[arr.length - 1].id_msg;    //номер последнего принятого сообщения
                                        
                        //Добавляем новые в конец
                        for (var i = 0; i < arr.length; i++){
                            var messageNew = message.cloneNode(true);
                            messageNew.setAttribute('data-msg_num', arr[i].id_msg);
                            messageNew.querySelector('.message__user').innerHTML = arr[i].user;
                            messageNew.querySelector('.message__text').innerHTML = arr[i].message;
                            messageNew.querySelector('.message__time').innerHTML = arr[i].date_msg;
                            //немного анимации
                            messageNew.style.display = 'none';
                            messages.appendChild(messageNew);
                            $(messageNew).slideDown(500);
                        }
                        if (messages.children.length >= 50){ //на случай если мало сообщений в БД
                            //И удаляем столько старых сколько пришло новых
                            for (var i = 0; i < arr.length; i++){
                                messages.removeChild(messages.children[0]);
                            }
                        }    
                    
                    }
                } catch(e) {
                    
                }
            }

        }
    });
}

Первым делом мы будем регулярно проверять наличие новых сообщений, для чего запускаем планировщик JS, который будет регулярно выполнять функцию refreshMessages().

Примечание: Да я в курсе за COMET, но тут я хочу показать, как реализовать это путём регулярной проверки новых сообщений на сервере.

Здесь мы так же формируем POST запрос, на основании объекта send:

send.last = lastMessageNum;
send = JSON.stringify(send);

Но теперь у него будет только одно свойство last, значением которого будет номер последнего сообщения в чате:

lastMessageNum = messages.lastElementChild.getAttribute('data-msg_num');

Помните ранее я показывал HTML код, и в нём были строки вида:

<div class="message" data-msg_num="499">

Здесь мы берём последний элемент из сообщений и получаем от него значение атрибута data-msg_num, в котором как раз и храниться эта информация.
Теперь мы можем полностью сформировать наш POST запрос к серверу:

data : {receive: send}

Заметили ‘receive’ ? Т.е. в POST запросе мы говорим о желании получить сообщения, и указываем порядковый номер последнего сообщения которое уже есть у нас.

Но самое интересное происходит при ответе сервера в анонимной callback функции function (reply).

Первым делом мы проверяем есть ли новые сообщения для нас:

if(reply != '' && reply != 'no')

Помните ранее в классе mMessages был метод showJson(), который в случае отсутствия новых сообщений отвечал ‘no’. Здесь мы как раз и проверяем этот ответ.

Если сообщения есть, то начинаем формирование массива с объектами сообщений:

var arr = JSON.parse(reply);

В этом массиве будут объекты со свойствами:

id_msg;
user;
message;
date_msg;

Думаю названия свойств говорят сами за себя и нет смысла описывать их.

Далее в цикле который выполниться столько раз сколько длина этого массива:

for (var i = 0; i < arr.length; i++)

Мы создаём новый элемент сообщения путём клонирования старого и его видоизменения:

var messageNew = message.cloneNode(true);
messageNew.setAttribute('data-msg_num', arr[i].id_msg);
messageNew.querySelector('.message__user').innerHTML = arr[i].user;
...

После чего динамически добавляем новые сообщения на страницу:

messages.appendChild(messageNew);

У некоторых может возникнуть вопрос зачем клонировать существующие сообщения, не проще ли создать новые элементы с нуля? В принципе можно. Но тогда вид добавляемых сообщений будет определяться файлом messages.js, а исходные сообщения будут генерироваться при помощи шаблона messages.php. А нам бы хотелось, чтобы внешний вид был определён по возможности в одном месте.
Именно поэтому мы и применяем глубокое клонирование (cloneNode(true)).

Осталось дело за малым, просто затереть тоже количество сообщений, что и было добавлено, это мы и делаем в цикле.

for (var i = 0; i < arr.length; i++){                       
       messages.removeChild(messages.children[0]);
}

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

Часть 7. База Данных

Всё это время я не касался непосредственно тому где хранятся, и куда отправляются сообщения, а именно о БД MySQL. Всё происходит в одной единственной таблице messages_oop, имеющую следующую структуру:

Столбец   

Тип   

id_msg    

int(7)

date_msg    

datetime

user    

varchar(32)

message    

varchar(256)

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

Update (2018-08-09)

Недавно закончил курсы по PostgreSQL, в связи с чем решил добавить поддержку БД Postgres в чат. А поскольку писать 2 модели для доступа к различным БД (MySQL и PostgreSQL), не совсем целесообразно, то в чате теперь используется модель доступа к БД на основе PDO.

Также поскольку MySQL и PostgreSQL всё же отличаются в синтаксисе SQL, то несколько отличаются команды по добавлению записей в чат.

MySQL:         INSERT INTO messages_oop (`date_msg`, `user`, `message`) VALUES (now(), 'user', 'message');
PosgtreSQL:  INSERT INTO messages_oop ("date_msg", "user", "message") VALUES (now()::timestamptz(0), 'user', 'message');

Эти изменения отразились в DbSQL.php (новая модель) и mMessagesCheck.php (теперь учитывает синтаксис различных бд).

Обновлять архив с чатом не вижу смысла, ориентируйтесь на GitHub.

Update (2018-08-16)

Внедрена поддержка Redis.
Идея заключается в подтягивании сообщений из SQL БД в Redis в момент старта, а также в момент добавления новых сообщений.

Может возникнуть вполне логичный вопрос, не лучше ли при добавлении новых сообщений добавлять их сразу в Redis? В принципе конечно можно, но тогда в случае если Redis не использует запись на диск, мы рискуем потерей данных. Есть и ещё один неприятный момент. Вполне вероятна ситуация когда id сообщений в Redis и SQL могут разойтись, а это черевато последствиями.

По этому Redis используется скорее вроде кеша, с полным обновлением в случае изменения SQL БД (добавления новых сообщений).

Будте внимательны, по умолчанию Redis включен (настройки в redis.ini), по этому если ваш хостинг не поддерживает Redis (а это в большинстве случаев так), то в конфиге измените опцию на status=off.

Заключение

На написание чата у меня ушло около 2-х суток чистого времени, а статьи и того меньше, по этому не исключено наличие ошибок как в коде, так и в тексте.
Безусловно, если руководствоваться принципами SOLID, то класс mMessages желательно разбить ещё на 2.
Также как вы могли заметить ранее, JSON данные формирует mMessages, что тоже лучше делать с использованием шаблонов (применительно к MVC - Вид), но только рассчитанных на выдачу не HTML кода, а JSON. Но по скольку здесь всё довольно просто, я решил пойти на такое упрощение.

Демонстрация работы чата: demo.unboxit.ru/chat

Все исходные тексты доступны на gitHub: github.com/dark705/chat-oop-mvc-ajax
И в виде zip архива: php_oop_chat.zip

Советую ориентироваться на gitHub, поскольку именно там будет находиться актуальная версия, в случае если я решу внести какие-либо изменения.

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

Добавить комментарий


Если информация была полезной для вас, вы можете поблагодарить за труды Юmoney: 41001164449086