/rating-skf

Рейтинг преподавателей СКФ МТУСИ

Primary LanguagePHP

Рейтинг преподавателей СКФ МТУСИ

Введение

В данной работе реализуется приложение, работающее с использованием клиент-серверной архитектуры. Эта архитектура позволяет распределить вычислительную нагрузку между клиентской машиной и сервером. Как клиентом, так и сервером в клиент-серверной архитектуре являются программы. Клиентской программой может быть Web-браузер, почтовый клиент, BitTorrent-клиент и т.п. Серверная программа это Web-сервер Apache или Nginx, СУБД MySQL или MariaDB и другие. Взаимодействие клиента и сервера в большинстве случаев происходит посредством глобальной сети Интернет: как правило используется стек протоколов TCP/IP. Особенностью этого взаимодействия является то, что его инициатором всегда является клиент. Сервер всегда лишь отвечает на запросы, поступающие от клиента. На один сервер могут одновременно отправлять запросы несколько клиентов.

Для реализации данной архитектуры была поставлена задача разработать систему подсчета рейтинга преподавателей СКФ МТУСИ. В качестве проекта было разработано веб-приложение, использующие язык программирования PHP и СУБД MySQL на бэкенде и HTML, CSS и фреймфорк Bootstrap на фронтенде. Для реализации внутренней логики приложения был использован архитектурный паттерн MVC: Model-View-Controller.

Паттерн MVC

MVC расшифровывается как Model-View-Controller или Модель-Представление-Контроллер. Это архитектурный паттерн, который применяется для разработки приложений, в том числе и для Web. Паттерн MVC создан для удобного разделения данных приложения на три независимые части: модель, представление и контроллер. Модель реагирует на команды контроллера и предоставляет возможность работы с данными: реализует запросы в базу данным, проверяет данные на корректность. Представление отвечает за вывод полученных данных конечному пользователю, иными словами отвечает за представление этих данных в некотором виде. Контроллер является «связующим звеном» между представлением и моделью, обеспечивая обмен данными между пользователем и системой как в одну, так и в другую сторону.

Настройка клиент-серверного взаимодействия

Так как цель данной работы это получение навыков разработки клиент-серверных приложений, то было принято решение использовать явное разделение на клиентскую машину и сервер. Можно было бы установить и использовать серверные приложения (такие как Apache и MySQL) на хост-машине и с нее же отправлять запросы на них, но в таком случае стирается четкая грань, разделяющая машины клиентов и сервер. Оно и не удивительно, ведь и виртуально, и физически сервер и клиент являются одной и той же машиной. В идеале нужно было бы использовать удаленный сервер, на котором было бы развернуто наше приложение и с различных машин отправлять на него запросы для демонстрации клиент-серверного взаимодействия. Но ввиду необходимости финансовых затрат на аренду удаленного сервера, было принято решение использовать программу, являющуюся абсолютно бесплатной. Для эмуляции удаленного сервера на хост-машине установлено специальное программное обеспечение Oracle VM VirtualBox, создана виртуальная машина с установленной операционной системой Linux Lubuntu 18.04 LTS, а также настроена переадресация портов для доступа к гостевой машине из хост-машины.

Установка и настройка LAMP

Допустим, на рабочей машине уже установлено программное обеспечение Oracle VM VirtualBox, а также настроена операционная система Linux Lubuntu 18.04 LTS на нем. Приступим к установке стека программ LAMP. Заходим в консоль гостевой машины и вводим следующие команды:

sudo apt update
sudo apt upgrade
sudo apt install apache2
sudo apt install mysql-server
sudo apt install php-mcrypt php-mysql libapache2-mod-php php-mbstring

Возможно, система потребует пароль администратора. Вводим пароль и ждем окончания выполнения команд. Первая команда обновляет списки ПО, доступного в стандартных репозиториях. Вторая обновляет ПО, установленное на сервере. Третья команда устанавливает веб-сервер Apache2. Четвертая устанавливает СУБД MySQL. Последняя команда устанавливает расширения языка php, необходимые для корректной работы нашего приложения. Далее необходимо создать директорию, в которой будут располагаться файлы нашего проекта. Сделать это можно следующей командой:

sudo mkdir /var/www/rating-skf.org

Теперь приступим к настройке сервера Apache. Все файлы настроек находятся в директории /etc/apache2. Скопируем настройки по умолчанию:

sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/rating-skf.org.conf

И изменим их:

sudo nano /etc/apache2/sites-available/rating-skf.org.conf

Раскомментируем строку ServerName и запишем в нее:

ServerName rating-skf.org

Укажем директорию, содержащую файлы сайта:

DocumentRoot /var/www/rating-skf.org

Включаем сайт:

sudo a2ensite rating-skf.org

Перезагружаем сервер:

sudo service apache2 reload

В виртуальной машине добавляем в файл /etc/hosts строку:

127.0.0.1 rating-skf.org

После выполнения приведенной последовательности действий мы имеем настроенный стек LAMP, а также домен rating-skf.org на локальной машине. Проверить работоспособность можно введя rating-skf.org в строке браузера гостевой ОС. Если в ответ была получена стандартная страница приветствия Apache, значит все работает правильно.

Установка Guest Additions в ВМ и настройка общих папок

Теперь устанавливаем дополнения гостевой ОС. Для этого монтируем образ с дополнениями через верхнее меню «Устройства» работающей виртуальной машины. В выпадающем меню выбираем «Подключить образ диска Дополнений гостевой ОС...". Монтируем диск (Обычно диск монтируется в директорию /media/lubuntu/VboxGAs***, где вместо «***» стоит версия Дополнений):

sudo mount /dev/cdrom /mnt

Переходим в директорию /mnt:

cd /mnt

Устанавливаем несколько дополнительных пакетов, необходимых для успешной установки Guest Additions:

sudo apt install gcc make perl

Из всех доступных нам файлов в директории /mnt выбираем файл VBoxLinuxAdditions.run и запускаем его на выполнение:

sudo ./VboxLinuxAdditions.run

Отмонтируем диск:

sudo umount /dev/cdrom

После этого стала доступна возможность использовать общие с хост-машиной папки. Создаем на хосте папку с проектом, например в ~/project. Затем добавляем общую папку в настройках виртуальной машины. Общие папки → Добавить новую общую папку. «Путь к папке» это путь к проекту на хост-машине (~/project), а «точка подключения» это папка в гостевой машине, в нашем случае это должна быть /var/www/rating-skf.org. Жмем ОК два раза: в окне добавления папки и в окне настроек. Перезагружаем виртуальную машину командой:

systemctl reboot

Настройка переадресации портов

Для того, чтобы с хост-машины иметь доступ к портам гостевой машины, нужно настроить т.н. «проброс» или, другими словами, переадресацию портов. Для этого в настройках виртуальной машины:

Настройки → Сеть → Дополнительно → Проброс портов

Добавляем новое правило:
Порт хоста: любой не занятый порт, например 8081;
Порт гостя: 80.

На хост-машине редактируем файл hosts:

127.0.0.1 rating-skf.org

Теперь на хост-машине доступен сайт по адресу rating-skf.org:8081. Для проверки правильности настройки введем адрес rating-skf.org:8081 в строке браузера именно хост-(а не гостевой) машины. Стоит обратить внимание на то, что адрес нужно указывать именно так, как обозначено выше: без указания протокола (http:// или https://) или службы (www), которые браузеры автоматически дописывают к строке запроса. Если показывается уже знакомая страница приветствия Apache, значит все работает правильно.

Настройка Apache для переадресации запросов

Ввиду особенностей реализации нашего проекта, нам необходимо настроить сервер Apache для возможности переадресации запросов. Правила переадресации будут указаны в файле .htaccess в корневом каталоге проекта. Для настройки открываем файл конфигураций сервера apache2.conf командой:

sudo nano /etc/apache2/apache2.conf

Находим в нем контейнер Directory и меняем в нем строки:

<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
</Directory>

на

<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>

Это позволит серверу прочитать файл .htaccess. Подключаем модуль перезаписи:

sudo a2enmod rewrite

Перезагружаем сервер:

sudo systemctl restart apache2

Описание внутреннего устройства проекта

Данный проект представляет собой множество вложенных директорий, предназначение каждой из которых будет описано далее. Как было сказано ранее, данным проект реализует шаблон MVC, следовательно, в этом проекте присутствуют отдельные директории для моделей (models), представлений (views) и контроллеров (controllers). Древовидное представление проекта:

rating-skf
├── components
│      ├── Controller.php
│      ├── Db.php
│      └── Router.php
├── config
│      ├── db_params_example.php
│      ├── db_params.php
│      └── routes.php
├── controllers
│      ├── RatingController.php
│      ├── SiteController.php
│      └── UserController.php
├── index.php
├── models
│      ├── Check.php
│      ├── Rating.php
│      └── User.php
├── README.md
├── template
│      ├── css
│      │      └── style.css
│      └── images
│               └── profile_default.jpg
└── views
    ├── layouts
    │      ├── footer.php
    │      └── header.php
    ├── rating
    │      ├── edit.php
    │      └── index.php
    ├── site
    │      ├── index.php
    │      └── login.php
    └── user
        ├── edit.php
        └── password.php

Директория components

В этой директории хранятся различные компоненты, обеспечивающие функционирование приложения. Controller.php это компонент, созданный для наследования другими контроллерами. Следуя принципу DRY (Don't repeat yourself) мы единожды реализуем методы и свойства, необходимые всем остальным контроллерам, которые будут его наследовать.

public function __construct() {
  // Получаем идентификатор пользователя
  $this->isLogged = User::isLogged();
  // Если пользователь авторизован
  if ($this->isLogged) {
    //Получаем информацию о пользователе
    $this->userInfo = User::getUserInfoById($this->isLogged);
  }
}

Db.php это компонент, обеспечивающий взаимодействие с базой данных MySQL посредством PDO (PHP Data Object). PDO это расширение языка PHP, которое обеспечивает единый интерфейс для работы с различными базами данным. В компоненте Db.php реализован абстрактный класс Db с одним приватным свойством $DBH (Database Handle) и тремя статическими методами: openConnection(), getConnection() и closeConnection(). Метод openConnection() открывает соединение с базой данных, DSN (Data Source Name) которой указаны в файле db_params.php в виде массива. Метод getConnection() возвращает соединение с базой данных в виде переменной $DBH. Метод closeConnection() закрывает соединение с базой данных присваиванием переменной $DBH значения null.

public static function getConnection() {
  // Если соединение еще не открыто, то открываем новое соединение
  if (!self::$DBH) self::openConnection();
  return self::$DBH;
}

Router.php это компонент, предназначенный для обработки различных маршрутов. В этом компоненте реализован одноименный класс Router, в котором присутствуют одно приватное свойство $routes, конструктор и два метода: getURI() и run(). В ассоциативном массиве $routes хранятся пары ключ => значение, где ключом является URI, запрошенный пользователем, а значением — название соответствующего контроллера и экшена. Значения этого массива берутся из файла /config/routes.php. В методе run() происходит обработка URL и в соответствии с полученными данными управление передается тому или иному экшну некоторого контроллера.

Директория config

Эта директория создана для хранения различных настроек. Файл db_params_example.php является примером файла, хранящего данные доступа к базе данных. Файл с реальными данными хранится в этой же директории и называется db_params.php. Так как логины и пароли пользователей СУБД являются приватной информацией, то выкладывать их в открытый доступ нельзя не в коем случае. Файл db_params_example.php хранит лишь структуру массива, который должен быть заполнен данными для доступа к БД.

<?php

// Массив с параметрами подключения к базе данных
return array(
    'host' => '',
    'dbname' => '',
    'user' => '',
    'password' => '',
    'charset' => 'utf8',
);

Файл routes.php хранит ассоциативный массив, в котором содержатся пары ключ => значение, где ключом является URI, запрошенный пользователем, а значением — название соответствующего контроллера и экшена.

return array(
  'login' => 'site/login',
  'logout' => 'site/logout',
  'edit/password' => 'user/password',
  'edit' => 'user/edit',
  'rating/edit' => 'rating/edit',
  'rating' => 'rating/index',
  '' => 'site/index',
);

Директория controllers

Эта директория предназначена для хранения контроллеров. В ней присутствуют три контроллера: контроллер рейтинга RatingController, контроллер сайта SiteController и контроллер пользователей UserController.

Контроллер RatingController.php обрабатывает запросы, связанные с подсчетом рейтинга. У него есть два экшена: actionIndex() и actionEdit(). Первый отвечает за обработку запросов на просмотр рейтинга, а второй за редактирование.

public function actionIndex()
{
  // Если пользователь не авторизован
  if (!$this->isLogged) {
    // Редирект на страницу входа
    header('Location: /login');
  }
  // Получаем рейтинг пользователя
  $rating = Rating::getRating();
  // Получаем коэффициент пользователя
  $coefficient = Rating::getCoefficient();

  require_once(ROOT . '/views/rating/index.php');
  return true;
}

Контроллер SiteController.php отвечает за функционирование всего сайта, за главную страницу, а также за вход и выход из системы. Контроллер UserController.php отвечает за те функции сайта, которые связаны с данными пользователя: личные данные, пароль.

Директория models

Данная директория создана для хранения файлов модели. В проекте используется три модели: Check, Rating и User. Из названия ясно, что первая реализует функционал проверки различных данных на валидность (проверка фамилии, имени, отчества, адреса электронной почти, пароля). Вторая реализует функционал бизнес-логики рейтинга преподавателей. Иными словами, позволяет вычислять рейтинг, а также вносить в него изменения. И, наконец, третья модель (User) реализует взаимодействие с данными пользователя. Как было сказано выше, модель Check.php реализует различные проверки данных, введенных пользователем. Реализованы проверка логина и пароля, введенных пользователем при попытке входа в систему; проверка данных, введенных при редактировании анкеты; проверка пароля на соответствие требованиям безопасности (минимальная длина, наличие специальных символов) и другие.

public static function checkDataLogin($userData) {
  // Массив с ошибками по умолчанию false
  $errors = false;
  // Если введенный email некорректен, то пишем соответствующую ошибку в массив ошибок
  if (self::incorrectEmail($userData['email'])) { $errors[] = 'Неправильный email'; }
  // Если введенный пароль некорректен, то пишем соответствующую ошибку в массив ошибок
  if (self::incorrectPassword($userData['password'])) { $errors[] = 'Пароль должен быть длинее пяти символов'; }
  return $errors;
}

В модели Rating.php обрабатываются данные, связанные с рейтингом преподавателя. В этой модели есть статические функции: getRating(), getCoefficient() и edit($newRatingData). Первая запрашивает данные о рейтинге преподавателя из базы данных и возвращает их тому контроллеру, который вызвал данную функцию. Вторая получает коэффициент пользователя. А третья сохраняет изменения рейтинга в базе данных.

public static function getRating() {
  // Соединение с БД
  $db = Db::getConnection();
  // Получаем id пользователя
  $id = User::isLogged();
  // Сумма по всем показателям
  $total = 0;

  // Выборка из OD_table
  $sum = 0;
  $sql = 'SELECT * FROM OD_table
          WHERE teacher_id = :teacher_id';
  $result = $db->prepare($sql);
  $result->bindParam(':teacher_id', $id, PDO::PARAM_INT);
  $result->execute();
  $rating['OD']['data'] = $result->fetch(PDO::FETCH_NUM);
  // Выбрасываем первый элемент
  array_shift($rating['OD']['data']);
  // Считаем сумму
  foreach ($rating['OD']['data'] as $key => $value) {
    $sum += $value;
  }
  $rating['OD']['sum'] = $sum;
  $total += $sum;
    //. . . 
}

Модель User.php обрабатывает данные пользователя. С помощью различных методов этой модели можно как получить данные о пользователе системы, так и сохранить новые данные о пользователе в БД.

public static function auth($id) {
  $_SESSION['user'] = $id;
}
/**
 * Выход пользователя из системы
 *
 * @return void
 */
public static function logout() {
  unset($_SESSION["user"]);
}
/**
 * Проверка того, что пользователь аутентифицирован в системе
 *
 * @return integer|false
 */
public static function isLogged() {
  // Если $_SESSION['user'] определено идентификатором пользователя, то возвращаем это значение
  // Иначе возвращаем false
// Эта строка эквивалентна следующему:
  // return isset($_SESSION['user']) ? $_SESSION['user'] : false;
  return $_SESSION['user'] ?? false;
}

Директория template

В этой директории хранятся шаблоны для представлений. Иными словами тут собрано все, что отвечает за внешний вид и дизайн проекта. В моем случае тут присутствует поддиректория css, содержащая файл style.css и поддиректория images, хранящая различные изображения. Файл style.css хранит css-код страниц и подключается в шапке представления.

Директория views

В данной директории хранятся файлы, отвечающие за представление данных. Это то, что видит конечный пользователь. Представление полностью отделено от остальных частей, поэтому можно безболезненно заменить один вид на другой. В папке views находится четыре подпапки: layouts, rating, site и user. В директории layouts хранятся макеты страниц: header и footer, подключаемые в начали и конце каждого представления соответственно. В директориях rating, site и user хранятся, соответственно, файлы, являющиеся представлением рейтинга, основных страниц сайта и данных пользователя. Файл header.php содержит «шапку» сайта: она одинаковая для всех страниц. Аналогично, файл footer.php содержит «подвал» сайта. «Подвал», как и «шапка», не отличается на разных страницах. Файл views/rating/index.php является представлением главной страницы рейтинга. Файл views/rating/edit.php является представлением страницы редактирования рейтинга. Файл views/site/index.php является представлением главной страницы сайта. Файл views/site/login.php является представлением страницы входа в систему. Файл views/user/edit.php является представлением страницы редактирования данных пользователя. Файл views/user/password.php является представлением страницы редактирования пароля пользователя.

Остальные файлы в корне проекта

Файл .gitignore используется системой контроля версий Git. В нем указаны те файлы или директории, изменения в которых отслеживать не нужно. В данном случае этот файл содержит одну лишь строку

config/db_params.php

Эта строка означает, что изменения в файле config/db_params.php не будут отслеживаться системой контроля версий. Этот файл игнорируется. Файл .htaccess указывает серверу Apache на то, куда запросы должны перенаправляться. В данном случае файл .htaccess указывает серверу, что все запросы должны перенаправляться на файл index.php, лежащий в корневом каталоге проекта и являющийся основной точкой входа в приложение.

AddDefaultCharset utf-8
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php

Как было сказано выше, основная точка входа в приложение это файл index.php. Все запросы к серверу перенаправляются на этот файл, обрабатываются в нем и далее, в зависимости от запроса, происходит то или иное действие.

<?php
/*
  Front Controller
  Изначально все запросы перенаправляются на этот файл
*/

// Вывод ошибок
// ini_set('display_errors', 1);
// error_reporting(E_ERROR | E_PARSE);

// Старт куки-сессии
session_start();

// Определение константы ROOT именем корневого каталога
define('ROOT', __DIR__);

// Автозагрузка заданной анонимной функции
spl_autoload_register(function ($class) {
  // Массив директорий в которых хранятся подключаемые классы
  $paths = array(
      '/models/',
      '/controllers/',
      '/components/',
  );
  // Проходим по массиву директорий
  foreach ($paths as $path) {
      // Формируем путь к файлу с классом
      $path = ROOT . $path . $class . '.php';
      // Если файл по такому пути существует, подключаем подключаем его
      if (is_file($path)) {
          include_once $path;
      }
  }
});

// Создаем объект класса Router
$router = new Router();
// Вызываем метод run()
$router->run();