Легкий ORM для PHP и MySQL
В статье приводится реализация объектно-реляционного отображения, выполненная в виде небольшой библиотеки с простым и гибким интерфейсом. Код библиотеки доступен в виде одного php-файла. В качестве интерфейса к базе данных используются удобные функции PHP для работы с MySQL.
Объектно-реляционное отображение (англ. object-relational mapping или ORM) — техника программирования, которая позволяет разработчику абстрагироваться от механизмов работы хранилища данных и писать код в терминах логики конкретного приложения. В частности, заменять написание SQL-запросов вызовом специальных функций/методов.1
Поиск по параметрам
Самая частая задача при разработке веб-приложений — это построение списка на основании некоторых условий. Такой список может быть разбит на страницы и определенным образом отсортирован.
В качестве примера рассмотрим получение списка товаров для страницы каталога в интернет-магазине, относящейся к определенному производителю.
Предположим, товары хранятся вот в такой таблице:
+----+-----------+-------+----------+-----------------+-------+
| id | title | price | discount | manufacturer_id | text |
+----+-----------+-------+----------+-----------------+-------
| 1 | Веник | 100 | 0 | 1 | |
| 2 | Швабра | 500 | 0 | 1 | |
| 3 | Чайник | 1500 | 10 | 2 | |
| 4 | Совок | 150 | 0 | 1 | |
| 5 | Телевизор | 5000 | 30 | 3 | |
| 6 | Ведро | 200 | 0 | 1 | |
+----+-----------+-------+----------+-----------------+-------+
Страница каталога снабжена формой поиска, позволяющей ограничивать список товаров по некоторым параметрам:
HTML-код формы:
Цена от <input name="price_final[min]">
до <input name="price_final[max]">
<input name="has_discount" type="checkbox"> скидка
<button>OK</button>
<br>
Название (начинается с) <input name="title">
</form>
В этом случае код, необходимый для получения данных товаров, будет выглядеть следующим образом:
$query = array(
"manufacturer_id = 2",
'where' => $_GET,
'orderby' => '[ord]',
'limit' => [ 'p', ['N',20] ],
);
$P = new Products;
$products = $P->find($query);
Методу find() передается массив с условиями поиска: конкретные значения параметров, указания по сортировке и LIMIT, а также ограничения выборки, задаваемые вручную.
Ключ where этого массива содержит значения параметров поиска, из которых по определенным правилам формируются условия конечного SQL-запроса (см. конфигурация параметров поиска). При вставке в запрос значения параметров автоматически экранируются2, лишние ключи (для которых в конфигурации нет соответствующего параметра) просто игнорируются.
Остальные условия поиска будут подробно рассмотрены в соответствующих разделах статьи (произвольные условия, LIMIT, сортировка).
Класс Products унаследован от специального абстрактного класса библиотеки и содержит в своем определении отображение параметров поиска (читай — полей формы) на структуру базы данных:
public $tables = [
"FROM products p",
// здесь могут быть также JOIN с другими таблицами
];
public $columns = [
'id' => [],
'price_final' => [
'sql' => "price * (100 - discount) / 100",
'type' => '[min,max]',
],
'title' => array[
'type' => 'LIKE%',
],
'has_discount' => array[
'sql' => "discount > 0",
'type' => 'on-off'
],
];
}
Чтобы вместо полных данных строк получить только их идентификаторы, нужно воспользоваться методом findIds(), интерфейс которого полностью аналогичен таковому метода find():
Для получения полных данных списка на основе уникальных идентификаторов служит метод getList():9
// $product_ids внутри метода будут проэкранированы
Идентификатор одиночной сущности можно получить с помощью метода findId():
Конфигурация параметров поиска
Теперь обратимся к массиву $columns и рассмотрим конфигурацию каждого из полей детально.
Поле, являющееся уникальным идентификатором записи в главной таблице, обязательно должно присутствовать и следовать в списке первым. Именно поэтому id упоминается в конфигурационном массиве, хотя его нет в форме.
Ключ type определяет, как конкретно указанное SQL-выражение будет сопоставлено значению соответствующего параметра (эти значения входят в условия для построения списка (массив $query) и передаются в ключе where). Сопоставление бывает четырех видов.
Равенство
type, не установленный вовсе (как в случае с id) или равный FALSE в результате даст условие со знаком «равно», а если значения параметра является не скалярной величиной, а массивом, то IN.
Например, если бы $_GET содержал в ключе id число 10, получилось бы условие
а если бы — массив array("Атос", "Д'Артаньян"), то3
Поддерживается и NULL — array("Атос", NULL, "Д'Артаньян") превратится в условие:
Сопоставление типа LIKE
Другой вариант сопоставления параметра содержит конфигурация поля title:
'type' => 'LIKE%',
...
)
Такая конфигурация даст условие вида
где 'параметр' — это экранированный $_GET['title']4.
Если $_GET['title'] является массивом, условие будет составлено для каждого из его элементов, и затем все они объединены через OR (получится как бы IN для LIKE):
Аналогичным образом можно использовать '%LIKE' и '%LIKE%'.
Интервальное сопоставление
Конфигурация поля price_final является примером интервального сопоставления:
'sql' => "price * (100 - discount) / 100",
'type' => '[min,max]',
...
)
Указанные ключи параметра ограничивают выборку: первый — снизу, второй — сверху. Условие в данном случае примет вид:
AND
price * (100 - discount) / 100 <= $_GET[price_final][max]
Вместо min и max можно указывать любые другие имена ключей, нужно лишь следить за тем, чтобы они совпадали с именами полей формы.
Можно указать только один ключ (например, '[min,]'), в этом случае будет возможность ограничить выборку только с одной стороны.
Если квадратные скобки заменить на круглые — '(min,max)' — нестрогое неравенство заменится на строгое (так же, как обозначают интервалы в математике). При этом они не обязательно должны быть одинаковыми: с одной стороны неравенство может быть строгим, а с другой — нет.
Сопоставление типа «включено/выключено»
Для такого сопоставления SQL-выражение просто вставляется в запрос в неизменном виде, но лишь если соответствующий параметр присутствует в запросе и отличен от FALSE.
Для поля has_discount с конфигурацией
'sql' => "discount > 0",
'type' => 'on-off'
)
в запрос попадет условие WHERE (discount > 0).
Это сопоставление лучше всего подходит для полей формы типа checkbox.
Произвольные условия
Ограничения списка могут не только формироваться на основании параметров поиска, но и быть прямо указаны вручную.
В рассматриваемом примере страница каталога относится к какому-то конкретному производителю. В этом случае условие на manufacturer_id должно выполняться всегда — независимо от содержимого $_GET:
"manufacturer_id = 2",
'where' => $_GET,
...
);
Таких произвольных условий может быть сколько угодно. Все они войдут запрос непосредственно в том виде, в котором указаны, без изменений.
Разбивка на страницы, LIMIT, получение общего количества строк
Для разбивки на страницы в условия для построения списка нужно включать элемент limit. В случае, если номер страницы передается в GET-параметре p, а количество записей на странице может содержаться в параметре N, код может выглядеть следующим образом:5
'limit' => array(
'page' => ( isset($_GET['p']) ? $_GET['p'] : 1),
'per_page' => ( isset($_GET['N']) ? $_GET['N'] : 20),
),
...
);
То же самое можно записать в сокращенной форме (как это и сделано в примере):
'limit' => [ 'p', ['N',20] ], // [...] - короткий вариант объявления массивов
'where' => $_GET, // в PHP с версии 5.4
...
);
При такой форме записи имена ключей в limit относятся к массиву с параметрами поиска (where).
Запись вида [ 'p', ['N',20,100] ] устанавливает для количества записей ограничение сверху.
Можно указывать числовые значения явно: 'limit' => '0, 20' или 'limit' => 100. Такая запись перейдет в LIMIT запроса в неизменном виде.
Общее количество строк без учета LIMIT можно получить, передав методу find() необязательный второй аргумент — переменную, в которую это количество будет записано:6
$products = $P->find($query, $count);
Сортировка
Указывать сортировку в условиях запроса следует в ключе orderby. При этом можно ссылаться на элементы массива $columns. Например:
'orderby' => "{*price_final DESC*}, id DESC",
'where' => ...
...
);
Фрагмент {*price_final DESC*} заменится на соответствующее SQL-выражение из массива $columns.
Чтобы дать возможность управлять сортировкой через параметры GET-запроса страницы, в ключ orderby нужно поместить специальную метку:
'orderby' => '&ord',
'where' => $_GET,
...
);
Здесь ord — ключ массива $_GET, содержащий текущие параметры сортировки.
Соответственно, адрес страницы должен иметь вид ...&ord=(имя поля из конфигурации) (направление). Например, ...&ord=price_final DESC. Метка в orderby заменится SQL-выражением для выбранного поля7, в результате запрос примет вид:
Можно комбинировать метку и постоянные части:
...
'orderby' => "&ord, discount > 0 DESC, ...",
);
А также задавать сортировку по умолчанию для случая, если она не указана через GET-параметр:
...
'orderby' => "&ord || discount > 0, id DESC",
);
Получение и обработка данных
Набор полей списка
Каждому параметру поиска соответствует одноименное поле в результирующем массиве данных. Взглянем еще раз на конфигурацию8:
'id' => array(
'sql' => "p.id",
...
),
'price_final' => array(
'sql' => "price * (100 - discount) / 100",
...
),
'title' => array(
...
),
'has_discount' => array(
'sql' => "discount > 0",
...
),
);
Вот как примерно выглядит результат работы метода find():
[5] => Array
(
[id] => 5
[price_final] => 35000
[title] => Телевизор
[has_discount] => 1
)
[4] => Array
(
[id] => 4
[price_final] => 1500
[title] => Совок
[has_discount] => 0
)
...
Если поле нужно в качестве параметра поиска, но среди конечных данных не требуется, его можно исключить из результирующего массива, поместив в начало ключа специальную последовательность @@:
'id' => ...
...
'@@: has_discount' => array(
'sql' => "discount > 0",
...
),
);
При такой конфигурации элемент has_discount среди полученных данных будет отсутствовать.
Получение данных одиночной сущности
Для получения данных одиночной сущности по её идентификатору служит метод getSingle():
$product = $P->getSingle($product_id);
Поля, которые для списка не требутся и нужны только для случая одиночной сущности, можно выделить явным образом. Их объявление должно начинаться с последовательности @::
...
"@: p.text",
);
Поле p.text при поиске и работе со списками запрашиваться из БД не будет, что даёт определенный выигрыш в быстродействии.
Пост-обработка данных
К каждой строке, полученной из БД, могут быть применены дополнительные преобразования. Для этого нужно определить метод processData(). Например:
...
function processData($row) {
$row['url'] = "/products/$row[id].html";
return $row;
}
}
Каждой записи результирующих данных добавится ключ url
[5] => Array
(
[id] => 5
[price_final] => 35000
[title] => Телевизор
[has_discount] => 1
[url] => /products/5.html
)
[4] => Array
(
[id] => 4
[price_final] => 1500
[title] => Совок
[has_discount] => 0
[url] => /products/4.html
)
...
Пост-обработка выполняется после приведения типов.
Получение специальных данных
Периодически бывает нужно получить не просто список сущностей со всеми полями, а какой-то специальный набор их данных. Для этого служит метод getSpecialData().
На вход метод принимает набор SQL-выражений для будущих колонок результата, а также массив с условиями поиска (аналогичный тому, что используется для методов find() и findIds()).
Например, для заданных условий поиска получить ценовой диапазон и количество товаров со скидкой от 50% можно с помощью следующего кода:
"manufacturer_id = 2",
'where' => $_GET,
...
);
$special_data = $P->getSpecialData(
array(
"SUM(discount >= 50) AS sale_count",
"MIN({*price_final*}) AS price_min",
"MAX({*price_final*}) AS price_max",
),
$query
);
Здесь {*price_final*} — ссылка на SQL-выражение в массиве $columns.
Содержимое $special_data будет примерно следующим:
(
[0] => Array
(
[sale_count] => 35
[price_min] => 1000
[price_max] => 12500
)
)
Получить текст SQL-запроса вместо результата можно с помощью метода getSpecialDataSQL(), принимающего те же аргументы, что и getSpecialData().
Если результат заведомо состоит из одной строки, удобно использовать метод getSpecialDataRow(), он вернёт массив меньшего уровня вложенности.
Другой пример — получение всех производителей (точнее, их идентификаторов), товары которых удовлетворяют определенным условиям:
$special_data = $P->getSpecialData(
"DISTINCT p.manufacturer_id",
$query
);
Здесь методу передается одна колонка вместо массива, что является инструкцей вернуть вместо табличного результата колоночный:
(
[0] => 2
[1] => 5
[2] => 10
)
Для получения результата, который состоит из одной ячейки, предназначен метод getSpecialDataCell(). Первый аргумент для него - строка с SQL-выражением, а не массив таких строк:
$special_data = $P->getSpecialDataCell("COUNT(*)", $query);
DDL: изменение структуры данных таблицы
ORM снабжен средствами генерации запросов, изменяющих структуру хранения данных (DDL — data definition language).
CREATE TABLE для главной таблицы
ORM позволяет выполнить SQL-запрос на создание главной таблицы или получить его содержимое.
Для этого в конфигурации соответствующих полей нужно указать ключ column_definition, а также создать специальное свойство класса под названием $mainTableConfig:
'name' => 'products',
'alias' => 'p',
'keys' => [ // определения ключей списком
'KEY (price)',
'KEY (discount)'
],
'properties' => [
'ENGINE' => 'MyISAM', // можно в виде пар ключ=значение
'AUTO_INCREMENT = 7', // а можно просто строкой
]
];
public $columns = [
'id' => [
'sql' => "p.id",
'column_definition' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
],
'title' => [
'column_definition' => 'VARCHAR(255) NOT NULL'
]
];
Создать таблицу можно так:
Вызвав вместо этого метод generateMainTableSQL(), получим текст запроса:
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
KEY (price),
KEY (discount)
) ENGINE = MyISAM AUTO_INCREMENT = 7
Запрос без IF NOT EXISTS в обоих методах можно получить, передав false в качестве аргумента.
Модификация колонок таблицы
Методы changeOrModifycolumn() и changeOrModifycolumnsQL() служат для исполнения запросов CHANGE/MODIFY/RENAME column.
Рассмотрим пример: нам требуется поменять тип колонки title на TEXT. Для этого нужно:
- Изменить её column_definition в $columns
-
Выполнить код: $P->changeOrModifycolumn('title');
(Аргументом здесь является ключ в $columns.)
Другой пример — нужно не только поменять тип колонки, но и переименовать её в name. В этом случае последовательность действий такая:
- Изменить column_definition в $columns
- Выполнить код: $P->changeOrModifycolumn('title', 'name');
- Изменить ключ в $columns с title на name
Если требуется только переименовать колонку:
-
Выполнить код: $P->changeOrModifycolumn('title', 'name', true);
Третий аргумент включает работу в режиме RENAME column. - Изменить ключ в $columns с title на name
Вызвав changeOrModifycolumnsQL с теми же аргументами, можно получить текст запроса без выполнения.
Приведение к типу при получении данных
PHP при получении данных из MySQL, преобразует все значения, кроме NULL, в строки.
Можно настроить поля в $columns так, чтобы данные преобразовывались в значения нужного типа.
Это делается либо в явном виде с помощью ключа value_type, либо автоматически на основании column_definition (см. ниже).
value_type может принимать следующие значения:
'int'
Величина будет преобразована в целое число (intval()).
Такой value_type автоматически устанавливается полям, которые согласно column_definition имеют тип из семейства INTEGER или DECIMAL без дробной части.
'float'
Величина будет преобразована в число с плавающей точкой (floatval()).
Автоматически устанавливается полям, которые в column_definition имеют тип FLOAT, DOUBLE или DECIMAL с дробной частью.
'json'
Этот тип автоматически устанавливается колонкам, имеющий одноименный тип в column_definition и приводит к преобразованию JSON-строки в массив или скалярную величину.
'bool'
Величина будет преобразована в логическое значение — true или false. NULL, как и во всех других случаях, будет оставлен без изменений.
Этот тип преобразования устанавливается только вручную в явном виде.
'datetime'
Такое преобразование можно использовать для значений даты/времени. Из обычных строк они превратятся в экземпляры специального класса, производного от DateTime, который снабжен методом __toString(), благодаря чему, при надобности (например, при выводе через echo или склейке с другими строками), преобразуются в стандартную строку даты. При этом с ними можно работать как с обычными объектами DateTime — смещать по временным интервалам, выводить в указанном формате и т.п.:
Этот тип преобразования, как и 'bool', устанавливается только вручную.
Отмена преобразования
Если в конфигурации явно указать value_type равным false, преобразование типа выполняться не будет независимо от column_definition.
JOIN подчиненных таблиц с группировкой
В случае связи с другой таблицей типа «один ко многим» можно настроить получение из неё данных с использованием различных агрегирующих функций.
Рассмотрим пример: для каждого товара нужно узнать сумму продаж.
Предположим, продажи хранятся в таблице order_items с колонками:
- product_id — id товара
- price — цена продажи
- quantity — количество единиц
- sold — был ли товар фактически продан
Конфигурацию класса нужно дополнить:
...,
"GROUPING LEFT JOIN order_items AS i ON i.product_id = p.id AND i.sold = 1"
// Нужен именно LEFT JOIN, т.к. продаж товара может не быть
];
public $columns = [
...,
"total_sales_cost" => [
"type" => "[min,max]",
"sql" => "SUM(i.price * i.quantity)"
]
];
Ключевое слово GROUPING указывает на необходимость группировки результата по уникальному идентификатору главной таблицы, которая будет включена в запрос механизмом ORM автоматически. (Оно не является синтаксической конструкцией MySQL и будет исключено из итогового текста запроса.)
С total_sales_cost можно обращаться как с обычным параметром, используя его при фильтрации и группировке:
$query = [
'where' => [ 'total_sales_cost' => [ 'min' => 1000000 ] ],
];
// 10 самых продаваемых товаров
$query = [
'orderby' => '{*total_sales_cost*} DESC',
'limit' => 10
];
Рассмотрим другой, немного более сложный случай: получение суммы покупок клиентов.
В этом случае связь главной таблицы — таблицы клиентов — с таблицей продаж не прямая, а через промежуточную таблицу заказов:
"FROM clients AS c",
"GROUPING LEFT JOIN orders AS o ON o.client_id = o.id",
"LEFT JOIN order_items AS i ON i.product_id = p.id AND i.sold = 1"
];
Здесь GROUPING указывается для таблицы orders, т.к. связь «один ко многим» начинается именно с неё.
Связанные сущности
Под связанной понимается сущность другого рода, на которую через уникальный идентификатор ссылается некоторое поле данной сущности.
Например, у каждого товара есть производитель, id которого хранится в поле manufacturer_id. Производителям соответствует свой собственный класс Manufacturers, также унаследованный от _List, где описывается вся логика работы с ними: набор полей, параметры поиска, пост-обработка данных и т.д.
Чтобы этой логикой пользоваться в классе товаров, нужно установить его связь с классом производителей. Это делается через соответствующее поле (в данном случае — manufacturer_id), которому в конфигурации устанавливается свойство related:
public $columns = array(
...
'manufacturer_id' => array(
'related' => array('manufacturer' => 'Manufacturers')
// 'имя ключа' => 'класс' (объяснение см. далее по тексту)
),
);
...
}
class Manufacturers extends _List {
public $tables = array(
"FROM manufacturers m",
);
public $columns = array(
"id",
"title",
"country_id",
);
function processData($row) {
$row['url'] = "/manufacturers/$row[id].html";
return $row;
}
...
}
Установление такой связи даёт две важных возможности.
Поиск по свойствам связанных сущностей
Среди параметров поиска могут быть относящиеся именно к производителям, а не только к самим товарам.
При передаче методу find() в составе $query['where'] они должны быть сгруппированы в подмассив manufacturer (имя подмассива указывается в конфигурации в качестве ключа в related — см. выше).
Например, включать в форму поиска поле для указания страны производителя нужно с атрибутом name="manufacturer[country_id]":
Цена от <input name="price_final[min]">
до <input name="price_final[max]">
<input name="has_discount" type="checkbox"> скидка
<br>
Название (начинается с) <input name="title">
<br>
Страна производства:
<select name="manufacturer[country_id]">
<option value="1">Россия</option>
<option value="2">США</option>
<option value="3">Германия</option>
</select>
<button>OK</button>
</form>
При этом сам вызов find() не меняется никак:
'where' => $_GET,
'orderby' => '[ord]',
'limit' => [ 'p', ['N',20] ],
);
$P = new Products;
$products = $P->find($query);
Единственное, что требуется — составить форму так, чтобы параметры поиска, относящиеся к производителю, приходили в подмассиве $_GET['manufacturer'].
Получение данных связанных сущностей
У каждой записи из $products автоматически появится подмассив manufacturer:
[5] => Array
(
[id] => 5
[price_final] => 35000
[title] => Телевизор
[has_discount] => 1
[url] => /products/5.html
[manufacturer] => Array
(
[id] => 3
[title] => SONY
[country_id] => 7
[url] => /manufacturers/3.html
)
)
[4] => Array
(
[id] => 4
[price_final] => 1500
[title] => Совок
[has_discount] => 0
[url] => /products/4.html
[manufacturer] => Array
(
[id] => 1
[title] => Полимербытхимпром
[country_id] => 1
[url] => /manufacturers/1.html
)
)
...
Добавление, изменение и удаление записей
write()
Для добавления записей и изменения их данных служит метод write(). В качестве аргументов он принимает массив полей записи и её идентификатор:
$P->write($_POST, $_GET['id']); // данные пришли из формы со страницы вида ?id=...
Если идентификатор не указывать, будет добавлена новая запись, а в ответ возращен ее идентификатор:
$new_id = $P->write($_POST);
При записи данные экранируются (используется mysql_write_row()), однако никаких проверок метод в себя не включает. Для этого рекомендуется использовать специальный инструмент для проверки правильности заполнения веб-форм.
write() может работать также в режимах DUPLICATE, IGNORE и REPLACE. Использование аналогично mysql_write_row(), за исключением того, что не требуется передавать имя таблицы.
Если таблица имеет уникальные ключи помимо первичного, для вставки данных в режиме DUPLICATE можно использовать метод insertOnDuplicateKeyUpdateAndReturnId(), который позволяет получить значение автоинкрементного поля для затронутой строки, будь то вновь вставленная или обновленная существующая:
delete()
Метод delete() в качестве аргумента принимает уникальный идентификатор (автоматически экранируется) и просто удаляет соответствующую запись из таблицы, возвращая TRUE в случае успеха:
$P = new Products;
$P->delete($id);
Дополнительные сведения и возможности
В основе данной библиотеки лежит подход к построению списков, сущность которого заключается в следующем: сначала из БД с учетом условий, сортировки и LIMIT извлекаются уникальные идентификаторы строк, после чего для этих строк запрашиваются остальные данные. Обоснованию и разъяснению этого подхода посвящена отдельная статья.
1. ▲ В классической реализации объектно-реляционного отображения каждой строке базы данных соответствует экземпляр объекта. Предлагаемая реализация не является классической: каждой строке здесь соответствует просто ассоциативный массив данных.
2. ▲ Используется функция mysql_escape().
3. ▲ Каждое из условий заключается в круглые скобки, чтобы ограничить действие оператора OR, если таковой встретится в выражении. То же самое касается произвольных условий (см. соответствующий раздел).
4. ▲ Следует отметить, что в конфигурации поля title отсутствует ключ sql. В таких случаях в качестве SQL-выражения используется непосредственно имя параметра.
5. ▲ Нумерация страниц начинается с единицы.
6. ▲ Если переменная для записи общего количества строк передана, будет выполнен отдельный запрос вида SELECT COUNT(*). Вопреки распространенной практике, такой вариант обладает более высокой производительностью, чем использование SQL_CALC_FOUND_ROWS. Подробнее см. http://sqlinfo.ru/forum/viewtopic.php?pid=38337
7. ▲ Если GET-параметр сортировки не соответствует ни одному из ключей массива $columns, он будет просто проигнорирован.
8. ▲ Конфигурация в примере имеет полную форму записи. Можно использовать также сокращенные формы:
// все нижеуказанные варианты записи равнозначны
'column' => [ // полная запись
'sql' => 'table.column',
],
'column' => 'table.column',
'table.column',
'column', // если среди всех задействованных таблиц нет колонок с одинаковыми именами,
// указывать таблицу в явном виде необязательно
...
);
9. ▲ Метод find(), собственно говоря, состоит из вызова findIds() и getList().
10. ▲ Из-за этой возможности важно проверять пользовательские данные в случае, если они передаются методу напрямую. Например, для $P->getList($_GET['ids']) важно убедиться, что в GET-запросе передается именно массив чисел, а не строка, иначе возможна SQL-инъекция.
© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.