JohnCMS | Переход на PDO в примерах

2.18K
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
Итак, разберем на реальном примере, как можно переделать старый модуль на PDO.
Переделывать мы будем нашу гастивуху /guestbook.php

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

Чтоб тему не разосрали, я ее закрою.
Высказаться и задать вопрос можно в этой теме: Переход на PDO - обсужнение
Шпаргалка по PDO тут: JohnCMS | Шпаргалка по PDO
З.Ы.
Пишу "вживую" по мере работы, посему посты будут постоянно переделываться и дополняться.
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
ПОДГОТОВКА РАБОЧЕГО МЕСТА

Для работы и отладки нам понадобится какий-нибудь AMP сервер (Apache + MySQL + PHP).
Мы для примеру возьмем Open Server.
Ну и для кодинга желательно использовать не блокнот, а какую-нибудь IDE, это избавит Вас от множества ошибок и сильно облегчит процесс кодирования.

НАСТРОЙКА OPEN SERVER

1)В настройках Open Server отключаем все не нужные модули (типа nginx, memcached и др.).
Оставляем только:
apache 2.4
PHP 5.5, или 5.6
Mysql 5.5 или MariaDB 10

2) В папке с доменами создаем папку для нашего движка, пусть это будет johncms.dev

3) Запускаем OpenServer, убеждаемся, что все работает и наш домен johncms.dev открывается (там пока ничего нет).

4) Открываем PhpMyAdmin и создаем пустую базу данных для нашего проекта, к примеру johncms

УСТАНОВКА JOHNCMS

Заходим к нам в загрузки, скачиваем и устанавливаем JohnCMS 7.x.x
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
Далее, я буду рассказывать что делал я сам и показывать на примерах. К Вам описанный мною код попадет уже доработанным, но на примерах Вы сможете понять что делать со своими модулями.
Запускаю свой редактор кода (у меня PhpStorm) и открываю /guestbook/index.php
===
В начале, мне нужно получить объект PDO, иными словами это будет переменная $db в которой будет наш PDO. Для этого в самом начале файла, НО ПОСЛЕ подключения ядра (после строки require('../system/bootstrap.php');) я размещаю данный код:
/** @var PDO $db */
$db = App::getContainer()->get(PDO::class);


Я обращаюсь к контейнеру и получаю у него объект PDO, который для дальнейшего применения в пределах данного файла, будет находиться в переменной $db
Далее, мы уже сможем переделывать наши запросы.
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
Первый попавшийся запрос в строке 39
mysql_query("DELETE FROM `guest` WHERE `id` = '" . $id . "'");

После переделки на PDO он выглядит так:
$db->exec("DELETE FROM `guest` WHERE `id` = '" . $id . "'");

В принципе уже все готово, но можно слегка сократить и оптимизировать код.
Внимательно глянув на запрос, мы видим, что поле `id` у нас имеет числовой тип и ключевое. Поэтому, в запросе для переменной $id (которая у нас имеет числовой тип) можно избавиться от кавычек.
Кроме того, сам запрос вставляем не в двойные кавычки, а в одинарные. Это сильно разгрузит РНР, ибо ему не придется интерпретировать переменные в строке нашего запроса.
Окончательный вариант выглядит так:
$db->exec('DELETE FROM `guest` WHERE `id` = ' . $id);

Пример в репозитории

З.Ы.
В данном запросе я применил не $db->query (который тоже будет работать) а метод $db->exec
О разнице между $db->query и $db->exec я потом расскажу в отдельной теме со шпаргалками.
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
Следующий устаревший код, предназначенный для переделки у нас находится в строке 62
$from = $user_id ? $login : mysql_real_escape_string($name);

Это интересный и ВАЖНЫЙ момент, ибо он связан не с самим запросом, а с экранированием его данных, для предотвращения SQL инъекции и взлома Вашего сайта.
В старом коде для этого у нас была функция mysql_real_escape_string()
В PDO для достижения той же цели, можно пойти несколькими путями...

1) Простейший путь - это применить вместо устаревшей функции ее аналог $db->quote() и строка в этом случае будет выглядеть так:
$from = $user_id ? $login : $db->quote($name);
Однако $db->quote не полный аналог mysql_real_escape_string(), ибо он кроме экранирования кавычек внутри строки, еще и берет в кавычки саму строку. Следовательно нам надо будет переделать еще и запрос, убрав кавычки вокруг вставляемой переменной.

2) Другой и более правильный путь - это подготовленный запрос, который мы сейчас и будем реализовывать.
===
Итак, решил делать подготовленный запрос.
В начале я осмотрел дальнейший код на предмет, ГДЕ используется наша переменная $from и нашел нужный запрос, который в строке 120 и который надо переделать на подготовленное выражение. Для этого применяем метод $db->prepare() и заменяем все переменные в запросе на простые плейсхолдеры ? (знак вопроса). Есть еще именованные плейсхолдены, но их рассмотрим отдельно.
Старый код запроса временно где-то сохраняем, ибо нам понадобятся оттуда переменные.
После переделки на подготовленный запрос, код будет выглядеть так:
$stmt = $db->prepare("INSERT INTO `guest` SET
  `adm` = ?,
  `time` = ?,
  `user_id` = ?,
  `name` = ?,
  `text` = ?,
  `ip` = ?,
  `browser` = ?,
  `otvet` = ''
");

Обратите внимание на строку `otvet` = '', там передается пустое значение, поэтому я не стал ставить плейсхолдер а применил прямой запрос. То же самое можно делать, если передаете фиксированное числовое значение, к примеру `otvet` = 5
А для всех "опасных" данных, нужны плейсхолдеры.

Но это пока только подготовленное выражение, которое ничего не добавит в базу и которому мы пока не передали никаких данных. займемся этим. Нам нужно передать подготовленному запросу реальные данные и запустить его на выполнение (вставить запись в базу данных). Для этого применяем $stmt->execute(); в который в виде массива передаются наши данные, которые мы заменили плейсхолдерами.
ОЧЕНЬ ВАЖНО, чтоб данные в массиве находились именно в той последовательности, как у нас размещены плейсхолдеры, не перепутайте!
В итоге сравним что было и что стало:

СТАРЫЙ ЗАПРОС:
mysql_query("INSERT INTO `guest` SET
  `adm` = '$admset',
  `time` = '" . time() . "',
  `user_id` = '" . ($user_id ? $user_id : 0) . "',
  `name` = '$from',
  `text` = '" . mysql_real_escape_string($msg) . "',
  `ip` = '" . core::$ip . "',
  `browser` = '" . mysql_real_escape_string($agn) . "',
  `otvet` = ''
");


НОВЫЙ ПОДГОТОВЛЕННЫЙ ЗАПРОС
$db->prepare("INSERT INTO `guest` SET
  `adm` = ?,
  `time` = ?,
  `user_id` = ?,
  `name` = ?,
  `text` = ?,
  `ip` = ?,
  `browser` = ?,
  `otvet` = ''
")->execute([
  $admset,
  time(),
  ($user_id ? $user_id : 0),
  $from,
  $msg,
  core::$ip,
  $agn,
]);


ВАЖНО: в подготовленном запросе не надо экранировать данные, то, что мы делали с помощью mysql_real_escape_string(). Более того, если вспомним строку 62, мы с нее тоже должны убрать экранировку.

Итого, после доработки в репозитории у нас следующее:
Строка 62: https://github.com/john-cms/jo ... p#L62
Подготовленный запрос: https://github.com/john-cms/jo ... -L137

Можно работать дальше
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
В том же блоке кода, который мы дорабатывали выше, есть еще пара мелких запросов, давайте разберем их.

В строке 55 мы видим следующий запрос:
$req = mysql_query("SELECT `time` FROM `guest` WHERE `ip` = '$ip' AND `browser` = '" . mysql_real_escape_string($agn) . "' AND `time` > '" . (time() - 60) . "'");
Глядя на него я вижу, что там применяется всего одна переменная требующая экранировки, остальные числовые и безопасные. Посему стало лень писать подготовленный запрос и решил обойтись простой экранировкой с помощью метода ->quote() тем более на практике покажу его применение.
Для начала, мы заменяем все старые функции Mysql на их аналоги из PDO.
$req = $db->query("SELECT `time` FROM `guest` WHERE `ip` = '$ip' AND `browser` = '" . $db->quote($agn) . "' AND `time` > '" . (time() - 60) . "'");
Но если запустите скрипт, он вывалится с ошибкой, SQL Запрос не рабочий. Почему?
Да потому, что как я писал выше, ->quote не только экранирует строку, но и берет ее в кавычки.
А у нас то в запросе переменная уже была взята в кавычки, посему получается ошибка.
Следовательно из запроса надо убрать кавычки вокруг $db->quote($agn)
НЕ ЗАБЫВАЙТЕ ПРО ЭТО!
Рабочий вариант запроса выглядит так:
$req = $db->query("SELECT `time` FROM `guest` WHERE `ip` = '$ip' AND `browser` = " . $db->quote($agn) . " AND `time` > '" . (time() - 60) . "'");

---
Далее (строка 97), мы видим, что проверяется число возвращенных запросом строк mysql_num_rows().
if (mysql_num_rows($req)) {...}

В PDO для этого есть аналог ->rowCount() и переделанная строка будет смотреться так:
if ($req->rowCount()) {...}


Ну и наконец, получение результата запроса в виде ассоциативного массива (строка 98)
$res = mysql_fetch_assoc($req);
после переделки выглядит так:
$res = $req->fetch();
.
AlkatraZ
╭∩╮ (`-`) ╭∩╮
Ну и далее, в том же духе, постепенно перебираете весь код и меняете старые mysql функции на новые от PDO
===
Далее, в случае с нашей гастивухой я уже не буду описывать все свои действия, ибо они в основном повторяют то, что уже было написано выше. Остановлюсь только на интересных моментах.

СОКРАЩЕНИЕ ОБЪЕМА КОДА

У нас есть стандартный запрос на выборку:
$req = mysql_query("SELECT `edit_count` FROM `guest` WHERE `id`='$id'");
$res = mysql_fetch_array($req);

Если переменная $req больше нигде не используется (не подсчитывапется число полученных строк), то код можно сократить в одну строку:
$res = mysql_fetch_array(mysql_query("SELECT `edit_count` FROM `guest` WHERE `id`='$id'"));

То же самое мы проведем и с PDO, переделанный код будет выглядеть так:
$res = $db->query("SELECT `edit_count` FROM `guest` WHERE `id`='$id'")->fetch();
.
(\/)____o_O____(\/)
AlkatraZ, забыл упомянуть про fetchColumn()
.
╭∩╮ (`-`) ╭∩╮
# Koenig (12.08.2016 / 19:04)
AlkatraZ, забыл упомянуть про fetchColumn()
Это я раскажу в шпаргалках.
.
╭∩╮ (`-`) ╭∩╮
ОКОНЧАТЕЛЬНОЕ ТЕСТИРОВАНИЕ

Для окончательного тестирования, в настройках Open Server устанавливаете PHP 7.
Далее, перезагружаете сервер и проверяете свой скрипт. Все должно работать.
З.Ы.
Дело в том, что из РНР7 старый Mysql вообще удален. И если вы где-то забыли переписать функции, или сделали ошибку, сразу все покажет.

А если из под РНР 7 все нормально работает, значит радуйтесь, переход на PDO сделан успешно.
Всего: 26