Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Кодирование Base64 в PHP: полное руководство

У вас есть данные, которым нужно пережить канал, который их не любит. Двоичный blob, которому предстоит жить в поле JSON. Изображение, которое должно уместиться внутри HTML-тега. Сертификат, которому место в конфигурационном файле. Токен, который будет путешествовать через URL, заголовки и cookie. Это будни Base64-кодирования: оно переписывает каждые три байта сырых данных в четыре символа из 64-буквенного алфавита, а хвост дописывают один-два знака =, так что на выходе получается обычный текст, который может перенести что угодно. Домашняя страница этого сайта полностью объясняет формат, поэтому статья сосредоточена на том, что PHP вам даёт, что решает за вас молча и где подстерегают ловушки.

На стороне PHP история начинается с хорошей новости: base64_encode() живёт в ядре с PHP 4, принимает один аргумент, всегда возвращает строку и не может завершиться ошибкой. Нет ни строгого режима, ни пути ошибки, ни конфигурации. Кодирование детерминировано: одни и те же байты всегда дают одни и те же буквы. Ваша работа как разработчика - не заставить функцию работать, а заставить окружающий мир себя слушаться: выбрать правильный алфавит для пункта назначения, добавить нужные переводы строк, перекодировать в нужную кодировку и нести счёт за размер в 33 процента с открытыми глазами. (Base64 обычно раздувает данные примерно на треть, четыре символа на три входных байта; подержите это в уме, потому что оно будет возвращаться снова и снова.)

К концу этой статьи вы будете знать, как производить каждый вид Base64, с которым реально сталкивается PHP-разработчик: однострочный вывод, почта с MIME-обёрткой, ключи с PEM-бронёй, URL-безопасные токены и data URI, плюс поточные приёмы на случай, когда данных слишком много, чтобы держать их в памяти.

Одна функция, ноль опций

Весь API, в точности как его сообщает современный PHP:

base64_encode(string $string): string

Перечитайте это ещё раз. Один параметр, одно возвращаемое значение, никаких флагов. Мануал описывает его как MIME base64, «созданное, чтобы двоичные данные выживали при транспортировке через транспортные слои, которые не являются 8-битно чистыми, такие как тела писем». Обратите внимание, чего это формулирование не обещает: ни переводов строк, ни обёртки, ни мнения о том, где будет жить вывод. Функция выдаёт одну длинную строку, и какая бы обёртка ни требовалась пункту назначения, это ваша работа вторым вызовом. Начиная с PHP 8.0, сигнатура несёт нативные типы; начиная с PHP 8.1, передача null вызывает уведомление об устаревании, так что сначала приводите любое значение, допускающее null, к ''.

Размер вывода следует фиксированному шаблону, который можно предсказать до вызова:

Входные байты Выходные символы Заполнения
0 0 нет
1 4 два =
2 4 одно =
3 4 нет
3,000,000 4,000,000 нет
100,000 133,336 два =

Шаблон - четыре символа на каждую полную группу из трёх байтов плюс последняя неполная группа, дополненная одним-двумя знаками =. Последствие, о котором стоит знать: и один байт, и три байта дают четыре символа, поэтому закодированная длина скрывает точный размер входа. Его можно оценить (разделить на четыре, умножить на три, вычесть заполнения), но прочитать точно - нельзя.

Куда ставить переводы строк

Поскольку base64_encode() сама по себе никогда не переносит строки, решение об обёртке - это проблема пункта назначения. На практике существуют три ответа.

Без переводов строк. Сырой вывод функции, ровно одна строка. Именно это нужно для URL, JSON-блоков, заголовков, значений в базе данных и всего остального, где перевод строки был бы багом. Именно это чаще всего имеют в виду, говоря «просто дай мне Base64».

MIME-обёртка: 76 символов плюс CRLF. Почтовая конвенция из RFC 2045, раздел 6.8: закодированные строки не должны превышать 76 символов, и декодеры должны игнорировать переводы строк. Классический компаньон - chunk_split(), который мануал ставит рядом с base64_encode() в списке «See Also» именно по этой причине:

$binary = file_get_contents('/var/www/uploads/report.pdf');
$wrapped = chunk_split(base64_encode($binary), 76, "\r\n");

PEM-броня: 64 символа плюс LF. Ключи и сертификаты используют более старую конвенцию Privacy-Enhanced Mail (RFC 1421): более короткие строки по 64 символа. Тот же инструмент, другие числа:

$der = file_get_contents('/etc/ssl/raw-key.der');
$armor = "-----BEGIN PRIVATE KEY-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END PRIVATE KEY-----\n";

Одна особенность chunk_split() касается обоих обёрнутых видов: функция дописывает разделитель в конец результата даже тогда, когда длина входа кратна длине строки в точности. Если потребитель ниже по цепочке спотыкается о завершающую пустую строку, вот в чём причина; один вызов rtrim() с разделителем это чинит. Обратите также внимание на асимметрию, которая когда-нибудь вас спасёт: декодеры полностью игнорируют переводы строк, поэтому MIME-обёрнутый блок и необёрнутый декодируются в одни и те же байты. Обёртка - это вежливость по отношению к строковым инструментам и людям, а не семантическая разница.

Как сделать вывод безопасным для URL

Стандартный алфавит содержит + и /, и за пределами текстового файла оба приносят беду. + в form-кодированной строке запроса превращается в пробел до того, как ваше приложение его увидит, а / в URL - разделитель пути. У имён файлов и токенов есть свои жалобы. RFC 4648, раздел 5, решает это алфавитом, безопасным для URL и имён файлов: + становится -, / становится _, а хвостовые заполнения = обычно убираются. RFC настаивает, что это «не следует считать тем же самым, что и кодирование base64», так что обращайтесь с этим как с отдельным форматом, обычно называемым base64url.

Произвести его - дело двух строковых операций:

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
var_dump(base64url_encode("hi>?there\x00bin")); // string(18) "aGk-P3RoZXJlAGJpbg"

Когда использовать: части JSON Web Token, параметры state и nonce в OAuth, идентификаторы API, которые вы кладёте в пути URL, и всё, что будет скопировано в адресную строку или в имя файла. Когда не использовать: тела писем, PEM-броня и любое место, где на другом конце сидит потребитель стандартного алфавита, потому что - и _ не входят в его словарь. И не смешивайте два алфавита молча: токен, закодированный в URL-безопасной форме, должен декодироваться в URL-безопасной форме, везде, навсегда. Вот и всё правило совместимости base64url.

Юникод и те байты, которые вы имели в виду

Строки PHP - это последовательности байтов, и base64_encode() кодирует любые байты, которые ему дают, не спрашивая, что они значат. Это черта-преимущество, до того дня, когда ваш «текст» окажется не в той кодировке, в которой вы о нём думаете. Классический сбой: строка, которая в редакторе выглядит как UTF-8, но пришла из устаревшего источника в Windows-1252. Закодируйте эти байты как есть - и принимающая сторона, которая декодирует и предполагает UTF-8, получит кракозябры вместо ваших букв с диакритикой.

Решение - нормализовать до кодирования, с расширением mbstring (оно поставляется с исходниками PHP, но по умолчанию не включено):

$fromLegacy = "caf\xE9 au lait"; // байты Windows-1252: буква é - это 0xE9
$utf8 = mb_convert_encoding($fromLegacy, 'UTF-8', 'Windows-1252');
$encoded = base64_encode($utf8);
var_dump($utf8); // string(13) "café au lait": теперь é - это два байта UTF-8

Если исходник уже в UTF-8, конверсию можно пропустить, а дешёвая проверка здравомыслия - mb_check_encoding($utf8, 'UTF-8'). Совет в одном предложении: никогда не пытайтесь «починить» уже закодированную Base64-строку, перекодируя её как текст. Это ловушка двойного кодирования из раздела о граблях ниже, и это самый частый Base64-баг во всех PHP-кодбазах.

Файлы, блобы и конвенция .b64

Самая простая задача кодирования: файл становится текстом. Строки PHP - это байты, поэтому о «бинарном режиме» заботиться не нужно: file_get_contents() отдаёт точные байты, а base64_encode() отдаёт точный текст:

$binary = file_get_contents('/var/www/uploads/photo.png');
$encoded = base64_encode($binary);
file_put_contents('/var/www/uploads/photo.png.b64', $encoded);
var_dump(strlen($binary), strlen($encoded)); // тот самый 33% счёт, каждый раз

Две привычки держат это в безопасности. Первая - знайте, что вы кодируете. Класс finfo (расширение fileinfo, входит в стандартные сборки PHP) говорит вам реальный тип по байтам, а не по имени файла:

$mime = (new finfo(FILEINFO_MIME_TYPE))->file('/var/www/uploads/photo.png');
var_dump($mime); // string(9) "image/png"

Вторая - помните о счёте за размер, когда планируете хранение: изображение в 500 КБ становится текстовым файлом в 670 КБ, а видео в 1 ГБ - текстовым файлом в 1,33 ГБ. Именно поэтому ниже существует раздел о больших данных.

Data URI: встраивание изображения в страницу

Data URI встраивает блок прямо в URL, поэтому для его получения не нужен второй запрос. RFC 2397 определяет форму: data:, опциональный MIME-тип, опциональный флаг ;base64, запятая и данные. Для двоичных медиа, таких как изображения, флаг присутствует, поэтому блок - это ровно то, что произвёл base64_encode():

function data_uri_for(string $path): string
{
  $mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
  $payload = base64_encode(file_get_contents($path));
  return 'data:' . $mime . ';base64,' . $payload;
}
$uri = data_uri_for('/var/www/uploads/avatar.png');
echo '<img src="' . $uri . '" alt="avatar" />' . "\n";

Зачем здесь Base64? Потому что URI не может безопасно содержать сырые байты или запятые, а алфавиту Base64 не нужно вообще никакого экранирования. Но и цена вопроса реальна. Закодированный блок примерно на 33 процента больше файла, а значит, больше становится и сам HTML-документ. Браузеры не кэшируют data URI так, как кэшируют URL файлов, поэтому каждый просмотр страницы заново скачивает байты. И сам RFC говорит, что data URI полезны только для коротких значений: старые HTML-парсеры имели жёсткие лимиты на длину атрибутов, а современные браузеры, пусть и куда более щедрые, всё равно не любят мегабайты внутри тега. Используйте их для аватарок, иконок и небольших встроенных графических элементов, а для всего остального - настоящие файлы.

JWT и API-токены

JSON Web Token - флагманский потребитель Base64 в современных API, и он использует URL-безопасный диалект без заполнений из раздела выше. Согласно RFC 7519, компактный JWT - это три разделённые точками base64url-части: заголовок, блок данных, подпись. Заголовок и блок данных - обычный JSON; подпись - сырые байты. Собрать один вручную - приятный способ увидеть каждую движущуюся деталь:

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'iat' => 1516239022,
]));
$signingInput = $header . '.' . $payload;
$signature = base64url_encode(hash_hmac('sha256', $signingInput, $secret, true));
$token = $signingInput . '.' . $signature;

Две вещи, на которые стоит обратить внимание. Подпись - это base64url-кодирование сырых HMAC-байтов, поэтому hash_hmac() вызывается с true для сырого вывода. И заголовок с блоком данных может прочитать любой, и это сделано намеренно: JWT - подписанный билет, а не секрет. Для продакшена не пишите подписание и проверку вручную. Пакет сообщества - firebase/php-jwt (v7, требуется PHP 8.0 или новее), устанавливается через Composer:

composer require firebase/php-jwt
use Firebase\JWT\JWT;
$secret = 'correct-horse-battery-staple-long-enough-secret';
$token = JWT::encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'exp' => time() + 3600,
], $secret, 'HS256');
var_dump(substr_count($token, '.')); // int(2): заголовок, блок данных, подпись

Заметка о версиях для v7-линии библиотеки: алгоритмы HMAC требуют минимальной длины ключа, поэтому секрет HS256 короче 32 байт отклоняется до начала какого-либо кодирования. Длинные секреты - это норма и так; это просто заставляет библиотеку отказываться от небрежности.

Библиотека берёт на себя base64url-конверсию, подписание и проверки срока действия, и бросает типизированные исключения вместо того, чтобы возвращать наполовину доверенные данные. Когда вы пишете токены с её помощью, вы никогда не прикасаетесь к base64_encode() напрямую, и именно так и должно быть.

HTTP: Basic Auth и рукопожатие WebSocket

Две задачи построения заголовков, где PHP делает Base64, а протокол делает остальное.

HTTP Basic-авторизация (RFC 7617): клиент отправляет Authorization: Basic и Base64 строки username:password. Построение - одна строковая конкатенация:

function basic_authorization_header(string $username, string $password): string
{
  return 'Basic ' . base64_encode($username . ':' . $password);
}
$headers[] = 'Authorization: ' . basic_authorization_header('alice', 'secret123');

Произнесите вслух хотя бы раз, потому что RFC заставляет: это кодирование, а не защита. Любой, у кого есть захват пакетов, восстанавливает обе половины одним нажатием клавиши, поэтому Basic-авторизация принадлежит только HTTPS-соединениям.

Рукопожатие WebSocket (RFC 6455): сервер доказывает, что услышал клиента, отзеркаливая преобразованный ключ. Он конкатенирует Sec-WebSocket-Key клиента с фиксированным магическим GUID, берёт SHA-1 от результата и base64-кодирует дайджест. Это стандартный Base64, заполнения включены, потому что живёт он в заголовке, а не в URL:

function websocket_accept_key(string $clientKey): string
{
  return base64_encode(hash('sha1', $clientKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}
$accept = websocket_accept_key('dGhlIHNhbXBsZSBub25jZQ==');
var_dump($accept); // string(28) "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

Этот пример - тот самый из самого RFC, что делает его удобным самопроверочным тестом: если ваша реализация выдаёт те же 28 символов, слой WebSocket говорит правильно.

Электронная почта: исходный случай использования

Всё остальное в этой статье - потомок одного факта: SMTP был задуман для пересылки 7-битного ASCII, а люди хотели отправлять двоичные файлы. Ответ стандарта MIME, в RFC 2045, раздел 6.8, - это Base64 как Content-Transfer-Encoding, с двумя внутренними правилами, с которыми вы уже знакомы: строки не длиннее 76 символов и декодеры, которые игнорируют каждый символ вне алфавита. Так путешествует PDF-вложение:

$pdf = file_get_contents('/var/www/uploads/report.pdf');
$attachment = chunk_split(base64_encode($pdf), 76, "\r\n");
// теперь почтовая библиотека кладёт $attachment в MIME-часть,
// с Content-Transfer-Encoding: base64

Практические числа: само кодирование стоит 33 процента, а CRLF каждые 76 символов стоит ещё немного сверху, поэтому вложение в 100 КБ уезжает примерно как 137 КБ текста. Когда вы пишете почту из PHP, библиотеки (PHPMailer и его стабильные родственники) делают обёртку за вас, а вы отдаёте им сырые двоичные данные. Если когда-нибудь вы увидите стену букв шириной в 76 символов в сыром файле .eml, теперь вы знаете точный алгоритм, который её произвёл.

PEM-броня для ключей и сертификатов

Ключам и сертификатам нужно что-то больше, чем стена букв. Им нужны метки. PEM-броня - это строка BEGIN, блок Base64, обёрнутый по 64 символа, и строка END. Это конвенция, унаследованная от Privacy-Enhanced Mail (RFC 1421) и поддерживаемая OpenSSL. Расширение openssl в PHP производит и потребляет эту форму напрямую:

$res = openssl_pkey_new([
  'private_key_bits' => 2048,
  'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($res, $pem);
// $pem уже обёрнут: метка BEGIN, строки по 64 символа, метка END

Интересный случай - когда обёртку приходится собирать вручную. Например, когда вы получили сырые DER-байты из API и нужен PEM-файл для инструмента, который читает только PEM. Конвенция: 64 символа на строку, переводы строк LF и метка, называющая содержимое:

$rearmored = "-----BEGIN CERTIFICATE-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END CERTIFICATE-----\n";
var_dump(openssl_x509_parse($rearmored) !== false); // bool(true): обёртка валидна

Ошибётесь в метке - и файл станет мусором, каким бы безупречным ни был Base64. А ошибётесь в длине строки - большинство инструментов всё равно прочитают, потому что декодеры игнорируют переводы строк, но diff-инструментам и людям придётся несладко. Шестьдесят четыре - это то самое число.

Конфигурационные файлы, переменные окружения и базы данных

Base64 - это текстовый контейнер, что делает его инструментом контрабанды для значений, которые иначе сломают свой контейнер. DSN базы данных, полный точек с запятой и кавычек, JWT в .env-файле, двоичный blob в столбце TEXT: всё это превращается в одну длинную безопасную строку.

Вариант с переменными окружения - это двухшаговый ритуал. Один раз, на машине, которая строит конфигурацию, вы кодируете:

echo 'DB_DSN_B64=' . base64_encode('pg:host=db;password=qu"ote') . PHP_EOL;

А затем при каждом запуске приложения вы декодируете и валидируете на старте, чтобы наполовину вставленная конфигурация упала громко, а не загадочно:

$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}

В базе данных та же идея хранит двоичные данные в текстовых столбцах. Счёт за размер действует: хранимое значение примерно на 33 процента больше blob, поэтому файл в 1 МБ занимает в столбце примерно 1,33 МБ, и выбирать тип столбца стоит с учётом этого. И то же предупреждение, что и везде: это безопасность формата, а не секретность. Любой, кто может прочитать конфиг или сделать запрос к столбцу, развернёт значение одним вызовом. Если значение чувствительное - шифруйте его. Base64 лишь делает его переносимым.

Большие данные и ровная память

Кодирование - направление, которое вам стоит денег: вывод на треть больше входа, поэтому двоичные данные на 2 ГБ хотят 2,66 ГБ закодированной строки в памяти. В долгоживущем веб-процессе или на хосте с тесными лимитами памяти это причина перекачивать, а не заглатывать целиком, и PHP даёт вам два способа.

Первый способ - потоковый фильтр convert.base64-encode, поточный двойник функции. Он принимает параметры в виде ассоциативного массива: line-length для ширины обёртки и line-break-chars для разделителя, что воспроизводит эффект chunk_split() без удержания всей строки:

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE, [
  'line-length' => 64,
  'line-break-chars' => "\n",
]);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

Второй способ - классический трюк с 57 байтами, и это небольшой кусочек PHP-фольклора. В 76-символьную MIME-строку помещаются ровно 57 байт исходных данных, поэтому если читать входной файл кусками, кратными 57 байтам, каждый кусок кодируется независимо, без остаточных битов, которые надо носить между кусками. Чтение кусками по 8151 байту (57 раз по 143: 143 полные 76-символьные строки вывода, близко к традиционному 8192-байтовому I/O-буферу PHP) держит память ровной, пока файл выливается идеальным MIME:

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
while (!feof($in)) {
  $plain = fread($in, 57 * 143);
  $encoded = chunk_split(base64_encode($plain), 76, "\r\n");
  fwrite($out, $encoded);
}
fclose($in);
fclose($out);

Что выбрать? Фильтр, если хотите, чтобы PHP владел коммуникациями, и вам безразличны точные границы кусков; цикл по 57 байт, если нужен детерминированный MIME-вывод, крючки прогресса или жёсткий потолок размера буфера. В любом случае потребление памяти остаётся одним куском, а не одним файлом.

Ловушки с PHP-акцентом

Ловушки, которые встречаются в реальных PHP-кодбазах, все в одном месте:

  • Двойное кодирование. Классика: значение, которое уже Base64 (из переменной окружения, базы данных, предыдущего скрипта), проходит через base64_encode() ещё раз, потому что никто не проверил. Результат декодируется один раз и выдаёт... ещё больше Base64. Лечение - проверка полного цикла на границе или одна известная функция, которой принадлежит всё кодирование в коде.
  • + в URL. Стандартный вывод содержит + и /. В form-кодированной строке запроса плюс становится пробелом до того, как ваш код его видит. В пути - это разделитель. Для всего, что связано с URL, выдавайте base64url или percent-кодируйте всё значение через rawurlencode().
  • Хвостовой разделитель. chunk_split() завершает результат разделителем даже на точных кратных длине строки. Завершающая пустая строка обычно безвредна (декодеры её игнорируют), но спотыкаются о неё наивные счётчики строк и diff-инструменты. Если потребитель капризный, rtrim()-ните разделитель.
  • Несовпадение обёрток. Писать строки MIME по 76 символов, а потребитель ожидает строки PEM по 64 (или наоборот), - это не проблема декодирования, потому что декодеры игнорируют переводы строк, но это проблема для строковых инструментов и людей, читающих результат. Выбирайте конвенцию, которую ожидает пункт назначения, и придерживайтесь её.
  • Хвостовой перевод строки - это данные. base64_encode() кодирует каждый байт, включая перевод строки в конце текстового файла. Когда две системы дают «разный» Base64 для одинакового на вид текста, обычный подозреваемый - хвостовой \n.
  • Base64 - не шифрование. Кодирование пароля перед тем, как он попадёт в базу, не защищает его, а форматирует. «Зашифрованный» столбец - в одном вызове функции от открытого текста для любого, у кого есть доступ к запросам. Настоящие секреты шифруйте или хэшируйте. Base64 - это транспортный костюм.
  • Память на треть больше. На 32-битной сборке PHP или хосте с тесными лимитами памяти кодирование большого двоичного файла может провалиться начисто. Перекачивайте, как показано выше, прежде чем крутить memory_limit.
  • Переводы строк никогда не добавляются, никогда. «MIME base64» в описании функции не означает «MIME-обёрнутый вывод». Если вашему выводу нужны строки по 76 символов, добавьте их через chunk_split() или фильтр.

Краткая история base64_encode

Кодирующая сторона истории PHP почти освежающе скучная, в лучшем смысле этого слова. base64_encode() приехала в PHP 4 как ядерная функция с одним параметром и без опций, и с тех пор не получила ни единой новой. Строгий режим никогда не требовался (когда данные производите вы, тут нечего делать строго), опция заполнений никогда не добавлялась, а работа с обёрткой с первого дня была делегирована chunk_split(), и именно поэтому две функции до сих пор сидят вместе в списках «See Also» мануала.

Мануал несёт цифру 33 процента с тех пор, как кто-либо может помнить: «Данные, закодированные в Base64, занимают примерно на 33% больше места, чем исходные данные». Это предложение до сих пор там, и именно поэтому эта цифра вообще появляется в этой статье. Потоковый фильтр convert.base64-encode присоединился позже, и у него был свой баг, из которого ему пришлось вырасти: в 2015 году PHP исправил дефект (баг #68532), из-за которого фильтр в режиме чтения на memory-потоках мог опускать последний символ заполнения, а это ровно тот вид тихого повреждения, от которого не страдает функциональный путь. PHP 8.0 добавил нативные типы string для параметра и возврата, и на этом changelog заканчивается. Одна функция, один параметр, двадцать лет, ноль опций: памятник тому, как сделать поверхность правильно с первого раза.

Весёлые PHP-факты

Потому что референс не полон без странностей:

  • Пустая идентичность. base64_encode('') - это ''. Без заполнений, без вывода, без сюрпризов: пустота на входе, пустота на выходе.
  • Странный адрес. PHP-мануал относит base64_encode() к главе «URLs» книги «Прочие базовые расширения». Главы «кодирование» нет; «URLs» - вот где вы её найдёте, рядом с parse_url().
  • Алфавит никогда не двигался. Одни и те же 64 символа выходят из PHP-кодировщика с PHP 4. Base64-строка, произведённая PHP 4-скриптом на Windows-машине в 2001 году, декодируется идентично на PHP 8.4 на Linux сегодня. Это совместимость с 25-летним послужным списком.
  • Один байт и три байта выглядят одинаково по длине. Оба дают четыре символа; отличить их может только число заполнений. Поэтому в этой статье и существует таблица размеров.
  • Пятьдесят семь - магическое число. В 76-символьную MIME-строку помещаются ровно 57 байт исходных данных, и именно это совпадение делает возможным поточный цикличный кусок из раздела о больших данных, без ношения состояния между чтениями.
  • У него есть dial-up-брат. В «See Also» у base64_encode() даже числится convert_uuencode(), PHP-обёртка над uuencode, форматом, который кодировал двоичные данные для почты до того, как MIME стандартизировал Base64. Он живёт в главе String Functions, функции строк, но это окаменелость, фиксирующая изначальное назначение этой функции.
  • Старые браузеры были капризны к заполнениям. Заметка на php.net от 2004 года сообщает, что Internet Explorer отказывался от имён cookie, содержащих =, поэтому ветеранский код иногда срезает хвостовые заполнения с Base64, хранящейся в cookie. Современным сетапам этот трюк не нужен, но он объясняет странные вызовы rtrim($x, '='), которые вам могут достаться.

Обратная сторона

Вот и вся сторона кодирования, и это более лёгкая из двух: функция не может упасть, вывод детерминирован, а формат - тот, который вы контролируете от конца до конца. Более трудное направление - то, где вы получаете Base64 других людей: их выбор заполнений, их переводы строк, их URL-безопасные диалекты, их испорченные вставки. Именно там декодеру нужны строгий режим, валидационный конвейер и здоровое подозрение ко всему. Декодирование Base64 в PHP, на которое есть ссылка с этой страницы, разбирает сторону декодирования с той же глубиной.

Последнее обновление: 2026-09-08

Связанная статья: Декодирование Base64 в PHP: полное руководство