PHP, la manera correcta

5.3. Trabajando con UTF-8

5.3.1. No hay más secreto que tener cuidado

Las versiones actuales de PHP no soportan la codificación UTF-8 a bajo nivel. Por supuesto existen formas de asegurar que las cadenas de texto con contenidos UTF-8 se procesan correctamente, pero no es algo sencillo. De hecho, requiere tener cuidado en todos los niveles de la aplicación, desde el contenido HTML hasta las sentencias SQL y las instrucciones PHP.

5.3.2. UTF-8 a nivel de PHP

Las operaciones básicas sobre cadenas de texto, como concatenar dos cadenas o guardar contenidos de texto en variables, no requieren de ningún proceso especial para soportar UTF-8.

No obstante, hay que tener especial cuidado con muchas otras de las funciones relacionadas con las cadenas, como strpos() y strlen(). Estas funciones suelen tener definida una función relacionada que empieza por mb_. Así que por ejemplo también existen mb_strpos() y mb_strlen(). Para disponer de estas funciones que empiezan por mb_, debes tener instalada la extensión Multibyte String Extension de PHP.

Cuando trabajes con información codificada con UTF-8, debes utilizar siempre las funciones mb_*. Si por ejemplo aplicas la función substr() a una cadena UTF-8, lo más probable es que el resultado contenga caracteres incorrectos que se verán como errores en el navegador. En este caso, debes utilizar la función mb_substr().

Lo más difícil es acordarte de que tienes que utilizar siempre las funciones mb_*. Si por ejemplo olvidas utilizar estas funciones una sola vez, entonces es prácticamente seguro que el contenido original de tu cadena sufrirá errores irrecuperables.

Lo peor es que no todas las funciones relacionadas con cadenas de texto tienen una función mb_* asociada. Así que si no existe una función equivalente, no podrás hacer nada para trabajar correctamente con la información UTF-8.

Otra recomendación interesante consiste en añadir la función mb_internal_encoding() al principio de cada script PHP y justo después de ella, la función mb_http_output() en caso de que tu script escriba contenidos directamente en el navegador. Estas funciones indican explícitamente la codificación de las cadenas de texto, lo que te evitará muchos problemas.

Por otra parte, muchas de las funciones PHP relacionadas con las cadenas de texto definen un parámetro que permite definir la codificación de caracteres. Si este parámetro está disponible en una función, deberías utilizarlo para indicar que vas a utilizar UTF-8. La función htmlentities() por ejemplo es una de las funciones que incluyen este parámetro. Eso sí, a partir de la versión 5.4 de PHP las funciones htmlentities() y htmlspecialchars() ya utilizan la codificación UTF-8 por defecto.

Por último, si estás desarrollando una aplicación de software libre que será utilizada en muchos servidores diferentes sobre los que no puedes controlar si está disponible o no la extensión mbstring, entonces puedes utilizar el paquete patchwork/utf8. Internamente esta librería utiliza mbstring si está disponible y si no, utilizará sus propias funciones UTF-8 alternativas.

5.3.3. UTF-8 a nivel de base de datos

Si tus scripts PHP acceden a una base de datos MySQL, es posible que la información se esté guardando con una codificación diferente a UTF-8, incluso aunque hayas utilizado todas las precauciones explicadas en la sección anterior.

Para asegurarte de que la información que viaja entre PHP y MySQL se mantenga codificada con UTF-8, comprueba que las bases de datos y todas sus tablas tienen el valor utf8mb4 tanto en la opción character set como en la opción collation. Asegúrate también de que la conexión PDO utiliza el valor utf8mb4 en la opción character set. Todo esto es extremadamente importante.

Si utilizas como valor utf8, no obtendrás soporte UTF-8 completo. El único valor que lo garantiza es utf8mb4.

5.3.4. UTF-8 a nivel de navegador

Utiliza la función mb_http_output() para asegurarte que el contenido generado por tu script PHP está codificado con UTF-8.

Después es necesario decirle al navegador que los contenidos de la página deben procesarse como UTF-8. Para ello, tradicionalmente se utilizaba la etiqueta charset dentro del elemento <head> de la página. Aunque esta opción todavía funciona, ahora se recomienda indicar la codificación mediante la cabecera Content-Type de HTTP ya que es mucho más rápido.

<?php
// Esto le dice a PHP que usaremos cadenas UTF-8 hasta el final
mb_internal_encoding('UTF-8');
 
// Esto le dice a PHP que generaremos cadenas UTF-8
mb_http_output('UTF-8');
 
// Un ejemplo de cadena UTF-8
$string = 'Êl síla erin lû e-govaned vîn.';
 
// Transformamos la cadena utilizando una función mb_*
$string = mb_substr($string, 0, 15);
 
// Conectamos con la base de datos para guardar la cadena transformada
// Observa que también añadimos el comando `set names utf8mb4`
$link = new \PDO(   
    'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
    'el-usuario',
    'la-contraseña',
    array(
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_PERSISTENT => false
    )
);
 
// Guardar en la base de datos la cadena con codificación UTF-8
$handle = $link->prepare('insert into ElvishSentences (Id, Body) values (?, ?)');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->bindValue(2, $string);
$handle->execute();
 
// Obtener la cadena de nuevo para demostrar que se guardó bien
$handle = $link->prepare('select * from ElvishSentences where Id = ?');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();
 
// Almacenar el resultado en un objeto utilizado después para generar HTML
$result = $handle->fetchAll(\PDO::FETCH_OBJ);

header('Content-Type: text/html; charset=UTF-8');
?><!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Página de prueba UTF-8</title>
    </head>
    <body>
        <?php
        foreach($result as $row){
            print($row->Body);  // esta cadena UTF-8 debería mostrarse bien
        }
        ?>
    </body>
</html>

5.3.5. Leer más sobre UTF-8