string в PHP — это байты, а не «текст вообще»
В PHP строка — это последовательность байтов. У неё нет встроенной метки «это UTF-8», «это Windows-1251» или «это бинарный файл». Один и тот же тип string хранит имя пользователя, JSON, HTML-фрагмент, содержимое CSV, бинарные данные из файла и тело HTTP-запроса. Поэтому строковый код в PHP почти всегда должен отвечать на два вопроса: «я работаю с байтами или с символами?» и «в какой кодировке эти байты надо понимать?»
<?php
$text = 'Привет';
echo strlen($text) . PHP_EOL; // 12 байтов в UTF-8
echo mb_strlen($text, 'UTF-8') . PHP_EOL; // 6 символовstrlen() не «ошибается»: она делает ровно то, что обещает, — считает байты. Для сетевых протоколов, бинарных файлов, хэшей и лимитов в байтах это правильная функция. Для пользовательского текста на русском, казахском, японском или с эмодзи чаще нужны функции mb_*.
flowchart LR
A[Байты в string] --> B{Какой контекст?}
B --> C[Бинарные данные<br/>strlen/substr допустимы]
B --> D[Пользовательский текст UTF-8<br/>mb_strlen/mb_substr]
B --> E[HTML-вывод<br/>htmlspecialchars]
B --> F[JSON<br/>json_encode/json_decode]
D --> G[Явная кодировка: UTF-8]
E --> G
F --> GЛитералы, кавычки и interpolation
Строковые литералы в PHP можно записывать несколькими способами. Одинарные кавычки почти ничего не интерпретируют: переменные не подставляются, \n остаётся двумя символами — обратным слэшем и n.
<?php
$name = 'Анна';
echo 'Привет, $name\n';
// Привет, $name\nДвойные кавычки интерпретируют escape-последовательности и подставляют переменные.
<?php
$name = 'Анна';
$count = 3;
echo "Привет, $name\n";
echo "Новых сообщений: {$count}\n";Фигурные скобки в interpolation лучше использовать, когда выражение стоит рядом с буквами или индексами массива. Это убирает неоднозначность и хорошо читается рядом со структурами из Массивы как ordered map.
<?php
$user = ['name' => 'Анна'];
echo "Профиль: {$user['name']}";Для больших многострочных строк есть heredoc и nowdoc. Heredoc ведёт себя как двойные кавычки: переменные подставляются. Nowdoc ведёт себя как одинарные кавычки: содержимое почти буквально.
<?php
$title = 'Отчёт';
$html = <<<HTML
<h1>{$title}</h1>
<p>Строка с подстановкой.</p>
HTML;
$template = <<<'TPL'
<h1>{$title}</h1>
<p>Строка без подстановки.</p>
TPL;Практическое правило: heredoc удобен для читаемых шаблонных фрагментов внутри PHP-кода, nowdoc — для примеров кода, SQL-шаблонов, регулярных выражений и текста, где $ не должен внезапно стать переменной. Но HTML для пользователя всё равно надо экранировать; см. XSS, экранирование вывода и шаблоны.
Байтовые функции и UTF-8
Многие стандартные строковые функции исторически работают с байтами: strlen(), substr(), strpos(), strtolower(), strtoupper() и часть функций семейства str_*. На ASCII-тексте это незаметно, потому что один символ обычно занимает один байт. На UTF-8 кириллице один символ обычно занимает два байта, а некоторые символы — больше.
<?php
$text = 'Книга';
echo substr($text, 0, 2) . PHP_EOL; // может вывести только «К»
echo mb_substr($text, 0, 2, 'UTF-8') . PHP_EOL; // «Кн»Если разрезать UTF-8 строку посередине многобайтного символа, получится битая строка. Она может странно отображаться, не пройти JSON-кодирование или испортить данные при записи в файл. Это особенно часто всплывает в коротких превью: «обрезать заголовок до 120 символов» через substr() — баг для русского интерфейса.
Для пользовательского текста обычно выбирают такие пары:
<?php
mb_strlen($text, 'UTF-8');
mb_substr($text, 0, 120, 'UTF-8');
mb_strtolower($text, 'UTF-8');
mb_strtoupper($text, 'UTF-8');
mb_strpos($text, 'искать', 0, 'UTF-8');Есть тонкость: mb_strlen() считает символы в выбранной кодировке, но не всегда совпадает с тем, что человек считает «одним видимым знаком». Например, буква с комбинируемым диакритическим знаком или некоторые эмодзи могут состоять из нескольких code points. Для таких случаев в PHP есть функции grapheme_* из расширения intl, но в обычном CRUD-коде mb_* уже закрывает главный класс ошибок с кириллицей.
mbstring: не магия, а явный контракт
Расширение mbstring даёт функции для многобайтных кодировок и конвертации между кодировками. В современном веб-приложении разумный дефолт — UTF-8 на всех границах: исходники, HTML, JSON, база, файлы импорта, HTTP-заголовки.
<?php
mb_internal_encoding('UTF-8');
header('Content-Type: text/html; charset=UTF-8');mb_internal_encoding() задаёт внутреннюю кодировку по умолчанию для функций mbstring, но это не превращает любые входящие байты в UTF-8. Если файл пришёл из старой бухгалтерской системы в Windows-1251, его надо конвертировать явно.
<?php
$raw = file_get_contents('clients.csv');
$utf8 = mb_convert_encoding($raw, 'UTF-8', 'Windows-1251');Не стоит слепо полагаться на autodetect кодировки: короткие строки и похожие байтовые диапазоны легко дают неправильный результат. Если источник известен, передавайте исходную кодировку явно. Работа с файлами и file_get_contents() подробнее разбирается в Файловая система и stream wrappers.
Строки на границах системы
Самые неприятные строковые баги возникают не внутри маленькой функции, а на границах: HTTP-ввод, база, файл, JSON, HTML-вывод.
Входные данные надо валидировать как данные, а не «экранировать на входе». Например, имя пользователя можно проверить на длину через mb_strlen(), нормализовать пробелы, запретить управляющие символы. Это связано с GET, POST и фильтрация ввода.
<?php
$name = trim($_POST['name'] ?? '');
if ($name === '' || mb_strlen($name, 'UTF-8') > 80) {
throw new InvalidArgumentException('Некорректное имя');
}Вывод в HTML — отдельная операция. Для HTML-текста обычно используют htmlspecialchars() с явной кодировкой и флагами по умолчанию современных PHP-версий.
<?php
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');Для JSON не собирайте строку руками через конкатенацию: используйте json_encode() и проверяйте ошибки или включайте исключения. Это уже тема JSON, сериализация и форматы данных. Для HTTP-заголовков важно помнить, что заголовок Content-Type и фактические байты ответа должны совпадать; см. HTTP-заголовки, ответы и редиректы.
Частые ловушки
Первая ловушка — считать strlen() длиной текста. Это длина в байтах. Для лимита «не больше 80 символов» используйте mb_strlen() с явной кодировкой.
Вторая — обрезать UTF-8 через substr(). Для превью, заголовков и имён используйте mb_substr().
Третья — думать, что mb_internal_encoding('UTF-8') чинит входные файлы. Она задаёт дефолт для mbstring, но не угадывает происхождение байтов.
Четвёртая — смешивать в проекте UTF-8, Windows-1251 и «как получилось». Лучше один раз договориться: всё внутреннее хранение — UTF-8, конвертация только на входе из легаси-источников и на выходе в чужие системы.
Пятая — путать экранирование и кодировку. UTF-8 отвечает на вопрос «как символы представлены байтами». Экранирование отвечает на вопрос «как безопасно вставить строку в HTML, SQL, JSON, URL или shell». Для SQL это ведёт к Prepared statements и SQL injection, для HTML — к XSS, экранирование вывода и шаблоны.
См. также
- Массивы как ordered map — строковые ключи, interpolation с массивами и структура данных вокруг текста.
- JSON, сериализация и форматы данных — UTF-8, ошибки кодирования и безопасная сериализация.
- Файловая система и stream wrappers — чтение файлов,
php://inputи байтовые потоки. - GET, POST и фильтрация ввода — проверка пользовательских строк на границе HTTP.
- HTTP-заголовки, ответы и редиректы —
Content-Type, charset и порядок отправки заголовков. - XSS, экранирование вывода и шаблоны — контекстное экранирование HTML, атрибутов и JavaScript.
- Ошибки, исключения и Throwable — как обрабатывать
ValueError, исключения JSON и ошибки на границах данных.