Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 116 · После первого SELECT и подготовленных параметров

MySQL: кодировка, правила сравнения и понятный текстовый поиск

Почему cafe находит Café, когда UNIQUE считает коды одинаковыми, откуда берутся невидимые пробелы и как вернуть только нужную страницу товаров.

Сначала определить, что означает совпадение

Покупатель вводит cafe и хочет увидеть Café. Интеграция присылает Ab-7 и ab-7 как разные коды. Каталог должен найти только опубликованные товары и вернуть страницу, которую человек сейчас видит. Во всех трёх задачах недостаточно просто написать WHERE name = ?: сначала требуется договор о том, что считается совпадением.

Договор текстового поля
  1. Сохранить исходный текст без потерь: utf8mb4
  2. Определить равенство и порядок: collation
  3. Выбрать условие: точное значение, начало, подстрока или слова
  4. Отфильтровать и ограничить результат на стороне 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Чай
2Cafe
3Café
4Чай 😀
Путь текста и сравнения
  1. PHP передаёт строку Чай 😀 параметром INSERT
  2. Соединение charset=utf8mb4 сообщает кодировку текста
  3. VARCHAR с utf8mb4 сохраняет все пять символов
  4. SELECT возвращает Чай 😀: 5 символов и 11 байт

Что получится

IDСохранённое названиеСимволов MySQLБайт MySQLБайт PHP
1Чай366
2Cafe444
3Café455
4Чай 😀51111

Минимальная запись по задаче

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.

Полная самостоятельная программа и файлы
Сохранить кириллицу и emoji: символы не равны байтамСамостоятельный пример
<?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Название
1Café
2CAFE
3cafe
4Чай
5чай
Путь текста и сравнения
  1. Поисковый параметр cafe
  2. ai_ci игнорирует акцент и регистр → ID 1,2,3
  3. as_cs учитывает акцент и регистр → ID 3
  4. В обеих выдачах база возвращает исходное написание

Что получится

ИщемПравилоНайденный IDНаписание в базе
cafeai_ci1Café
cafeai_ci2CAFE
cafeai_ci3cafe
cafeas_cs3cafe
чайai_ci4Чай
чайai_ci5чай
чайas_cs5чай

Минимальная запись по задаче

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 в запросе также может изменить доступность индексного пути: совместимость с индексом и фактический план нужно смотреть отдельно.

Полная самостоятельная программа и файлы
Выбрать, должны ли cafe, Café и CAFE совпадатьСамостоятельный пример
<?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Эти два написания поставщик считает разными
Путь текста и сравнения
  1. Cafe попадает в поле label с ai_ci и UNIQUE
  2. café сравнивается с Cafe по тому же ai_ci → конфликт 1062
  3. Ab-7 и ab-7 попадают в code с as_cs
  4. Для этого поля два кода различаются → сохранены обе строки

Что получится

Договор поляВходИтог записиСтрок после операции
Название, ai_ciCafeСохранено1
Название, ai_cicaféОтклонено: дубликат 10621
Внешний код, as_csAb-7Сохранено1
Внешний код, as_csab-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 не превращает текстовый код в двоичный токен. Автоматически обрезать пробелы или менять регистр кодов можно только по согласованному договору.

Полная самостоятельная программа и файлы
Согласовать UNIQUE с правилами имени и внешнего кодаСамостоятельный пример
<?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Байт
1AB74142373
2AB7␠ (␠ обозначает пробел)414237204
Путь текста и сравнения
  1. VARCHAR сохраняет AB7 и AB7 + пробел как два значения
  2. NO PAD учитывает хвостовой пробел при = → ID 1
  3. PAD SPACE игнорирует его при = → ID 1,2
  4. LIKE без% всё равно учитывает хвостовой пробел → ID 1

Что получится

Правило пробеловОператорИскомое значениеНайденные ID
NO PAD=AB71
NO PADLIKEAB71
PAD SPACE=AB71, 2
PAD SPACELIKEAB71

Минимальная запись по задаче

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 решают отдельную задачу подготовки данных.

Полная самостоятельная программа и файлы
Найти невидимый пробел: равенство и 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_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КофеДа
Путь текста и сравнения
  1. PHP готовит шаблон чай% или%чай%
  2. WHERE active=1 оставляет опубликованные товары
  3. LIKE применяет выбранный смысл поиска
  4. 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
Путь текста и сравнения
  1. Приложение получает отдельную строку ввода
  2. trim удаляет свой набор краевых ASCII-символов
  3. Проверка === пустой строке пропускает только пустой запрос
  4. Непустое значение передаётся в WHERE name=?; collation сравнивает его с данными

Что получится

ВходПосле trimДействиеНайденные ID
Обычные пробелычайВыполнен один SELECT1
Только пробелыпустоЗапрос пропущеннет
Строка ноль0Выполнен один SELECT3
Неразрывные пробелы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; в символ NBSP и не приводит Unicode к NFC. Значение берётся из фиксированной строки; реальная HTTP-форма дополнительно проверяет, что пришла строка, разрешённую длину и правила доступа. Пустой запрос не превращается в LIKE %% и не выгружает весь каталог.

Полная самостоятельная программа и файлы
Обработать пустой поиск, строку 0 и скопированный пробелСамостоятельный пример
<?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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.