Skip to content

Latest commit

 

History

History
executable file
·
691 lines (431 loc) · 87.7 KB

student-list.md

File metadata and controls

executable file
·
691 lines (431 loc) · 87.7 KB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

......
  • HTML-шаблоны должны быть отделены от PHP кода (урок про шаблоны)
  • Надо использовать ООП.
  • Для работы с базой данных можно использовать паттерн TableDataGateway.
  • Желательно использовать автозагрузку для подключения классов.
  • Для оформления формы и таблицы можно использовать готовый 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-код надо вынести в отдельный шаблон, как описано в уроке про шаблонизацию.

Не забывай экранировать данные при выводе в шаблоне, иначе получишь уязвимость 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 и вызвать контроллер нужной страницы.

База данных

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

Я рекомендую использовать для задачи свободную СУБД, вот самые популярные, в случайном порядке:

  • PostgreSQL
  • MySQL (и ее форки MariaDB, Percona)
  • SQLite (это встраиваемая СУБД, она используется как расширение к PHP и не требует установки отдельной программы-сервера СУБД и хранит данные в файлах)

Об их различиях написано множество статей в Интернете. Погугли.

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

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

Ты можешь добавлять к таблице и отдельным колонкам комментарии: /db/comments.md. Эти комментарии сохраняются в базе и выводятся в программах для работы с ней. Добавляй комментарии там, где назначение колонки не очень очевидно, они помогут другим людям разобраться. Например, если ты хранишь в базе какой-то токен, то опиши его в комментарии.

Для колонок с несколькими вариантами значений, вроде «пол», желательно использовать тип, который не позволяет вставлять значения не из списка:

Для значений пола (male/female) в коде стоит завести константы (вроде Abiturent::GENDER_MALE).

Для хранения года в PostreSQL и SQLite можно использовать INTEGER, в MySQL есть специальный тип YEAR.

Не стоит использовать для хранения коротких строк (вроде фамилии) гигантские поля вроде TEXT. Для строк нужно указывать их максимальную длину, например, VARCHAR(50).

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

Ограничения в базе данных

При создании таблицы мы можем указать ограничения (constraint) для значений отдельных ячеек или их сочетаний. Эти ограничения не позволят вставить некорректные данные в базу. К ограничениям относятся NOT NULL, UNIQUE, FOREIGN KEY и CHECK. Первые три можно нагуглить, а последнее разберем подробнее.

Ограничение CHECK позволяет указать условие, которому должны соответствовать все вставляемые в строку таблицы данные. Оно может ограничивать значение одной или нескольких колонок. Его можно указать при создании таблицы или добавить к таблице запросом ALTER TABLE. Вот, так, например, можно указать, что поле percent может принимать значения от 1 до 20, поле name должно иметь не менее 3 символов, а сумма percent и commission не превышает 25:

CREATE TABLE credits (
    -- имя заемщика
    name VARCHAR(100) NOT NULL CHECK (LENGTH(name) >= 3),
    -- процент по кредиту
    percent INT(2) NOT NULL CHECK (percent BETWEEN 1 AND 20),
    -- комиссия за выдачу кредита
    commission INT(2) NOT NULL,
    CHECK (percent + commission <= 25)
)

MySQL поддерживает ограничение CHECK, начиная с версии 8.0.16, MariaDB - с версии 10.2.1.

PostgreSQL поддерживает эту возможность с давних времен: https://postgrespro.ru/docs/postgrespro/10/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS

Возможность указать ограничение CHECK есть и в SQLite: https://www.sqlite.org/lang_createtable.html

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

В этой задаче необходимо хранить данные в базе. Потому, убедись что ты имеешь хотя бы базовые знания языка SQL. Из PHP с базой данных можно работать через такие расширения:

  • с SQLite - через расширение SQLite3 или PDO.
  • с MySQL - через одно из двух расширений: MySQLi (хабр, мануал) или PDO. Расширение mysql (без i на конце) устарело. Если ты видишь учебник, где используются устаревшие функции вроде mysql_query() — выбрасывай это старье.
  • с PostgreSQL - через расширение PostgreSQL или PDO.

PDO - это универсальное расширение, которое поддерживает несколько СУБД: (статья на Хабре, мануал).

У нативного драйвера для СУБД (SQLite/PostgreSQL/MySQLi) и PDO есть сильные и слабые стороны, если интересно, то можно погуглить или поискать их в мануале, например: http://php.net/manual/ru/mysqli.overview.php . При использовании любой из библиотек, необходимо включить режим выброса исключений при ошибках. Если это не сделать, то либо информация об ошибках не будет нигде выводиться, либо придется после вызова каждой функции ставить if с проверкой результата. К сожалению, по умолчанию в обоих библиотеках режим выброса исключений выключен.

В PDO включить его можно одной строчкой сразу после соединения с БД (также, можно передать эти параметры в конструктор). Параметры задаются для отдельного соединения:

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

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

В MySQLi режим выброса исключений включается такой строчкой, до соединения с БД. Он применяется глобально ко всем mysqli-соединениям в скрипте:

mysqli_report(MYSQLI_REPORT_STRICT | MYSQLI_REPORT_ERROR);

Мануал: http://php.net/manual/ru/mysqli-driver.report-mode.php

В SQLite3 режим включается вызовом метода enableExceptions после создания объекта («соединения» там нет, так как нет сервера):

$sqlite->enableExceptions(true);

Мануал: http://php.net/manual/ru/sqlite3.enableexceptions.php

В расширении PostgreSQL включить режим выброса исключеий нельзя, и, значит, надо писать if с проверкой результата после вызова каждой pg_* функции. Или, что проще, использовать PDO.

В некоторых статьях код, работающий с БД, завернут в блок try { ... } catch (\Exception $e) { echo ... ; }. Так делать ни в коем случае не надо, подробности в уроке про исключения.

При соединении с базой не забудь задать кодировку соединения (в какой кодировке ты отправляешь и получаешь данные). Это удобно сделать так:

  • в MySQLi с помощью метода $mysqli->set_charset(...): http://php.net/manual/ru/mysqli.set-charset.php
  • в PDO с помощью параметра charset в строке DSN: http://php.net/manual/ru/ref.pdo-mysql.connection.php
  • в PostgreSQL с помощью функции pg_set_client_encoding(): http://php.net/manual/ru/function.pg-set-client-encoding.php
  • SQLite поддерживает хранение данных в UTF-8 или UTF-16, это задается при создании БД и не меняется. Далее, скорее всего, она принимает и отдает данные в той кодировке, в которой была создана БД, но стоит это проверить путем вставки в базу значений с разными юникодными символами и чтением их обратно.

Не забудь, что в MySQL для полноценной поддержки utf-8 нужно указать название utf8mb4. Кодировка utf8 в MySQL обозначает урезанную версию Юникода, содержащую только Basic Multilingual Plane и не поддерживает, например, сохранение символов эмодзи.

Также в MySQL кодировку можно задать SQL-запросом SET NAMES utf8mb4. Разумеется, при создании новой базы данных (SQL-запрос CREATE DATABASE) ей также рекомендуется задать кодировку utf8mb4, чтобы создаваемые в ней таблицы поддерживали хранение любых символов Юникода.

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

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

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

Чтобы уязвимости не было, вставлять все данные в запрос надо через плейсхолдеры (метка, например, знак вопроса, указывающая место для подстановки значений). Вот пример кода для MySQLi:

// http://php.net/manual/ru/mysqli.quickstart.prepared-statements.php
// Создаем подготовленный запрос с плейсхолдерами вместо реальных значений
$stmt = $mysqli->prepare("SELECT * FROM table WHERE x = ? AND y = ?");

// Задаем значения для подстановки на место плейсхолдеров (знаков вопроса)
$stmt->bind_param('ii', $x, $y);

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

Вот пример кода для PDO (работает с любой из СУБД):

// http://php.net/manual/ru/pdo.prepared-statements.php
// Создаем подготовленный запрос с плейсхолдерами вместо реальных значений
$stmt = $pdo->prepare("SELECT * FROM table WHERE x = :x AND y = :y");

// Задаем значения для подстановки на место плейсхолдеров (:x и :y)
$stmt->bindValue(':x', $x);
$stmt->bindValue(':y', $y);

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

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

Пример для SQLite3:

// http://php.net/manual/ru/sqlite3.prepare.php
// Создаем подготовленный запрос с плейсхолдерами вместо реальных значений
$stmt = $db->prepare("SELECT * FROM table WHERE x = :x AND y = :y");

// Задаем значения для подстановки на место плейсхолдеров (:x и :y)
$stmt->bindValue(':x', $x, SQLITE3_INTEGER);
$stmt->bindValue(':y', $y, SQLITE3_INTEGER);

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

Пример для расширения PostgreSQL:

// http://php.net/manual/ru/function.pg-prepare.php
// Создаем подготовленный запрос с плейсхолдерами вместо реальных значений
$result = pg_prepare($conn, "", 'SELECT * FROM table WHERE x = $1 AND y = $2');
if (!$result) {
    throw new ...;
}

// Выполяем запрос с подстановкой значений
$result = pg_execute($conn, "", [$x, $y]);
if (!$result) {
    throw new ...;
}

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

Есть определенные паттерны проектирования для сохранения и загрузки объектов из БД. В этом задании удобно сделать это с помощью паттерна 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, когда он выставляет свойства до вызова конструктора. Вертикальная черта | — это битовый оператор, который используется для объединения флагов (чтобы понять, как это работает, надо знать двоичные числа).

  • для MySQLi такую же возможность предоставляет метод $result->fetch_object(...): http://php.net/manual/ru/mysqli-result.fetch-object.php

  • в SQLite3 такой возможности нет (можно использовать PDO)

  • в расширении PostgreSQL есть функция pg_fetch_object(...): http://php.net/manual/ru/function.pg-fetch-object.php

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

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

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

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

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

При выполнении почти любой функции, взаимодействующей с БД, может произойти ошибка. Например: не удалось соединиться с сервером СУБД, SQL-запрос написан некорректно. Многие из функций по умолчанию в такой ситуации молчат как партизаны, ничего не выводят ни в лог ошибок, ни на экран, и лишь сообщают о проблеме возвратом значения false. В такой ситуации после каждого вызова функции необходимо писать if с проверкой наличия ошибки.

Решить эту проблему можно, включив в используемом расширении режим выброса исключений при ошибке, как описано выше.

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

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

Строгий режим в 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 ).

У PostgreSQL и SQLite «нестрогого» режима нет, и для них делать ничего не требуется. В новейших версиях MySQL (8 и выше) строгий режим включен по умолчанию.

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

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

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

Для выделения найденного слова в таблице при выводе можно написать простую функцию, которая получает на вход значение в столбце и окружает найденное слово HTML-тегами вроде <mark>. Не забудь про экранирование символов и 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, PostgreSQL и SQLite такая конструкция есть.

Также, тебе понадобится посчитать общее число студентов (или результатов поиска) в базе, чтобы узнать число страниц, для этого можно использовать конструкцию 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 — то есть те файлы, к которым можно обращаться напрямую. А весь остальной код размещается за пределами этой папки.

Как именно задать корневую папку - зависит от используемого веб-сервера.

Встроенный в PHP сервер: в нем корневая папка задается опцией командной строки -t папка, а если она не указана, то берется текущая папка. Мануал: http://php.net/manual/ru/features.commandline.webserver.php

Апач: корневая папка сайта указывается в директиве 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 (для других дистрибутивов или других ОС пути к файлам могут отличаться, и может не потребоваться настройка прав доступа, но принцип тот же).

Nginx: корневая папка задается директивой root, например:

location / {
    root /var/www/example.com/public;
}

Другие подробности можно искать в мануале, начиная например отсюда: https://nginx.ru/ru/docs/beginners_guide.html#fastcgi

Напоминания

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

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

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

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

Заполнение базы

Для теста ты можешь захотеть заполнить базу нужным числом сгенерированных пользователей. Рекомендую использовать библиотеку https://github.com/fzaninotto/Faker для этой цели.

Публикация проекта

Проект стоит разрабатывать с использованием системы контроля версий (VCS), например, Git или Mercurial. Система контроля версий позволяет создать хранилище файлов - репозиторий - и сохранять в него (коммитить) код после внесения изменений. Это позволяет отменять неудачные правки, видеть историю разработки проекта, сравнивать старые версии файла с текущей.

По системе Git есть хороший учебник на русском: https://git-scm.com/book/ru/v1/ (изучать все не требуется, достаточно освоить основные команды: init, clone, config, status, add, commit, log, remote, fetch, pull, push).

Для Mercurial тоже есть учебник на русском: https://hgbook.bacher09.org/html-single/

Учебники описывают работу через командную строку. Кроме утилит командной строки, работать с репозиторием можно через утилиты с графическим интерфейсом, также поддержка VCS встроена в популярные IDE вроде Eclipse, Netbeans или PhpStorm. Однако, изучать VCS проще всего именно в командной строке (так как там не требуется разбираться в сложном интерфейсе с множеством кнопок и опций), а, уже после, если хочется, можно попробовать использовать GUI программы.

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

  • GitHub - позиционирует себя как соцсеть для разработчиков. Самый популярный хостинг на 2018 год. Пример, как выглядит проект на Github: https://github.com/codedokode/task-checker (пример большого проекта: https://github.com/slimphp/Slim )
  • Bitbucket - пример проекта: https://bitbucket.org/thomasjackson/php-file-uploader
  • GitLab - пример проекта: https://gitlab.com/fdroid/fdroidserver . GitLab позволяет поднять личный хостинг репозиториев на своем сервере.
  • Launchpad - создан Canonical, компанией, которая разрабатывает Ubuntu. Бесплатен для проектов со свободной лицензией. Пример проекта: https://launchpad.net/inkscape
  • при большом желании репозиторий можно разместить на своем сервере, добавив к нему приложение для просмотра кода через браузер, например: Gogs, Gitlist, cgit, gitphp. Эту возможность чаще используют компании, чтобы создать недоступный извне хостинг репозиториев для своих проектов.

Использование этих сервисов также позволяет работать с кодом с разных устройств - например, с компьютера и ноутбука. Команды git push и git pull позволяют выгружать изменения на сервер и загружать их с сервера. Ну и конечно, они позволяют нескольким разработчикам совместно развивать проект.

Если проект использует базу данных, необходимо добавить в проект дамп таблиц и данных этой БД. Для MySQL дамп генерируется в формате SQL с помощью утилиты mysqdump. Также, код можно напистаь руками. Не стоит добавлять в дамп команды создания базы данных и пользователей (CREATE DATABASE, CREATE USER), так как их названия могут отличаться у того, кто устанавливает проект.

При публикации желательно указать лицензию на написанный код: закрытую (никто не имеет права использовать код) или открытую, например: MIT, GPL, Apache License, BSD, WTFPL. Также, можно полностью отказаться от прав на код и выложить его в общественное достояние - public domain. Этот сайт может помочь в выборе: https://choosealicense.com/licenses/ . Стоит учесть, что некоторые хостинги репозиториев разрешают размещать бесплатно только открытые проекты, также, некоторые сторонние библиотеки можно добавлять только в открытые проекты с определенными лицензиями.

Также, в репозиторий нужно добавить README-файл с информацией о проекте. Он должен содержать хотя бы такие разделы:

  • название проекта
  • краткое (1-2 предложения) описание
  • требования к установке: какие программы, какой версии нужны (например: PHP >= 7.0, PostgreSQL >= 8.0)
  • порядок установки (кратко), например: создать такой-то конфиг, создать базу данных, загрузить в нее такой-то дамп, настроить веб-сервер так-то и так-то
  • при желании, скриншоты

Файл можно создать в текстовом формате (README.txt) или с использованием разметки Markdown (README.md). Пример README: https://github.com/codedokode/task-checker/blob/master/README.md (если нажать кнопку Raw, то можно увидеть текст с разметкой).

Подробное описание Markdown: http://belousovv.ru/markdown_syntax

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

Скрытие файлов из репозитория

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

В идеале, в репозитории не должно быть:

  • временных файлов
  • файлов, которые генерируются в ходе работы приложения, в том числе логов
  • файлов, которые устанавливаются с помощью менеджеров пакетов вроде Composer
  • индивидуальных конфигов отдельного разработчика
  • кода сторонних библиотек (в этой задаче можно этим пренебречь)
  • логинов, паролей, ключей доступа, токенов API от сторонних сервисов

Чтобы указать, какие файлы нужно не добавлять в репозиторий, в Git используют файл .gitignore, а в Mercurial - .hgignore.

Отдельная проблема - файл конфига, содержащий, например, настройки доступа к БД. Мы не должны публиковать пароль от базы данных разработчика, а если над проектом работает несколько человек, то они будут мешать друг другу, перезаписывая этот файл своими настройками. Для решения проблемы сам файл конфига добавляют в ignore-файл, а в репозиторий кладут образец конфига с другим именем (например, config.ini.dist или config.json.example). Разработчик скачивает проект и создает конфиг со своими настройками на основе образца.

Другой часто встречающийся случай - иногда надо скрыть содержимое папки, но добавить в репозиторий саму пустую папку. Git и Mercurial управляют файлами, а не папками, потому используют такой способ. Создают в папке файл, например, .gitkeep и пишут в ignore-файл одно правило, которое скрывает все файлы из этой папки, и другое, которое разрешает добавление .gitkeep.

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

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

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