PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 116 · После первого SELECT и подготовленных параметров
MySQL: кодировка, правила сравнения и понятный текстовый поиск
Почему cafe находит Café, когда UNIQUE считает коды одинаковыми, откуда берутся невидимые пробелы и как вернуть только нужную страницу товаров.
Сначала определить, что означает совпадение
Покупатель вводит cafe и хочет увидеть Café. Интеграция присылает Ab-7 и ab-7 как разные коды. Каталог должен найти только опубликованные товары и вернуть страницу, которую человек сейчас видит. Во всех трёх задачах недостаточно просто написать WHERE name = ?: сначала требуется договор о том, что считается совпадением.
- Сохранить исходный текст без потерь: utf8mb4
- Определить равенство и порядок: collation
- Выбрать условие: точное значение, начало, подстрока или слова
- Отфильтровать и ограничить результат на стороне MySQL
| Понятие | Простой смысл |
|---|---|
| Кодировка / character set | Как символы представлены в байтах; utf8mb4 хранит в том числе четырёхбайтные символы |
| Collation | Правила сравнения и сортировки текста, включая чувствительность к регистру и акцентам |
| Нормализация ввода | Явное изменение строки приложением, например снятие разрешённых краевых пробелов |
| Предикат WHERE | Условие, по которому MySQL оставляет подходящие строки |
| Индекс | Дополнительная структура доступа; выбор и цена зависят от запроса и данных |
Сначала нужны работающее подключение, понимание таблицы и столбца и один подготовленный параметр. Здесь каждый новый SQL-элемент объясняется на маленьком результате; полный PHP-код с подключением доступен после этого результата.
Запустить пример и увидеть таблицу результата
Каждый архив сам создаёт только свои TEMPORARY-таблицы mysql111_text_* и фиксированные данные. Нужны PHP 8.4+ с PDO_MYSQL/mysqlnd и MySQL 8.4; контрольный стенд — PHP 8.5.11 / MySQL 8.4.13. Подключение через PDO::connect и современные константы драйвера определяют минимальную версию PHP этого комплекта.
Настройка отдельной локальной базы описана в первом подключении. В терминале задайте PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER, PHPMAN_MYSQL_PASSWORD. У контрольного сервера адрес 127.0.0.1 и порт 33070; ваш порт может отличаться. Нужны SELECT, INSERT и CREATE TEMPORARY TABLES. Распакуйте один архив, выполните php example.php, откройте result.html. README и tc111-helper.php уже внутри.
result.json содержит те же строки, metrics.json — версии, фактическую диагностику и проверки без реквизитов. Временные таблицы исчезнут вместе с соединением. Серверные настройки и постоянная схема не меняются. Вывод — сохранённый результат настоящего PHP CLI и MySQL, а не действующий поиск посетителя сайта.
Сохранить кириллицу и emoji: символы не равны байтам
Редактор сохраняет «Чай 😀» в каталоге. После чтения нужно получить то же написание, а не вопросительный знак вместо emoji. Одновременно лимит имени в символах нельзя проверять количеством байт PHP.
Исходные данные
| ID | Вводим |
|---|---|
| 1 | Чай |
| 2 | Cafe |
| 3 | Café |
| 4 | Чай 😀 |
- PHP передаёт строку Чай 😀 параметром INSERT
- Соединение charset=utf8mb4 сообщает кодировку текста
- VARCHAR с utf8mb4 сохраняет все пять символов
- SELECT возвращает Чай 😀: 5 символов и 11 байт
Что получится
| ID | Сохранённое название | Символов MySQL | Байт MySQL | Байт PHP |
|---|---|---|---|---|
| 1 | Чай | 3 | 6 | 6 |
| 2 | Cafe | 4 | 4 | 4 |
| 3 | Café | 4 | 5 | 5 |
| 4 | Чай 😀 | 5 | 11 | 11 |
Минимальная запись по задаче
SELECT name, CHAR_LENGTH(name) AS characters_count,
LENGTH(name) AS bytes_count
FROM mysql111_text_names
ORDER BY id;Этот фрагмент объясняет нужное действие на учебных таблицах. Полная программа ниже создаёт исходные данные сама; отдельно выполнять предыдущие примеры не нужно.
Набор символов (character set, здесь utf8mb4) отвечает за представление текста. Collation отвечает за его сравнение и порядок. Это разные настройки: можно сохранить Café без потерь и всё равно считать его равным cafe при поиске. В примере кодировка задана и у соединения PDO, и у таблицы. Изменение только правила сравнения не исправляет уже испорченные байты.
VARCHAR(40) задаёт верхнюю границу в символах для этого типа; это не обещание, что строка займёт 40 байт. SELECT перечисляет нужные поля, CHAR_LENGTH(name) считает символы MySQL, LENGTH(name) — байты. AS даёт вычисленному столбцу понятное имя. У русской буквы в этих примерах два байта UTF-8, у 😀 — четыре. PHP strlen также считает байты; именно поэтому последняя пара столбцов совпадает.
INSERT использует два вопросительных знака: id и name передаются отдельно через execute. foreach здесь заполняет четыре учебные строки, а не фильтрует большой каталог после его выгрузки. Проверка сравнивает обратно прочитанные названия с исходными, а не только успешность команды. helper формирует result.html из этих фактических значений и экранирует их для HTML.
Границы и частые ошибки
CHAR_LENGTH не является универсальным счётчиком видимых пользователю знаков: комбинируемые символы и составные emoji могут состоять из нескольких кодовых точек. Если ограничение относится именно к визуальным знакам, нужен отдельный Unicode-договор. Эти примеры не обучают восстановлению уже повреждённой кодировки. База, соединение и исходный PHP-файл должны согласованно использовать UTF-8.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/tc111-helper.php';
$db=tc111Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),'server'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION)];
$details=[];
$db->exec('CREATE TEMPORARY TABLE mysql111_text_names (id INT PRIMARY KEY,name VARCHAR(40) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci');
$input=['Чай','Cafe','Café','Чай 😀'];
$insert=$db->prepare('INSERT INTO mysql111_text_names (id,name) VALUES (?,?)');
foreach($input as $i=>$name){$insert->execute([$i+1,$name]);}
$data=tc111Rows($db,'SELECT id,name,CHAR_LENGTH(name) AS characters_count,LENGTH(name) AS bytes_count FROM mysql111_text_names ORDER BY id');
$rows=[];foreach($data as $r){$rows[]=[(int)$r['id'],$r['name'],(int)$r['characters_count'],(int)$r['bytes_count'],strlen($r['name'])];}
$charset=tc111Rows($db,'SELECT @@character_set_client AS client_set,@@character_set_connection AS connection_set,@@character_set_results AS results_set')[0];
$checks=['text_roundtrip'=>array_column($data,'name')===$input,'sizes_are_explicit'=>$rows===[[1,'Чай',3,6,6],[2,'Cafe',4,4,4],[3,'Café',4,5,5],[4,'Чай 😀',5,11,11]],'connection_is_utf8mb4'=>array_values($charset)===['utf8mb4','utf8mb4','utf8mb4']];
$details=['connection_charsets'=>$charset,'column_charset'=>'utf8mb4','column_collation'=>'utf8mb4_0900_ai_ci'];
$db=null;
tc111Report('mysql111-text-storage', ['ID', 'Сохранённое название', 'Символов MySQL', 'Байт MySQL', 'Байт PHP'], $rows, $checks, $runtime, $details);
echo 'mysql111-text-storage: verified result.html and result.json
';
Ожидаемый результат
mysql111-text-storage: verified result.html and result.json
Откройте result.html и сравните конкретные строки с таблицей выше. Архив содержит helper и README для самостоятельного запуска на локальном MySQL.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Замените 😀 на два отдельных 😀. Получится 6 символов и 15 байт: 3 русские буквы, пробел и два emoji. Это проверяет понимание единиц, а не другое правило сравнения.
Выбрать, должны ли cafe, Café и CAFE совпадать
В поиске по названиям cafe должен находить Café и CAFE. В отдельном поле точного кода регистр может быть значимым. Сначала формулируем эти два требования, затем выбираем правила для поля или конкретного выражения.
Исходные данные
| ID | Название |
|---|---|
| 1 | Café |
| 2 | CAFE |
| 3 | cafe |
| 4 | Чай |
| 5 | чай |
- Поисковый параметр cafe
- ai_ci игнорирует акцент и регистр → ID 1,2,3
- as_cs учитывает акцент и регистр → ID 3
- В обеих выдачах база возвращает исходное написание
Что получится
| Ищем | Правило | Найденный ID | Написание в базе |
|---|---|---|---|
| cafe | ai_ci | 1 | Café |
| cafe | ai_ci | 2 | CAFE |
| cafe | ai_ci | 3 | cafe |
| cafe | as_cs | 3 | cafe |
| чай | ai_ci | 4 | Чай |
| чай | ai_ci | 5 | чай |
| чай | as_cs | 5 | чай |
Минимальная запись по задаче
SELECT id, name
FROM mysql111_text_compare
WHERE name COLLATE utf8mb4_0900_as_cs = ?
ORDER BY id;
-- Значение параметра: cafeЭтот фрагмент объясняет нужное действие на учебных таблицах. Полная программа ниже создаёт исходные данные сама; отдельно выполнять предыдущие примеры не нужно.
Имя utf8mb4_0900_ai_ci читается по частям: utf8mb4 — набор символов; 0900 — версия правил Unicode Collation Algorithm; ai — accent insensitive, не различать акценты; ci — case insensitive, не различать регистр. В utf8mb4_0900_as_cs суффиксы as и cs требуют учитывать оба различия. Они не переводят содержимое в нижний регистр и не стирают акценты в сохранённом тексте.
WHERE оставляет подходящие строки; COLLATE указывает правило для сравниваемого выражения; = проверяет равенство согласно этому правилу. ? — одно значение поиска, переданное PDO. ORDER BY id закрепляет порядок демонстрации, поэтому сравнение результатов не зависит от неоднозначной сортировки равных имён. Кириллические Чай и чай позволяют увидеть тот же вопрос регистра на знакомом тексте.
Полная программа перебирает ровно два заранее записанных имени collation. Имена SQL-правил нельзя подставлять из пользовательской строки. Маркер ? обозначает значение, а не имя collation, таблицы или оператора. Если приложение поддерживает режимы поиска, выберите готовый SQL из разрешённого набора. Основное правило для часто используемого поля лучше определить при проектировании столбца.
Границы и частые ошибки
as_cs — лингвистическое сравнение, не обещание побайтовой идентичности любых Unicode-последовательностей. Для двоичных идентификаторов отдельно рассматривают VARBINARY. Не объявляйте одну collation правильной для всех языков и предметных областей. COLLATE в запросе также может изменить доступность индексного пути: совместимость с индексом и фактический план нужно смотреть отдельно.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/tc111-helper.php';
$db=tc111Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),'server'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION)];
$details=[];
$db->exec('CREATE TEMPORARY TABLE mysql111_text_compare (id INT PRIMARY KEY,name VARCHAR(40) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci');
$db->exec("INSERT INTO mysql111_text_compare VALUES (1,'Café'),(2,'CAFE'),(3,'cafe'),(4,'Чай'),(5,'чай')");
$rows=[];$ids=[];
foreach(['cafe','чай'] as $term){
foreach(['ai_ci'=>'utf8mb4_0900_ai_ci','as_cs'=>'utf8mb4_0900_as_cs'] as $label=>$collation){
// Both SQL names come from this fixed map, never from request data.
$found=tc111Rows($db,'SELECT id,name FROM mysql111_text_compare WHERE name COLLATE '.$collation.' = ? ORDER BY id',[$term]);
$ids[$term][$label]=array_map('intval',array_column($found,'id'));
foreach($found as $r){$rows[]=[$term,$label,(int)$r['id'],$r['name']];}
}
}
$checks=['accent_and_case_ignored'=>$ids['cafe']['ai_ci']===[1,2,3],'accent_and_case_kept'=>$ids['cafe']['as_cs']===[3],'cyrillic_case_ignored'=>$ids['чай']['ai_ci']===[4,5],'cyrillic_case_kept'=>$ids['чай']['as_cs']===[5],'stored_spelling_unchanged'=>array_column(tc111Rows($db,'SELECT name FROM mysql111_text_compare ORDER BY id'),'name')===['Café','CAFE','cafe','Чай','чай']];
$details=['matching_ids'=>$ids,'normalization_performed'=>false];
$db=null;
tc111Report('mysql111-text-collation', ['Ищем', 'Правило', 'Найденный ID', 'Написание в базе'], $rows, $checks, $runtime, $details);
echo 'mysql111-text-collation: verified result.html and result.json
';
Ожидаемый результат
mysql111-text-collation: verified result.html and result.json
Откройте result.html и сравните конкретные строки с таблицей выше. Архив содержит helper и README для самостоятельного запуска на локальном MySQL.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Поменяйте параметр cafe на Café. При ai_ci останутся ID 1,2,3; при as_cs останется ID 1. Строки таблицы после поиска не изменятся.
Согласовать UNIQUE с правилами имени и внешнего кода
В одном справочнике менеджер не должен повторно заводить название cafe под написанием café. Другая интеграция присылает два действительно разных внешних кода Ab-7 и ab-7. Одинаковая политика UNIQUE для этих полей была бы ошибкой.
Исходные данные
| Поле | Попытки записи | Предметное правило |
|---|---|---|
| Название | Cafe, затем café | Акценты и регистр не создают новую запись |
| Внешний код | Ab-7, затем ab-7 | Эти два написания поставщик считает разными |
- Cafe попадает в поле label с ai_ci и UNIQUE
- café сравнивается с Cafe по тому же ai_ci → конфликт 1062
- Ab-7 и ab-7 попадают в code с as_cs
- Для этого поля два кода различаются → сохранены обе строки
Что получится
| Договор поля | Вход | Итог записи | Строк после операции |
|---|---|---|---|
| Название, ai_ci | Cafe | Сохранено | 1 |
| Название, ai_ci | café | Отклонено: дубликат 1062 | 1 |
| Внешний код, as_cs | Ab-7 | Сохранено | 1 |
| Внешний код, as_cs | ab-7 | Сохранено | 2 |
Минимальная запись по задаче
code VARCHAR(40)
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_as_cs
NOT NULL UNIQUE
-- Фрагмент определения столбца внутри CREATE TABLE.Этот фрагмент объясняет нужное действие на учебных таблицах. Полная программа ниже создаёт исходные данные сама; отдельно выполнять предыдущие примеры не нужно.
UNIQUE — ограничение, которое база применяет при записи. Оно проверяет равенство текстовых значений по collation столбца. Поэтому дело не в запрете хранить буквы верхнего регистра: первый Cafe хранится как введён. Запрет относится ко второй строке, равной первой по выбранному правилу.
Определение code поясняется по элементам: VARCHAR(40) — текст до 40 символов; CHARACTER SET — кодировка; COLLATE — сравнение; NOT NULL — обязательное значение; UNIQUE — отсутствие повторения этого значения в таблице. id остаётся отдельным числовым первичным ключом. Эти две независимые учебные таблицы показывают два предметных договора, а не миграцию рабочей схемы.
Вызов execute для café намеренно получает PDOException. Программа принимает только ожидаемую пару SQLSTATE 23000 и код MySQL1062, остальные ошибки передаёт дальше. Общий 23000 описывает класс нарушения целостности и сам по себе не означает именно дубликат. После отказа проверяется, что первый Cafe остался, а в таблице кодов действительно две записи. Предварительный SELECT в приложении не заменяет ограничение: между проверкой и INSERT другой запрос тоже может создать запись.
Границы и частые ошибки
Колонки примера NOT NULL; поведение нескольких NULL в UNIQUE здесь не демонстрируется. Изменять collation существующего уникального поля нужно после проверки возможных новых совпадений и правил внешней системы, а не вслепую ALTER TABLE. as_cs не превращает текстовый код в двоичный токен. Автоматически обрезать пробелы или менять регистр кодов можно только по согласованному договору.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/tc111-helper.php';
$db=tc111Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),'server'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION)];
$details=[];
$db->exec('CREATE TEMPORARY TABLE mysql111_text_labels (id INT PRIMARY KEY,label VARCHAR(40) COLLATE utf8mb4_0900_ai_ci NOT NULL UNIQUE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$db->exec('CREATE TEMPORARY TABLE mysql111_text_codes (id INT PRIMARY KEY,code VARCHAR(40) COLLATE utf8mb4_0900_as_cs NOT NULL UNIQUE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$label=$db->prepare('INSERT INTO mysql111_text_labels (id,label) VALUES (?,?)');
$label->execute([1,'Cafe']);$duplicateCode=null;$duplicateState=null;
try{$label->execute([2,'café']);}catch(PDOException $e){
$duplicateCode=(int)($e->errorInfo[1]??0);$duplicateState=(string)$e->getCode();
if($duplicateCode!==1062 || $duplicateState!=='23000'){throw $e;}
}
$code=$db->prepare('INSERT INTO mysql111_text_codes (id,code) VALUES (?,?)');
$code->execute([1,'Ab-7']);$code->execute([2,'ab-7']);
$labels=tc111Rows($db,'SELECT id,label FROM mysql111_text_labels ORDER BY id');
$codes=tc111Rows($db,'SELECT id,code FROM mysql111_text_codes ORDER BY id');
$rows=[['Название, ai_ci','Cafe','Сохранено',1],['Название, ai_ci','café','Отклонено: дубликат 1062',count($labels)],['Внешний код, as_cs','Ab-7','Сохранено',1],['Внешний код, as_cs','ab-7','Сохранено',count($codes)]];
$checks=['expected_duplicate'=>$duplicateCode===1062 && $duplicateState==='23000','first_label_preserved'=>count($labels)===1 && $labels[0]['label']==='Cafe','two_codes_preserved'=>array_column($codes,'code')===['Ab-7','ab-7']];
$details=['duplicate_sqlstate'=>$duplicateState,'duplicate_vendor_code'=>$duplicateCode,'stored_labels'=>$labels,'stored_codes'=>$codes];
$db=null;
tc111Report('mysql111-text-unique-policy', ['Договор поля', 'Вход', 'Итог записи', 'Строк после операции'], $rows, $checks, $runtime, $details);
echo 'mysql111-text-unique-policy: verified result.html and result.json
';
Ожидаемый результат
mysql111-text-unique-policy: verified result.html and result.json
Откройте result.html и сравните конкретные строки с таблицей выше. Архив содержит helper и README для самостоятельного запуска на локальном MySQL.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Повторите в таблице code вставку Ab-7 под новым числовым id. Теперь будет дубликат 1062: регистрозависимость разрешает ab-7, но не вторую точно такую же строку Ab-7.
Практический разбор: Внешние коды отличаются регистром, но UNIQUE отклоняет запись. Разработчик интеграции получает разные идентификаторы с различием регистра; существующая collation считает их повтором. Это источник ситуации; технические правила сверены с официальной документацией, программа написана отдельно.
Найти невидимый пробел: равенство и LIKE имеют разные правила
В импорте есть два значения: AB7 и AB7 с пробелом в конце. На экране они выглядят одинаково. Нужно понять, почему = и LIKE или старое и новое правило сравнения находят разное число записей.
Исходные данные
| ID | Хранимое значение | HEX | Байт |
|---|---|---|---|
| 1 | AB7 | 414237 | 3 |
| 2 | AB7␠ (␠ обозначает пробел) | 41423720 | 4 |
- VARCHAR сохраняет AB7 и AB7 + пробел как два значения
- NO PAD учитывает хвостовой пробел при = → ID 1
- PAD SPACE игнорирует его при = → ID 1,2
- LIKE без% всё равно учитывает хвостовой пробел → ID 1
Что получится
| Правило пробелов | Оператор | Искомое значение | Найденные ID |
|---|---|---|---|
| NO PAD | = | AB7 | 1 |
| NO PAD | LIKE | AB7 | 1 |
| PAD SPACE | = | AB7 | 1, 2 |
| PAD SPACE | LIKE | AB7 | 1 |
Минимальная запись по задаче
SELECT id, code, LENGTH(code), HEX(code)
FROM mysql111_text_spaces
WHERE code COLLATE utf8mb4_0900_ai_ci = 'AB7'
ORDER BY id;Этот фрагмент объясняет нужное действие на учебных таблицах. Полная программа ниже создаёт исходные данные сама; отдельно выполнять предыдущие примеры не нужно.
Атрибут PAD SPACE означает, что завершающие пробелы не влияют на обычное сравнение текстовых строк. NO PAD учитывает их. В демонстрации utf8mb4_unicode_ci имеет PAD SPACE, а utf8mb4_0900_ai_ci — NO PAD. Фактические атрибуты обеих collations программа также читает из INFORMATION_SCHEMA.COLLATIONS и сохраняет в metrics.json.
LIKE — сопоставление с шаблоном; у него собственное правило для завершающих пробелов, независимо от PAD_ATTRIBUTE. Поэтому нельзя считать LIKE без знака % полной заменой =. Само наличие пробела видно через LENGTH и HEX: последний байт 20 у второй записи — обычный пробел. Для объяснения на странице он обозначен ␠, но в базе хранится именно пробел, не этот графический символ.
Здесь сознательно используется VARCHAR. CHAR имеет дополнительные правила дополнения и чтения завершающих пробелов, которые смешали бы две разные причины. Все сравнения выполняются над одними исходными значениями; никакого UPDATE или автоматического исправления данных нет. Если запись не находится, сначала посмотрите фактические байты, тип столбца и collation, а затем решайте, нарушены ли правила импорта.
Границы и частые ошибки
Старые ответы в интернете о том, что MySQL всегда игнорирует хвостовые пробелы при =, нельзя переносить наcollations 0900. Пример касается обычного пробела U+0020, не всех невидимых символов. COLLATE меняет правило сравнения, но не удаляет символ из сохранённого значения. SQL TRIM и PHP trim решают отдельную задачу подготовки данных.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/tc111-helper.php';
$db=tc111Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),'server'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION)];
$details=[];
$db->exec('CREATE TEMPORARY TABLE mysql111_text_spaces (id INT PRIMARY KEY,code VARCHAR(20) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci');
$db->exec("INSERT INTO mysql111_text_spaces VALUES (1,'AB7'),(2,'AB7 ')");
$rows=[];$ids=[];
foreach(['NO PAD'=>'utf8mb4_0900_ai_ci','PAD SPACE'=>'utf8mb4_unicode_ci'] as $label=>$collation){
foreach(['=','LIKE'] as $operator){
// Operator and collation are fixed author-controlled SQL tokens.
$found=tc111Rows($db,'SELECT id FROM mysql111_text_spaces WHERE code COLLATE '.$collation.' '.$operator.' ? ORDER BY id',['AB7']);
$values=array_map('intval',array_column($found,'id'));$ids[$label][$operator]=$values;
$rows[]=[$label,$operator,'AB7',implode(', ',$values)];
}
}
$stored=tc111Rows($db,'SELECT id,LENGTH(code) AS byte_count,HEX(code) AS hexadecimal FROM mysql111_text_spaces ORDER BY id');
$checks=['no_pad_equal_only_exact'=>$ids['NO PAD']['=']===[1],'pad_equal_ignores_last_space'=>$ids['PAD SPACE']['=']===[1,2],'like_keeps_last_space'=>$ids['NO PAD']['LIKE']===[1] && $ids['PAD SPACE']['LIKE']===[1],'varchar_kept_space'=>(int)$stored[1]['byte_count']===4 && $stored[1]['hexadecimal']==='41423720'];
$details=['saved_bytes'=>$stored,'collations'=>tc111Rows($db,"SELECT COLLATION_NAME,PAD_ATTRIBUTE FROM INFORMATION_SCHEMA.COLLATIONS WHERE COLLATION_NAME IN ('utf8mb4_0900_ai_ci','utf8mb4_unicode_ci') ORDER BY COLLATION_NAME")];
$db=null;
tc111Report('mysql111-text-trailing-spaces', ['Правило пробелов', 'Оператор', 'Искомое значение', 'Найденные ID'], $rows, $checks, $runtime, $details);
echo 'mysql111-text-trailing-spaces: verified result.html and result.json
';
Ожидаемый результат
mysql111-text-trailing-spaces: verified result.html and result.json
Откройте result.html и сравните конкретные строки с таблицей выше. Архив содержит helper и README для самостоятельного запуска на локальном MySQL.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Добавьте ID 3 со значением AB7 и двумя пробелами. NO PAD с = оставит ID 1, PAD SPACE с = найдёт1,2,3, оба показанных LIKE найдут толькоID1. HEX третьего значения закончится 2020.
Практический разбор: Почему LIKE без процентов не всегда заменяет равенство. Автор хочет использовать один поиск для простого текста и шаблона; новое объяснение отдельно проверяет современные NO PAD и старые PAD SPACE правила. Это источник ситуации; технические правила сверены с официальной документацией, программа написана отдельно.
Показать три товара: начало названия или подстрока
Каталог должен показать максимум три опубликованных товара. Пользователь выбирает «начинается с» или «содержит». Подарочный чай обязан попадать только во второй режим, а неопубликованный Чай мята — ни в один.
Исходные данные
| ID | Название | Опубликован |
|---|---|---|
| 1 | Подарочный чай | Да |
| 2 | Чай зелёный | Да |
| 3 | Чай чёрный | Да |
| 4 | Чайник | Да |
| 5 | Чай мята | Нет |
| 6 | Кофе | Да |
- PHP готовит шаблон чай% или%чай%
- WHERE active=1 оставляет опубликованные товары
- LIKE применяет выбранный смысл поиска
- ORDER BY id и LIMIT 3 возвращают готовую страницу из трёх строк
Что получится
| Режим | Шаблон | ID | Название |
|---|---|---|---|
| Начинается с | чай% | 2 | Чай зелёный |
| Начинается с | чай% | 3 | Чай чёрный |
| Начинается с | чай% | 4 | Чайник |
| Содержит | %чай% | 1 | Подарочный чай |
| Содержит | %чай% | 2 | Чай зелёный |
| Содержит | %чай% | 3 | Чай чёрный |
Минимальная запись по задаче
SELECT id, name
FROM mysql111_text_catalog
WHERE active = 1 AND name LIKE ? ESCAPE '!'
ORDER BY id
LIMIT 3;
-- Передать чай% либо %чай% одним параметром.Этот фрагмент объясняет нужное действие на учебных таблицах. Полная программа ниже создаёт исходные данные сама; отдельно выполнять предыдущие примеры не нужно.
Знак% означает любую последовательность символов, в том числе пустую. У шаблона чай% фиксировано начало, у%чай% — только наличие подстроки. Поэтому Чайник подходит первому шаблону: это поиск по символам, а не по словарным словам. Регистр здесь игнорирует collation столбца. Третий результат во втором режиме отличается, потому что сначала найден более ранний ID 1, а размер страницы остаётся три.
И active, и LIKE, и порядок, и ограничение выполняет MySQL. PHP получает уже нужные id/name; здесь нет SELECT * всего каталога, array_filter, usort и обрезки огромного массива. ORDER BY id задан ради простого устойчивого договора примера. В реальном каталоге можно согласовать другой порядок, сохраняя уникальный ключ для разрешения равенств.
Шаблон не собирается внутри SQL-кавычек: он передаётся целиком одним параметром. Обработка буквальных%, _ и! уже разобрана в подготовленных фильтрах; здесь тот же шаг сохраняет смысл пользовательского текста перед добавлением служебных%.
Для начала строки обычный B-tree индекс может дать диапазон поиска, для ведущего% этот конкретный способ построения диапазона недоступен. Это не обещание, что любой другой доступ тоже невозможен. Пример создаёт KEY(active,name) и сохраняет настоящие EXPLAIN обоих запросов. На шести строках оптимизатор вправе выбрать обход; сортировка по id также отдельная часть плана. Нельзя доказать производительность счётом возвращённых строк или заранее написать «индекс обязательно использован».
Границы и частые ошибки
LIMIT ограничивает выдачу PHP, а не гарантирует чтение сервером ровно трёх записей. Поиск подстроки в длинных описаниях — другая нагрузка; для поиска слов пригоден отдельный FULLTEXT-сценарий. Эта программа проверяет точные результаты и границу страницы, не измеряет скорость больших каталогов. Для следующей страницы нужен согласованный курсор/порядок, а не произвольный повтор LIMIT.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/tc111-helper.php';
$db=tc111Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),'server'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION)];
$details=[];
$db->exec('CREATE TEMPORARY TABLE mysql111_text_catalog (id INT PRIMARY KEY,name VARCHAR(80) NOT NULL,active TINYINT NOT NULL,KEY(active,name)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci');
$db->exec("INSERT INTO mysql111_text_catalog VALUES (1,'Подарочный чай',1),(2,'Чай зелёный',1),(3,'Чай чёрный',1),(4,'Чайник',1),(5,'Чай мята',0),(6,'Кофе',1)");
$term='чай';$literal=strtr($term,['!'=>'!!','%'=>'!%','_'=>'!_']);
$sql="SELECT id,name FROM mysql111_text_catalog WHERE active=1 AND name LIKE ? ESCAPE '!' ORDER BY id LIMIT 3";
$rows=[];$sets=[];$plans=[];
foreach(['Начинается с'=>$literal.'%','Содержит'=>'%'.$literal.'%'] as $mode=>$pattern){
$found=tc111Rows($db,$sql,[$pattern]);$sets[$mode]=array_map('intval',array_column($found,'id'));
foreach($found as $r){$rows[]=[$mode,$pattern,(int)$r['id'],$r['name']];}
$plans[$mode]=tc111Rows($db,'EXPLAIN '.$sql,[$pattern]);
}
$checks=['prefix_page'=>$sets['Начинается с']===[2,3,4],'contains_page'=>$sets['Содержит']===[1,2,3],'bounded_sql_page'=>count($sets['Начинается с'])===3 && count($sets['Содержит'])===3,'unpublished_excluded'=>!in_array(5,array_merge(...array_values($sets)),true)];
$details=['SQL'=>$sql,'plans'=>$plans,'sort_contract'=>'id ASC','page_limit'=>3,'latency_claim'=>null];
$db=null;
tc111Report('mysql111-text-prefix-page', ['Режим', 'Шаблон', 'ID', 'Название'], $rows, $checks, $runtime, $details);
echo 'mysql111-text-prefix-page: verified result.html and result.json
';
Ожидаемый результат
mysql111-text-prefix-page: verified result.html and result.json
Откройте result.html и сравните конкретные строки с таблицей выше. Архив содержит helper и README для самостоятельного запуска на локальном MySQL.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Сделайте LIMIT 2. Для начала названия получатся ID 2,3, для подстроки — ID 1,2. Затем поменяйте active у ID 5 на1: он всё равно не попадёт на первую страницу по id, пока перед ним достаточно подходящих записей.
Обработать пустой поиск, строку 0 и скопированный пробел
Поиск должен различать отсутствие запроса и настоящий текст0. После вставки из документа рядом с Чай может оказаться неразрывный пробел. Его нельзя принять за обычный пробел только потому, что они одинаково выглядят.
Исходные данные
| Вход | Намерение |
|---|---|
| ␠чай␠ | Снять обычные края строки |
| ␠␠␠ | Не выполнять поиск пустого значения |
| 0 | Найти реально существующее название 0 |
| NBSP + Чай + NBSP | Показать границу обычного trim |
- Приложение получает отдельную строку ввода
- trim удаляет свой набор краевых ASCII-символов
- Проверка === пустой строке пропускает только пустой запрос
- Непустое значение передаётся в WHERE name=?; collation сравнивает его с данными
Что получится
| Вход | После trim | Действие | Найденные ID |
|---|---|---|---|
| Обычные пробелы | чай | Выполнен один SELECT | 1 |
| Только пробелы | пусто | Запрос пропущен | нет |
| Строка ноль | 0 | Выполнен один SELECT | 3 |
| Неразрывные пробелы | NBSP + Чай + NBSP | Выполнен один SELECT | нет |
Минимальная запись по задаче
$term = trim($raw);
if ($term === '') {
// Покажите приглашение ввести запрос; к базе не обращаемся.
} else {
$statement = $db->prepare(
'SELECT id,name FROM mysql111_text_input WHERE name=? ORDER BY id LIMIT 3'
);
$statement->execute([$term]);
}Этот фрагмент объясняет нужное действие на учебных таблицах. Полная программа ниже создаёт исходные данные сама; отдельно выполнять предыдущие примеры не нужно.
Ввод, подготовка строки и сравнение в базе — три последовательных шага. trim($raw) возвращает новую строку; он не изменяет таблицу. Проверка $term === '' означает точное равенство пустой строке. empty($term) здесь был бы неверным выбором: строка 0 в PHP считается пустой, хотя каталог содержит отдельную запись с таким названием.
Обычный trim не удаляет неразрывный пробел U+00A0. Запись \u{00A0} в двойных PHP-кавычках создаёт этот символ. В отчёте он специально подписан NBSP, чтобы причина промаха была видна. Это не ошибка PDO и не проблема параметризации: приложение действительно передало строку с дополнительными символами. Политика удаления пробелов должна быть названа, а не угадываться по внешнему виду.
Если у формы по требованиям допустимы краевые Unicode-пробелы, можно отдельно выбрать mb_trim в PHP 8.4+ с расширением mbstring. Не передавайте многобайтный NBSP вторым аргументом обычного trim: тот работает с отдельными байтами набора. Правила нормализации строк нужно применять согласованно при вводе и поиске, сохраняя исходное значение там, где оно важно для аудита или интеграции.
Границы и частые ошибки
Демонстрация намеренно сохраняет ограниченную политику trim и показывает её предел, а не обещает универсальную очистку любого Unicode. Она не переводит HTML-текст в символ NBSP и не приводит Unicode к NFC. Значение берётся из фиксированной строки; реальная HTTP-форма дополнительно проверяет, что пришла строка, разрешённую длину и правила доступа. Пустой запрос не превращается в LIKE %% и не выгружает весь каталог.
Полная самостоятельная программа и файлы
<?php
declare(strict_types=1);
require __DIR__.'/tc111-helper.php';
$db=tc111Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->getAttribute(PDO::ATTR_CLIENT_VERSION),'server'=>$db->getAttribute(PDO::ATTR_SERVER_VERSION)];
$details=[];
$db->exec('CREATE TEMPORARY TABLE mysql111_text_input (id INT PRIMARY KEY,name VARCHAR(30) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci');
$db->exec("INSERT INTO mysql111_text_input VALUES (1,'Чай'),(2,'Кофе'),(3,'0')");
$inputs=[['Обычные пробелы',' чай '],['Только пробелы',' '],['Строка ноль','0'],['Неразрывные пробелы',"\u{00A0}Чай\u{00A0}"]];
$rows=[];$queried=[];
foreach($inputs as [$label,$raw]){
$term=trim($raw);
if($term===''){$rows[]=[$label,'пусто','Запрос пропущен','нет'];continue;}
$found=tc111Rows($db,'SELECT id,name FROM mysql111_text_input WHERE name=? ORDER BY id LIMIT 3',[$term]);
$ids=array_map('intval',array_column($found,'id'));$queried[$label]=$ids;
$display=$label==='Неразрывные пробелы'?'NBSP + Чай + NBSP':$term;
$rows[]=[$label,$display,'Выполнен один SELECT',$ids?implode(', ',$ids):'нет'];
}
$checks=['ordinary_spaces_trimmed'=>$queried['Обычные пробелы']===[1],'empty_query_skipped'=>count($queried)===3 && !isset($queried['Только пробелы']),'zero_is_valid'=>$queried['Строка ноль']===[3],'nbsp_not_trimmed'=>trim("\u{00A0}Чай\u{00A0}") === "\u{00A0}Чай\u{00A0}" && $queried['Неразрывные пробелы']===[]];
$details=['actual_data_queries'=>count($queried),'normalizer'=>'PHP trim default ASCII whitespace set','equality_collation'=>'utf8mb4_0900_ai_ci'];
$db=null;
tc111Report('mysql111-text-input-policy', ['Вход', 'После trim', 'Действие', 'Найденные ID'], $rows, $checks, $runtime, $details);
echo 'mysql111-text-input-policy: verified result.html and result.json
';
Ожидаемый результат
mysql111-text-input-policy: verified result.html and result.json
Откройте result.html и сравните конкретные строки с таблицей выше. Архив содержит helper и README для самостоятельного запуска на локальном MySQL.
Открыть фактический HTML-результат · Скачать результат JSON
Попробуйте сами. Добавьте ещё ввод с одной табуляцией до и после слова Чай: обычный trim уберёт её, поиск найдётID1. Для NBSP сначала сформулируйте разрешённую нормализацию; после осознанного перехода на Unicode-обрезку ожидание последнего случая станетID1.
Практический разбор: Почему trim оставляет неразрывный пробел. Разбор показывает отличие HTML-сущности, Unicode-символа и байтового набора trim; наш пример показывает промах поиска на собственных данных. Это источник ситуации; технические правила сверены с официальной документацией, программа написана отдельно.
Дальше: искать слова и управлять выдачей
Когда нужно искать слова в статьях и инструкциях, одного LIKE часто недостаточно: переходите к полнотекстовому поиску MySQL. Для большой выдачи отдельно задайте SQL под нужный экран и порядок и курсор страницы. Ошибки соединения и SQL разбираются в диагностике PDO.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


