Permalink
Find file
2af96c3 Nov 8, 2016
executable file 473 lines (284 sloc) 62.8 KB

Задача про список студентов

  • Требуется знать: PHP, основы ООП, основы баз данных, основы HTML/CSS, формы, таблицы
  • Уровень: начинающий
  • Время: 3-10 дней

Сделай сайт для регистрации абитуриентов. Он состоит из 2 страниц: список зарегистрированных абитуриентов (главная страница) и форма ввода/редактирования информации о себе. Любой абитуриент может зайти на сайт и добавить себя в список или отредактировать информацию о себе.

Форма содержит поля: имя, фамилия, пол, номер группы (от 2 до 5 цифр или букв), e-mail (должен быть уникален), суммарное число баллов на ЕГЭ (проверять на адекватность), год рождения, местный или иногородний. Данные надо сохранять в БД, все поля обязательны, все поля надо проверять (например нельзя ввести фамилию длиной 200 символов), при ошибке ввода отображать форму с сообщением об ошибке и выделенным красным цветом ошибочным полем, при успешном заполнении — спасибо, данные сохранены, вы можете их отредактировать или просмотреть список абитуриентов.

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

Список абитуриентов — выводит имя, фамилию, номер группы, число баллов. Выводятся по 50 человек на страницу, сортировка по любому полю делается кликом на заголовок колонки таблицы (по умолчанию по числу баллов вниз). Есть поле поиска, которое ищет сразу по всем строкам таблицы, регистронезависимо (то есть туда можно ввести номер группы либо часть имени/фамилии).

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

Вот примерный вид списка:

Список абитуриентов             Поиск: [___________][Найти]

Имя       Фамилия               Номер группы     Баллов [▲]
-----------------------------------------------------------
Иван      Иванов                1010Э            180
Петр      Петров                132М             220
Сидор     Сидоров               0012             250
...
-----------------------------------------------------------

Страницы: [1]  2   3   4   5 

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

Поиск абитуриентов               Поиск: [Иван_______][Найти]

Показаны только абитуриенты, найденные по запросу «Иван». 
[Показать всех абитуриентов]

......
  • HTML-шаблоны должны быть отделены от PHP кода ( http://www.phpinfo.su/articles/practice/shablony_v_php.html )
  • Надо использовать ООП.
  • Для работы с базой данных можно использовать PDO и паттерн TableDataGateway.
  • Желательно использовать autoloading для подключения классов.
  • Для оформления формы и таблицы можно использовать готовый CSS-фреймворк Twitter Bootstrap, но не тащи к нему кучу лишних файлов и плагинов.

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

Комментарии, подводные камни и советы

ООП

Мы используем ООП при решении этой задачи. Если ты его не знаешь, начни с его изучения.

Какие именно классы стоит сделать, написано ниже. В этой задаче, как и в большинстве приложений, используемые классы можно разделить на 2 вида:

  • класс, хранящий информацию о какой-то сущности, например Студент (с полями вроде «имя», «номер группы»). Объектов этого класса может быть много (список студентов можно представить как массив объектов Student).
  • класс-помощник (service, helper, manager) который делает какие-то действия над сущностями. У него могут вообще отсутствовать свойства. Такие классы как правило существуют в одном экземпляре (объекте) и создаются при запуске приложения. Например, в этой задаче ими будут класс, проверяющий правильно ли заполнена информация о студенте (валидатор) или класс, сохраняющий информацию о студенте в базу данных.

Если ты не очень хорошо знаком с исключениями, советую перечитать урок по ним.

PHP код

Не ставь в конце файлов тег ?> — за ним легко забыть пробел или перевод строки и это может сломать функции, отправляющие заголовки (header(), setcookie(), session_start()), так как PHP выведет этот пробел, а после вывода хотя бы одного символа отправлять заголовки нельзя.

Не используй короткий открывающий тег <? — он может быть отключен. Используй <?php и <?= для вывода в шаблоне.

Помни, что любые переданные пользователем параметры ($_COOKIE, $_GET, $_POST) могут отсутствовать или содержать что угодно (например, массив вместо строки). Этот код вызовет ошибку обращения к несуществующему индексу массива, если не передать элемент:

$x = $_POST['x'];

А вот этот код — не вызовет, а благодаря использованию strval (преобразует любые данные в строку) мы еще защищаемся от случая, когда нам передали массив вместо строки:

$x = array_key_exists('x', $_POST) ? strval($_POST['x']) : '';

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

Автозагрузка

К сожалению, есть много книг и статей, где упоминается устаревший способ автозагрузки классов через объявление функции __autoload(). Это неудачный подход, так как такая функция может быть только одна и сторонние библиотеки не могут добавить свою функцию-автозагрузчик. Потому надо использовать современный метод с использованием spl_autoload_register, которая не имеет такого ограничения. У меня есть урок на эту тему: автозагрузка и PSR-4. Использовать PSR-4 и неймспейсы не обязательно, но наверно будет удобнее с самого начала к ним привыкнуть. При желании для автозагрузки можно использовать и композер.

Шаблоны

Не смешивай в одном файле логику на PHP и вывод HTML-кода. Это ужасно:

echo "<div class=\"some-class\" style=\"padding-left: 20px;\"><span>...";

Такой код тяжело и читать, и редактировать. Весь HTML-код надо вынести в отдельный шаблон, как описано тут: http://www.phpinfo.su/articles/practice/shablony_v_php.html

Не забывай экранировать данные при выводе в шаблоне, иначе получишь уязвимость XSS. Прочитай урок про борьбу с XSS: security/xss.md.

Выводить переменные удобнее с помощью тега <?= который равносилен <?php echo: <?= html($name) ?>

Используй в шаблоне версии конструкций if/foreach c двоеточием, так как версии со скобками плохо читаются в гуще HTML-кода. Мануал: https://php.net/manual/ru/control-structures.alternative-syntax.php

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

Работа с формами

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

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

После успешной регистрации или обновления данных об абитуриенте нам надо показать сообщение об этом. Проще всего для этого при редиректе (после успешной обработки формы мы делаем редирект, не забыл?) приписать дополнительный параметр в URL, то есть редиректить на адрес вида index.php?notify=registered, а уже в index.php проверять значение параметра notify.

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

Не будь роботом

Важно уметь правильно формулировать сообщения, которые показываются пользователю. Они должны быть 1) точными 2) понятными пользователю, не знакомому с программированием 3) содержать варианты действия. Плохое сообщение: "Неверно заполнено поле surname" (или еще хуже: "Введите имя!"), хорошее сообщение: "В фамилии можно использовать только русские и латинские буквы, дефис, апостроф или пробел, а вы использовали символ '@'" или "Необходимо указать ваш адрес email, иначе мы не сможем связаться с вами". Визуально сообщение об ошибке можно выделить цветом (если ты используешь бутстрап, там есть готовые стили для этого), чтобы оно было хорошо заметно.

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

MVC

У меня есть урок про MVC: https://github.com/codedokode/pasta/blob/master/arch/mvc.md

Подход MVC заключается в том, что мы разбиваем приложение на 3 слабо связанных части: Модель (Model), Представление (View) и Контроллер (Controller).

Модель хранит и обрабатывает данные приложения, не взаимодействуя с внешним миром. Например, сохранение информации в БД, проверка правильности введенных данных — это задача Модели, но вывод информации — нет. Модель не должна обращаться к внешним переменным вроде $_GET/$_POST/$_SESSION/$_COOKIE и не должна ничего выводить. Все необходимые данные она получает через аргументы функций, и отдает результат через return.

В этой задаче Модель может состоять из таких классов:

  • класс, описывающий информацию об одном абитуриенте (модель абитуриента, не путай с Моделью как частью MVC)
  • класс, работающий с БД, реализующий например паттерн TableDataGateway. Все SQL запросы должны быть только в нем.
  • класс, выполняющий проверку данных (например что имя не длиннее разрешенного, содержит только разрешенные символы и тд)

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

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

Контроллер отвечает за взаимодействие с внешним миром (пользователем) и управление всем процессом. Обычно контроллер разбирает параметры запроса из $_POST/$_GET, обращается к модели, чтобы получить какие-то данные или сделать какое-то действие, и в конце вызывает Представление, чтобы отобразить результат. Число контроллеров определяется числом разделов и страниц сайта.

Здесь контроллерами могут быть скрипты, которые отвечают за вывод списка и обработку формы редактирования/регистрации (классы для контроллеров делать не требуется).

Если ты будешь искать в интернете информацию о MVC, учти что этот подход изначально придуман в 80-е годы для десктопных приложений (с окошечками и кнопочками), а не веб-приложений и «MVC для десктопа» чуть-чуть отличается от «MVC для веба», так как десктопные приложения в отличие от PHP-скрипта, не завершаются после вывода информации на экран, а продолжают работать. Но общие принципы те же.

Если следовать MVC, разделяя код на части, то он будет проще и надежнее.

Заблуждения, связанные с MVC

"Разделение на 3 части" не значит, что у тебя должно быть 3 папки с названиями Controller, Model, View - этого не требуется. Вполне можно все папки складывать на одном уровне, например, Controller, Helper, Database, и так далее. Для View вообще может не быть классов, часто представление состоит лишь из шаблонов страниц.

Ошибочно думать, что Модель - это 1 класс. Модель - это часть приложения, она может состоять из многих классов (хелперы, дата мапперы, сущности, валидаторы) или вообще не содержать ни одного класса (если мы пишем не в ООП, а в процедурном стиле на функциях).

Некоторые думают, что в MVC всегда есть N Контроллеров и ровно N соответствующих им Моделей. Это тоже неверно. Число Контролеров определяется числом разделов или страниц сайта. Число классов в Модели может быть разным, но как правило, оно пропорционально числу таблиц в БД, для каждой таблицы может быть класс-сущность, маппер, валидатор.

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

Структура URL и роутинг

Роутинг - это определение того, какой контроллер мы должны вызвать. Мы можем положиться в этом на веб-сервер (сделать разные входные скрипты для разных страниц), а можем назначить единственный скрипт (public/index.php) обработчиком для всех запросов и далее средствами PHP разбирать URL. Вот какие есть варианты:

  • делать роутинг средствами сервера. Для каждой страницы в папке public создается отдельный скрипт (index.php, register.php) и из него либо вызывается соответствующий контроллер, либо прямо там же и пишется код контроллера
  • сделать один входной скрипт, и указывать тип страницы параметром, вроде /index.php?page=register. Это простой в реализации вариант, но URL получаются некрасивые, а поисковые роботы могут подумать, что на сайте всего одна страница
  • использовать PATH_INFO. Веб-серверы позволяют после имени скрипта дописать слеш и произвольный путь, и этот путь в случае PHP помещается в $_SERVER['PATH_INFO']. URL при этом выглядит так: /index.php/register. Минус в том, что адреса всех страниц начинаются с index.php.
  • назначить (например с помощью .htaccess) скрипт index.php обработчиком для всех запросов (кроме тех что соответствуют статическим файлам) и далее уже средствами PHP разбирать URL (он хранится в $_SERVER['REQUEST_URI']). При этом мы можем делать для страниц любые URL, например /register. Но под каждый веб-сервер (Апач, нгинкс) нужен свой вариант конфига для того, чтобы назначить index.php обработчиком для любых запросов.

В случае, когда все запросы проходят через один скрипт или класс, его называют Front Controller. Его задача - проанализировать URL и вызвать контроллер нужной страницы.

База данных

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

Номер группы (а также номера телефонов, домов, паспортов) в базе данных надо делать строкой, а не числом. Число используется для обозначения количества чего-то или значения какой-то величины. Вот что бывает если считать телефон числом: http://habrahabr.ru/post/113435/

NULL это специальное значение, которое значит «не указано» или «неизвестно». При проектировании таблицы не забудь проставить для колонок, можно ли в них вставлять это значение или нет, DEFAULT NULL или NOT NULL. Обычно если у колонки есть значение по умолчанию или для нее разрешен NULL, она считается необязательной к заполнению, а если значения по умолчанию нет и NULL запрещен, то обязательной.

Ты можешь добавлять к таблице и отдельным колонкам комментарии: http://stackoverflow.com/a/200033 Эти комментарии сохраняются в базе и выводятся в программах для работы с ней. Добавляй комментарии там, где назначение колонки не очень очевидно.

Для колонок с несколькими вариантами значений, вроде «пол», надо использовать тип ENUM. Перечитай список типов данных в MySQL, если ты о нем не знал: http://phpclub.ru/mysql/doc/column-types.html . Для значений энумов (male/female) в коде стоит завести константы (вроде Abiturent::GENDER_MALE).

Да, для хранения года тоже предусмотрен специальный тип.

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

Работа с базой данных из PHP

В этой задаче необходимо хранить данные в базе. Потому, убедись что ты имеешь хотя бы базовые знания языка SQL. Из PHP с базой данных можно работать через 2 расширения: PDO (статья на Хабре, мануал) и mysqli (хабр, мануал). Расширение mysql устарело. Если ты видишь учебник, где используются устаревшие функции вроде mysql_query() — выбрасывай это старье.

У PDO и mysqli есть сильные и слабые стороны, но у PDO есть одно преимущество — он умеет выбрасывать исключения при любых ошибках. А mysqli — нет, и после любого действия ты должен проверять, не произошла ли ошибка, с помощью if (иначе ты о ней не узнаешь и будешь долго искать почему программа не работает). К сожалению, режим выброса исключений в PDO отключен по умолчанию, но включить его можно одной строчкой сразу после соединения с БД:

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Мануал: http://php.net/manual/ru/pdo.error-handling.php

При соединении с базой не забудь задать кодировку соединения (в какой кодировке ты отправляешь и получаешь данные). Это удобно сделать с помощью параметра charset: http://php.net/manual/ru/ref.pdo-mysql.connection.php (не забудь что в MySQL utf-8 пишется без дефиса: utf8).

В MySQLi для задания кодировки есть отдельный метод: http://php.net/manual/ru/mysqli.set-charset.php

Также кодировку можно задать запросом SET NAMES.

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

$stmt = $pdo->query("SELECT * FROM table WHERE x = $x"); // Хорошие дети, не делайте так

Это открывает путь к уязвимости под названием SQL-инъекция: мой урок по SQL инъекциям, wiki: внедрение SQL кода, подробная статья на rdot.

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

// Создаем подготовленный запрос
$stmt = $pdo->prepare("SELECT * FROM table WHERE x = :x AND y = :y");

// Привязываем значения переменных
$stmt->bindValue(':x', $x);
$stmt->bindValue(':y', $y);

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

В некоторых статьях вместо bindValue используют bindParam, но он менее удобен, так как в него нельзя передать число или выражение (только одну переменную) и он вообще предназначен для двухсторонней привязки, что в 99% случаев не требуется.

Есть определенные паттерны проектирования для сохранения и загрузки объектов из БД. В этом задании удобно сделать это с помощью паттерна TableDataGateway, который описан в моем уроке: db/patterns-oop.md. SQL-запросы не должны быть размазаны по всему коду, а собраны в одном классе.

Когда ты загружаешь данные из базы, тебе надо как-то создать соответствующий им список объектов (заполнение свойств объектов данными из БД называется hydration). В простых ситуациях можно использовать встроенную в PDO возможность создавать объекты с помощью PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE (пример есть тут: http://php-zametki.ru/php-prodvinutym/57-pdo-konstanty-vyborki-dannyx.html ). PDO в этом режиме создает объект указанного класса и копирует значения из базы в публичные свойства.

Флаг PDO::FETCH_PROPS_LATE нужен, чтобы исправить странность PHP, когда он выставляет свойства до вызова конструктора. Вертикальная черта | — это битовый оператор, который используется для объединения флагов (чтобы понять, как это работает, надо знать двоичные числа).

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

Чтобы получить id только что вставленной в базу записи, есть специальная надежная функция lastInsertId. Не изобретай велосипедов.

Если тебе надо посчитать число строк в таблице, не вздумай выбирать все записи и пересылать в PHP. Используй функцию SELECT COUNT(*). Аналогично, если тебе надо проверить нет ли такого email в базе, не надо ничего выбирать — достаточно посчитать число записей, где он встречается.

Если тебе надо выбрать не все записи, а только часть (например, первые 10), используй конструкцию LIMIT X OFFSET Y.

Проверка ошибок в Mysqli

Если ты используешь mysqli, то практически после каждой функции mysqli должен стоять if, проверяющий, не произошла ли ошибка, и выбрасывающий исключение, если это так (если ты используешь PDO, то делать это не требуется, он сам это делает).

По умолчанию, если при работе с БД происходит ошибка, mysqli молчит как партизан и не выводит никаких предупреждений (а только возвращает false что мало помогает в поиске причины). Ты не узнаешь об ошибке и удивляешься почему ничего не работает.

Посмотри код в примерах: http://php.net/manual/ru/mysqli.quickstart.statements.php

Видишь, после каждого вызова функций mysqli стоит if с выводом сообщения об ошибке? То же самое надо сделать и тебе. Только на реальном сайте ошибку лучше не показывать пользователю, а писать в логи или например выкидывать исключение.

Вот неплохая статья про mysqli и там тоже говорится про обработку ошибок: http://habrahabr.ru/post/141127/

Строгий режим в MySQL

Строгий режим — это режим, при котором MySQL более тщательно проверяет твои запросы и вместо предупреждений (которые ты не увидишь) в некоторых случаях выдает ошибки (из-за которых программа останавливается). Ну к примеру, если у тебя есть колонка типа varchar(200) и ты попытаешься вставить в нее строку из 300 символов, в нестрогом режиме MySQL молча отрежет лишнее (и в базе окажется обрезанная строка), а в строгом выдаст ошибку.

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

Я советую включать этот режим, сделав при соединении с БД запрос SET sql_mode='STRICT_ALL_TABLES'. Если ты используешь PDO то стоит указать этот запрос с помощью опции PDO::MYSQL_ATTR_INIT_COMMAND при создании объекта PDO (мануал: http://php.net/manual/ru/ref.pdo-mysql.php ).

Поиск в базе данных

Для поиска в одной колонке по части строки в SQL есть оператор LIKE: WHERE x LIKE '%hello%' (% здесь соответствует любым символам). Для поиска по всем колонкам можно применить оператор LIKE к соединенным через пробел значениям столбцов. Этот способ конечно неэффективен на больших таблицах, но у нас маленькое приложение и незачем что-то усложнять (на больших таблицах используют внешний поисковый движок вроде sphinx).

Другой вариант — искать в нескольких колонках через OR, например name LIKE '%hello%' OR surname LIKE '%hello%', но такой способ не сработает при поиске и по имени, и по фамилии одновременно по фразе вроде «Иван Иванов».

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

Валидация

Валидация — это проверка введенных данных на правильность. Ты можешь захотеть поместить функцию валидации в класс, представляющий абитуриента. Это не всегда хорошая идея, так как он, например, не может обратиться к базе данных (взаимодействием с базой данных занимается TableDataGateway). Может быть тогда стоит поместить код валидации в TDGateway? Это тоже не очень правильно, так как задача TDGateway - работать с базой данных, а не проверять данные на правильность.

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

Метод валидации логично сделать так, чтобы он принимал на вход объект-абитуриента и возвращал список ошибок в массиве или объекте-коллекции.

Хочешь проверять адрес email с помощью регулярных выражений? Прочти статьи: http://habrahabr.ru/post/55820/, http://habrahabr.ru/post/175375/

Делай понятные сообщения об ошибках, чтобы пользователь не гадал, что именно он перепутал. Не «неверный формат данных», а «имя не должно быть длиннее 90 символов» (а лучше так: «имя не должно быть длиннее 90 символов, а вы ввели 103») или «в номере группы можно использовать только буквы и цифры» (а еще лучше так: «в номере группы можно использовать только буквы и цифры, а символ '!' нельзя»).

Когда ты будешь проверять имена и фамилии, помни что в России фамилия может содержать дефис, апостроф и состоять из нескольких слов: О'Генри, Сан Антуан Кристоф, Римский-Корсаков. Существуют фамилии из одной буквы, например китайская «Ю».

Для всех вводимых в форму человеком данных надо делать trim() так как если случайно ввести пробел, твой скрипт выведет ошибку, а глазами этот пробел не увидеть.

Клиентская валидация

HTML5 позволил нам указывать в HTML дополнительные условия для вводимых данных, которые проверит при отправке формы браузер. Используй эти возможности, но помни, что ты по-прежнему должен проверять данные на сервере: есть старые браузеры, есть злонамеренные пользователи, которые могут слать любые данные.

Вот что можно использовать:

  • в HTML5 есть специальный тип email для поля ввода адреса почты, search для поля поиска и number для указания чисел
  • есть специальный атрибут required для указания обязательных полей
  • атрибут pattern позволяет указать регулярное выражение, которому должны соответствовать значения. Учти, что тут используется немного другой диалект регулярных выражений (используются регулярки из яваскрипта), в котором не ставятся ограничители, флаги, а бекслеш пишется один раз (например: \d). Писать ^ и $ для привязки выражения к краям не требуется.
  • при использовании атрибута pattern не забудь добавить атрибут title, который содержит подсказку, какие значения можно вводить. Эту подсказку будет выводить браузер вместе с сообщением об ошибке, иначе как пользователь догадается, что он ввел неправильно?
  • атрибут autofocus позволяет при открытии страницы поставить курсор в нужное поле
  • для полей типа number можно указать минимальное и максимальное значение

Ссылки:

Внимательный читатель заметит что использование клиентской валидации приводит к тому, что правила проверки хранятся в двух далеких друг от друга местах: в HTML коде и классе-валидаторе, и легко поменять что-то в одном месте и забыть в другом. Ты можешь попробовать бороться с этим, перенеся регулярные выражения из HTML в класс (например сделав там методы для их получения).

Составление URL

В задаче требуется обеспечить возможность сортировки и постраничного просмотра результатов поиска. Для этого тебе надо передавать все необходимые параметры в URL, например list.php?search=cat&sort=name&page=2 (как видишь URL содержит фразу для поиска, номер страницы, направление и колонку для сортировки). Помни, что спецсимволы в параметрах экранируются с помощью процентного кодирования. Это неправильный пример подстановки параметров в ссылку:

// Если в переменных есть символы & или #, ссылка сломается
$link = "search.php?q=$query&x=$x"; // Хорошие дети, не делайте так

В PHP кодирование спецсимволов для URL делает функция urlencode(). Это правильный пример:

$link = "search.php?q=" . urlencode($query) . "&x=" . urlencode($x);

Но удобнее собирать такие ссылки не вручную, а с помощью стандартной функции http_build_query. Она собирает строку с параметрами из массива и сама вызывает urlencode:

$link = "search.php?" . http_build_query([
    'q'     =>  $query,
    'x'     =>  $x
]);

Если ты затем выводишь эту ссылку в HTML-коде, разумеется, надо дополнительно экранировать ее с помощью htmlspecialchars().

Я также советую выносить генерацию ссылок из шаблона в отдельные функции или методы класса, чтобы было примерно так:

<a href="<?= htmlspecialchars(getSortingLink($search, $dir, $column), ENT_QUOTES) ?>">

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

Постраничная навигация

В задаче надо выводить студентов (а также результаты поиска) постранично. Для этого стоит использовать SQL-конструкцию LIMIT X, Y, которая позволяет выбрать не все, а только Y результатов начиная с X. Чтобы не запутаться что значат X и Y, эту конструкцию удобно писать как LIMIT Y OFFSET X. Обрати внимание, что LIMIT поддерживается только в MySQL, в других базах данных надо использовать другие конструкции для выборки части результатов.

Также, тебе понадобится посчитать общее число студентов (или результатов поиска) в базе, чтобы узнать число страниц, для этого можно использовать конструкцию SELECT COUNT(*) FROM ....

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

$pager = new Pager($totalPages, $recordsPerPage, 'index.php?page={page}');
echo $pager->getTotalPages();   // считает общее число страниц
echo $pager->getLinkForPage(2); // index.php?page=2
echo $pager->getLinkForLastPage();

Некоторые пытаются возложить на объект Pager лишние функции, например чтение параметров поиска из $_GET или подсчет числа записей в базе. Я не советую так делать, так как это явно должно быть в другом месте (например, работа с базой — в маппере).

Также, можно снять с класса Pager задачу генерации ссылок (оставить только расчет номеров страниц) и генерировать их где-то в другом месте.

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

Авторизация

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

При регистрации можно генерировать какой-то случайный код, сохраняя его и в базу и в куки пользователю на несколько лет. При попытке редактирования мы можем по коду из кук проверить, имеет ли пользователь право на редактирование данных и если да, то чьих. Код должен быть достаточно сложным, чтобы злоумышленник не мог его подобрать, например 32 символа из диапазона [a-zA-Z0-9] дадут 6232 ~ 2×1057 комбинаций, что довольно много.

Такая схема с куками имеет и недостатки: если пользователь возьмет другой браузер или почистит куки, он потеряет доступ к редактированию. Но такая схема вполне подходит для нашего случая.

Рекомендуемая архитектура

Вот, какие классы нам понадобятся:

  • класс для хранения информации об одном абитуриенте (имя, группа, год рождения и т.д.). Такие классы обычно называют моделью абитуриента. В него же можно поместить константы для значений с выбором (пол и проживание).
  • класс для сохранения/загрузки/поиска абитуриентов в базе данных. Если ты используешь паттерн TableDataGateway, то класс можно назвать AbiturientDataGateway или как-то так. Таким образом, вся работа с БД у нас собрана в одном классе. Этому классу через конструктор передается объект PDO.
  • класс для валидации (проверки введенных в форму данных), например AbiturientValidator. Этому классу наверно придется использовать Gateway для того, чтобы проверить например, не используется ли уже где-то введенный e-mail. Если так, то можно передавать объект-маппер ему в конструктор (это называется внедрение зависимостей, dependency injection, «зависимостью» тут называют объект который нужен другому объекту для работы).

Также, у нас могут быть какие-то общие функции, которые трудно отнести в какой-то конкретный класс, например функции для формирования ссылки для сортировки таблицы. Такие функции можно сделать статическими методами в классе-помощнике вроде ViewHelper или LinkHelper. Так как все методы в нем статические, создавать объект этого класса не требуется.

Структура файлов

В этом задании у нас фактически 2 страницы: страница со списком студентов, она же главная, она же страница поиска и вторая страница, с формой регистрации и редактирования своих данных. Логично для них сделать 2 входных (то есть те, которые мы будем запускать из браузера) скрипта, например index.php и register.php.

Затем, нам нужен каталог с файлами классов. Его можно назвать src или app.

У нас будут какие-то общие действия, которые надо сделать в начале каждого скрипта (например, создание объекта PDO, создание маппера). Их стоит вынести в файл init.php или bootstrap.php, который можно положить в ту же папку src.

В приложении нужно как-то задавать настройки, например настройки соединения с БД. Их надо выносить отдельно, чтобы их можно было поменять, не залезая в код. Есть такие форматы для хранения файлов настроек: ini-файлы - один из самых простых форматов, json который использует синтаксис языка Яваскрипт, и более сложные форматы, вроде YAML или XML. Также, некоторые делают конфиг в виде PHP файла с значениями переменных - но мне кажется, настройки лучше делать не в виде кода, чтобы их мог править даже не знающий языка PHP пользователь.

Файл с конфигом стоит положить в корневой каталог или в src. Пример ini-файла:

[db]
user=students
database=students
password=123456

Пример JSON-файла:

{
    "db": 
    {
        "user": "students",
        "password": "123456"
    }
}

Примеры php-файла:

$dbUser = 'user';
$dbPass = '123456';
$config['user'] = 'user';
$config['pass'] = '123456';

Также, нам понадобится каталог с шаблонами (templates или views).

Еще нам нужен каталог с CSS-файлами (например, фреймворком Twitter bootstrap) и картинками, если они используются. Его можно назвать static или public. Не меняй файлы Twitter Bootstrap и не перемешивай их со своими, а положи в отдельную папку как есть. Если ты используешь сторонние JS или CSS библиотеки, не перемешивай их со своими файлами, а храни в отдельной папке. Так их проще обновлять и лучше видно где чей код.

Наконец, не забудь добавить в проект его краткое описание в файле README.md (он использует формат разметки markdown). Вот пример хорошего описания: https://github.com/foobar1643/filehosting/blob/master/README.md

Выносим код за корень сервера

Корневая папка веб-сервера - это папка, из которой веб-сервер раздает файлы. Ну к примеру если корневая папка - /var/www/example.com/, то при обращении к файлу http://example.com/a/1.txt сервер будет искать его в /var/www/example.com/a/1.txt. Если весь твой код лежит в корне сервера, то к файлам можно обратиться напрямую (потому эта папка еще называется публичной). Если ты где-то положишь файл c паролем password.txt или php скрипт очистки базы - злоумышленник может через браузер прочитать пароль (открыв http://example.com/password.txt) или запустить скрипт очистки.

Для повышения безопасности делают так: создают в проекте отдельную папку, например public, и настраивают веб-сервер так чтобы корень сайта был в ней (например в /var/www/example.com/public/). А основной код и большинство файлов - за пределами этой папки (например в /var/www/example.com/src/File.php). В таком случае они будут недоступны снаружи.

В папку public мы кладем CSS, JS файлы, и входные скрипты вроде index.php, register.php — то есть те файлы, к которым можно обращаться напрямую. А весь остальной код размещается за пределами этой папки. Если ты используешь веб-сервер Апач, то корневая папка сайта указывается в конфиге внутри блока VirtualHost в директиве DocumentRoot:

<VirtualHost *:80>
# Имя сервера которое обслуживает этот VirtualHost
ServerName example.com
# Корневая папка сервера
DocumentRoot /var/www/example.com/public
# ....

Под windows путь будет еще содержать букву диска, например d:/www/example.com/public. Не забудь перезапустить Апач после правки конфига! Немного о настройке виртуальных хостов под линукс: https://www.digitalocean.com/community/tutorials/apache-ubuntu-14-04-lts-ru

Напоминания

Копипаста в коде недопустима. Если у тебя у двух HTML страниц общая шапка — вынеси это в отдельный файл.

Имена классов пишутся с большой буквы. Каждый класс должен быть в отдельном файле, и ничего другого в этом файле не должно быть. Имя файла должно соответствовать имени класса с точностью до регистра букв (StudentStudent.php).

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

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

Примеры работ

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

(чтобы попасть в этот список, добавь в описание своего репозитория слова "student list" и "registration")