Developer Guide

PHP Hex to String: hex2bin, Encoding, and Common Pitfalls

In PHP, hex ↔ string conversion is almost always a two-step process: hex characters become raw bytes, then those bytes are interpreted with a character encoding (usually UTF-8). Mixing up those steps is the main reason tutorials get this topic wrong.

This guide shows the correct mental model, the built-in APIs you should use first, how multi-byte text really works, and the pitfalls that break production code. You can also try conversions instantly with our Hex to String and String to Hex tools.

The correct mental model

bin2hex() and hex2bin() operate on bytes, not on “characters.” A PHP string is a byte buffer. Encoding (UTF-8, GBK, Windows-1252) only matters when you decide how those bytes should be read as text.

That means UTF-8 Chinese, emoji, and ASCII all work with hex2bin/bin2hex as long as the hex correctly represents the intended byte sequence. The common claim that these functions “only work for single-byte encodings” is incorrect.

Built-in conversion: start here

<?php
$hex = '48656c6c6f';          // bytes for "Hello"
echo hex2bin($hex);         // Hello

$text = 'Hello';
echo bin2hex($text);        // 48656c6c6f

// UTF-8 multi-byte text is fine — hex encodes bytes
$utf8 = '你好';
$hexUtf8 = bin2hex($utf8);
echo $hexUtf8, PHP_EOL;
echo hex2bin($hexUtf8), PHP_EOL; // 你好

From PHP 8 onward, invalid hex input to hex2bin() throws ValueError. On older versions it returned false and raised a warning. Always validate before converting untrusted input.

<?php
function hexToBytes(string $hex): string
{
    $clean = preg_replace('/\s+|0x/i', '', $hex) ?? '';
    if ($clean === '' || strlen($clean) % 2 !== 0 || !ctype_xdigit($clean)) {
        throw new InvalidArgumentException('Hex must be an even-length hex digit string');
    }
    return hex2bin($clean);
}

echo hexToBytes('48 65 6c 6c 6f'); // Hello

Where encoding actually belongs

Encoding conversion belongs around hex helpers, not inside a no-op wrapper.

<?php
// Wrong pattern seen in many AI tutorials:
// mb_convert_encoding($s, 'UTF-8', 'UTF-8') does nothing useful.

// Correct: convert character encoding FIRST, then hex the bytes.
$gbkText = mb_convert_encoding('你好', 'GBK', 'UTF-8');
$hexGbk = bin2hex($gbkText);

// Later: hex → bytes → interpret as GBK → convert to UTF-8 for output
$bytes = hex2bin($hexGbk);
$asUtf8 = mb_convert_encoding($bytes, 'UTF-8', 'GBK');
echo $asUtf8; // 你好

If your source string is already UTF-8 (typical for modern PHP apps), bin2hex($string) is enough. Use mb_convert_encoding() only when the byte layout must change (for example GBK storage or a legacy protocol).

pack / unpack alternative

pack('H*', $hex) is equivalent to hex2bin() for many cases. Prefer hex2bin/bin2hex for clarity unless you already use pack for neighboring binary formats. See also our deeper notes on PHP hex2bin() and PHP pack().

<?php
$hex = '48656c6c6f';
echo pack('H*', $hex);                 // Hello
echo unpack('H*', 'Hello')[1];         // 48656c6c6f

Practical pitfalls

1. Odd-length hex

Hex pairs map to bytes. An odd number of digits is invalid. Do not silently prepend 0 unless a protocol explicitly requires it — that changes the data.

2. Spaces, newlines, and 0x prefixes

hex2bin() does not ignore whitespace. Strip separators first (as in the helper above). Logs often look like 0x48 0x65; remove 0x markers before conversion.

3. Chunked / streaming hex files

If you read a hex file in fixed-size chunks (for example 4096 bytes), a chunk may end in the middle of a hex pair. Keep a one-character carry-over buffer between reads, or process only even-length slices.

<?php
function streamHexFileToBinary(string $in, string $out): void
{
    $hi = fopen($in, 'rb');
    $ho = fopen($out, 'wb');
    $carry = '';
    while (!feof($hi)) {
        $chunk = $carry . preg_replace('/\s+/', '', fread($hi, 8192));
        $even = strlen($chunk) - (strlen($chunk) % 2);
        $carry = substr($chunk, $even);
        if ($even > 0) {
            fwrite($ho, hex2bin(substr($chunk, 0, $even)));
        }
    }
    fclose($hi);
    fclose($ho);
    if ($carry !== '') {
        throw new RuntimeException('Trailing incomplete hex digit');
    }
}

4. Storage advice

Hex doubles storage size versus raw bytes. For databases, prefer a binary/BLOB column (or base64 when you need text-safe transport), not hex strings of every field. Hex is excellent for logs, debugging, and short tokens — not as a general persistence format.

A small, honest helper

<?php
final class HexCodec
{
    public static function toHex(string $bytes): string
    {
        return bin2hex($bytes);
    }

    public static function fromHex(string $hex): string
    {
        $clean = preg_replace('/\s+|0x/i', '', $hex) ?? '';
        if ($clean === '' || strlen($clean) % 2 !== 0 || !ctype_xdigit($clean)) {
            throw new InvalidArgumentException('Invalid hex');
        }
        return hex2bin($clean);
    }

    public static function stringToHex(string $text, string $fromEncoding = 'UTF-8'): string
    {
        if (strtoupper($fromEncoding) !== 'UTF-8') {
            $text = mb_convert_encoding($text, 'UTF-8', $fromEncoding);
        }
        // If you need a non-UTF-8 wire format, convert TO that encoding before bin2hex.
        return bin2hex($text);
    }

    public static function hexToString(string $hex, string $byteEncoding = 'UTF-8'): string
    {
        $bytes = self::fromHex($hex);
        if (strtoupper($byteEncoding) === 'UTF-8') {
            return $bytes;
        }
        return mb_convert_encoding($bytes, 'UTF-8', $byteEncoding);
    }
}

echo HexCodec::fromHex('e4bda0e5a5bd'); // 你好 (UTF-8 bytes)

Quick checklist

Related reading: PHP string to hex, hex2bin guide, and the overview What is hexadecimal encoding?.