mb_string en PHP: trabajar con Unicode y strings multibyte correctamente

PHP trabaja de fábrica con cadenas de bytes, no con caracteres Unicode. Eso significa que strlen("árbol") devuelve 6, no 5, porque la á ocupa dos bytes en UTF-8. La extensión mb_string resuelve ese problema: sus funciones entienden la codificación real del texto y cuentan caracteres, no bytes.

Activar mb_string

En la mayoría de servidores modernos ya viene habilitada. Puedes comprobarlo con:

php -m | grep mbstring

Si no aparece, añade extension=mbstring en tu php.ini y reinicia el servidor web. En Ubuntu/Debian: sudo apt install php-mbstring && sudo phpenmod mbstring.

mb_strlen y mb_substr

Las funciones más usadas del módulo. Siempre pasa la codificación explícita para evitar sorpresas si mbstring.internal_encoding difiere entre entornos:

<?php
$texto = "árbol";
echo strlen($texto);    // 6 — cuenta bytes
echo mb_strlen($texto, 'UTF-8');  // 5 — cuenta caracteres

$emoji = "Hola ?";
echo mb_strlen($emoji, 'UTF-8');  // 6

// Extraer los primeros 3 caracteres
echo mb_substr($texto, 0, 3, 'UTF-8');  // árb

// Árabe: cada letra es multibyte
$arabe = "?????";
echo mb_strlen($arabe, 'UTF-8');   // 5
echo mb_substr($arabe, 1, 3, 'UTF-8');  // ???

mb_strpos y mb_strrpos

Búsqueda de subcadenas respetando la codificación:

<?php
$frase = "El niño juega en el patio";
$pos = mb_strpos($frase, "niño", 0, 'UTF-8');
echo $pos;  // 3

// Búsqueda insensible a mayúsculas
$pos = mb_stripos("Árbol Grande", "árbol", 0, 'UTF-8');
echo $pos;  // 0

// Última ocurrencia
$cadena = "test-abc-test";
echo mb_strrpos($cadena, "test", 0, 'UTF-8');  // 9

mb_strtolower y mb_strtoupper

Las funciones nativas strtolower y strtoupper no conocen caracteres como Á, Ñ o Ü. Las versiones mb sí:

<?php
echo strtolower("ÁRBOL");    // ÁRbol — falla con la Á
echo mb_strtolower("ÁRBOL", 'UTF-8');  // árbol — correcto

echo mb_strtoupper("über cool", 'UTF-8');  // ÜBER COOL

// Chino: mb_strtolower no tiene efecto (ya no hay mayúsculas)
// pero mb_strlen funciona correctamente
$chino = "????";
echo mb_strlen($chino, 'UTF-8');  // 4

mb_str_split

Disponible desde PHP 7.4. Divide una cadena en un array de caracteres (o fragmentos) respetando la codificación:

<?php
$texto = "café";
print_r(str_split($texto));
// Array ( [0] => c [1] => a [2] => f [3] => Ã [4] => © )
// — str_split parte bytes, no caracteres

print_r(mb_str_split($texto, 1, 'UTF-8'));
// Array ( [0] => c [1] => a [2] => f [3] => é )

// Dividir en grupos de 2 caracteres
print_r(mb_str_split("árbol", 2, 'UTF-8'));
// Array ( [0] => ár [1] => bo [2] => l )

mb_convert_encoding

Cuando recibes texto de fuentes externas (ficheros legacy, APIs antiguas, emails) puede venir en ISO-8859-1, Windows-1252 o similares. mb_convert_encoding convierte entre cualquier par de codificaciones que soporte mb_string:

<?php
// Leer un CSV en ISO-8859-1 y pasarlo a UTF-8
$contenido = file_get_contents('clientes_old.csv');
$utf8 = mb_convert_encoding($contenido, 'UTF-8', 'ISO-8859-1');

// Detectar la codificación automáticamente
$detectada = mb_detect_encoding($contenido, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true);
echo $detectada;  // ISO-8859-1

// Convertir desde detección automática
$utf8 = mb_convert_encoding($contenido, 'UTF-8', $detectada ?: 'ISO-8859-1');

mb_substr_count

Cuenta cuántas veces aparece una subcadena, respetando codificación:

<?php
$texto = "niño, niña y niñez";
echo substr_count($texto, "niñ");    // puede fallar
echo mb_substr_count($texto, "niñ", 'UTF-8');  // 3

Configuración global en php.ini

Puedes establecer una codificación interna por defecto para no tener que pasarla en cada llamada:

; php.ini
mbstring.internal_encoding = UTF-8
mbstring.http_output       = UTF-8
mbstring.http_input        = UTF-8

O bien al inicio de tu aplicación:

<?php
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');
// A partir de aquí puedes omitir el parámetro de codificación:
echo mb_strlen("árbol");  // 5

Errores frecuentes

  • Mezclar strlen con mb_strlen: usar strlen para validar longitudes de contraseñas o usernames en UTF-8 produce límites incorrectos. Usa siempre mb_strlen.
  • Olvidar la codificación: si mbstring.internal_encoding no está a UTF-8 y omites el parámetro, los resultados varían entre servidores.
  • str_split en textos multibyte: parte bytes, no caracteres. Sustituye siempre por mb_str_split.
  • preg_ con textos Unicode: añade el modificador /u para que las expresiones regulares traten la cadena como UTF-8: preg_match('/p{L}+/u', $texto).

Ejemplo completo: truncar un título sin cortar palabras

<?php
function truncar(string $texto, int $max, string $enc = 'UTF-8'): string {
    if (mb_strlen($texto, $enc) <= $max) {
        return $texto;
    }
    $corte = mb_strrpos(mb_substr($texto, 0, $max, $enc), ' ', 0, $enc);
    if ($corte === false) {
        $corte = $max;
    }
    return mb_substr($texto, 0, $corte, $enc) . '…';
}

echo truncar("El árbol genealógico de la familia Martínez", 25);
// El árbol genealógico de…

Con mb_string tienes acceso a más de 100 funciones multibyte. Las cubiertas aquí son las que aparecen en el 90% de los proyectos reales. A partir de ellas el resto resulta intuitivo: son variantes con el prefijo mb_ de sus equivalentes estándar.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP