Buenas prácticas

Los principios

PHP es un lenguaje basto que permite a desarrolladores con cualquier nivel de habilidad producir código, no sólo rápidamente, sino también eficientemente. No obstante, mientras se avanza en el aprendizaje del lenguaje, tiende a olvidarse (o pasar por alto) las buenas prácticas en favor de atajos y malos hábitos. Para ayudar a prevenir esta inicidencia, esta sección se enfoca en recordar a los desarrolladores de las buenas prácticas a aplicar con PHP.

Fecha y tiempo

PHP provee la clase DateTime que facilita el trabajo para leer, escribir, comparar o calcular fecha y tiempo. Además de la citada clase, en PHP existen muchas funciones relacionadas con tiempo y hora, pero DateTime provee una cómoda interfaz orientada a objetos para la mayoría de los usos posibles.

Para comenzar, considérsse el siguiente ejemplo:

$raw = '06/09/1987';
$date = \DateTime::createFromFormat('d/m/Y', $raw);
$today = new \DateTime();

echo 'Fecha: ' . $date->format('Y-m-d') . "\n"; // 1987-09-06
echo 'Hoy: ' . $today->format('Y-m-d') . "\n";

A partir del método fábrica createFromFortmat() se crea una instancia de DateTime en función de los argumentos proveidos. Luego, indicando al método format() un formato específico se emite una nueva cadena de fecha con dicho formato. Cuando en lugar del método fábrica se instancia mediante el operador new entonces el objeto se crea con la fecha actual.

Es posible realizar cálculo sobre objetos DateTime con la clase DateInterval. DateTime provee métodos como add() y sub() que toman un argumento tipo DateInterval.

<?php
$start = \DateTime();
// crear un copia de $start y adicionar un mes y 6 días
$end = clone $start;
$end->add(new \DateInterval('P1M6D'));

$diff = $end->diff($start);
echo 'Diferencia: ' . $diff->format('%m mes, %d días (total: %a días)') . "\n";
// Diferencia: 1 mes, 6 días (total: 37 días)

En objetos tipo DateTime es posible realizar una comparación regular:

<?php
if($start < $end) {
    echo "¡El inicio es anterior al final!\n";
}

En el caso de iterar sobre eventos recurrente es posible hacer uso de la clase DatePeriod. Toma dos valores de tipo DateTime, inicio y fin, y el intérvalo para el que deolverá todos los eventos de por medio.

<?php
// muestra todos los jueves entre start y $end
$periodInterval = \DateInterval::createFromDateString('first thursday');
$periodIterator = new \DatePeriod($start, $periodInterval, $end, \DatePeriod::EXCLUDE_START_DATE);

foreach ($periodIterator as $date) {
    // mostrar cada fecha en el periodo
    echo $date->format('Y-m-d') . ' ';
}

Carbon es una extensión popular para PHP. Hereda toda la funcionalidad existente en la claase DateTime, por lo que implica mínimas adaptaciones en el código, no obstante provee características extra como el soporte de Localización, así como diversos mecanismos para agregar, restar, y formatear objetos DateTime, además de una forma de probar el código emulando fechas y tiempo arbitrarios.

Patrones de diseño

Al construir un proyecto es útil utilizar patrones comunes a lo largo del código y estructuras. Utilizar patrones es una gran ayuda en tanto que una mayor facilidad para gestionar el código y permitir a otros desarrolladores entender rápidamente como todo encaja y se estructura.

Si se utiliza un framework entonces la mayoría del código de alto nivel y la estructura del proyecto estarán basados en ese framework, así que muchas decisiones sobre patrones ya se encuentran tomadas. Pero sigue siendo una decisión de cada desarrollador(a) escoger los mejores patrones a seguir en el código que escribe aún sobre el framework. Si, por otra parte, no se está utilizando un framework para construir un proyecto, entonces se debe encontrar los mejores patrones acordes al tipo y tamaño de la aplicación que se construye.

Trabajando con UTF-8

Hasta ahora, PHP no soporta Unicode a bajo nivel. Existen mecanismos para asegurar un correcto procesamiento de cadenas UTF-8, pero no son sencillos, hay que ser cuidadosos, detallistas, consistentes, y se requiere lidiar con ellos por toda la estructura de la aplicación, desde el html, pasando por el SQL hasta el propio PHP.

UTF-8 a nivel de PHP

Las operaciones básicas con cadenas, como la concatenación o la asinación de cadenas a variables, realmente no requiere de ningún tratamiento especial para UTF-8. No obstante, la mayoría de funciones sobre cadenas, como strpos() y strlen, necesitan de consideraciones especiales. Esas funciones tienen un mb_* equivalente, por ejemplo: mb_strpos() y mb_strlen(). Tales funciones están especialmente diseñadas para operar con cadenas Unicode y se habilitan vía la extensión Multibyte String.

Se ha de tener el cuidado de utilizas las funciones mb_* siempre que se opere con cadenas Unicode. Por ejemplo, si se utiliza la función substr() en una cadena UTF-8, existe un buena probabilidad de que el resultado incluya algunos caracteres cortados. La función correcta a utilizar debe ser su correspondiente alternativa de multibyute, mb_substr().

Lo difícil puede ser recordar utilizar siempre las funciones mb_*. Con solo una que se olvide, las cadenas Unicode tratadas por dicha función podrían corromperse durante su procesamiento.

Ahora, no todas las funciones tienen una contraparte mb_*, si no existe la que se necesita para un caso dado, entonces se tiene un lío.

Además se debería utilizar mb_internal_encoding() al inicio de cada archivo PHP (o al principio de un archivo global de inclusión o autocarga), y mb_http_output() justo después de que se emita una salida al navegador. La definición explícita de codificación de cadenas en cada archivo puede salvar a más de alguno(a) de un dolor de cabeza.

También, muchas funciones de PHP que operan sobre cadenas tienen un parámetro opcional que permite especificar la codificación de caracteres. Siempre debería indicarse explícitamente 'UTF-8' en dicho parámetro. Por ejemplo, htmlentities() soporta un tercer parámetro para indicar la codificación de caracteres, y siempre se debería indicar 'UTF-8'. Nótese que en PHP 5.6 y posterior, la opción de configuración default_charset se emplea como valor predeterminado. PHP 5.4 y 5.5 utilizan UTF-8 como valor predeterminado. Las versiones anteriores de PHP emplean ISO-8859-1.

Si se esstá construyendo una aplicación distribuída y no se puede segurar que la extensión mbstring esté disponible, entonces considerar el paquete patchwork/utf8. Este paquete utiliza la extensión mbstring cuando está disponible, y de lo contrario provee de un soporte hacia funciones no UTF-8.

Finalmente, si se tiene la extensión mbstring instalada, se puede hacer uso de ocelote, que provee una clase StringHelper con métodos estáticos que envuelven y agregan funcionalidades a muchas de las funciones mb_* (incluyendo algunas funcionalidades no existentes en la extensión de mbstring). De esta manera se disminuye el riesgo de olvidar utilizar la correspondiente alternativa mb_* y se gana legibilidad, por su puesto, el pago es un pequeño sobre esfuerzo (overhead) que implica hacer estas llamadas.

UTF-8 a nivel de base de datos

Si un programa de PHP se conecta a MySQL (o SQL Server), existe una buena probabilidad de que las cadenas no se almacenen codificadas como UTF-8 incluso si se consideran las precauciones citadas en el apartado anterior.

Para garantizar que las cadenas desde PHP a MySQL se almacenen como UTF-8, hay que asegurarse de que la base de datos y sus tablas estén configuradas con la opción utf8mb4, y que en la cadena de conexión se indique la codificación de caracteres con dicho valor. Nótese que se debe indicar ``utf8mb4`` para soportar el conjunto completo de UTF-8, y no solo el conjunto utf8.

En el caso de SQLServer, la extensión sybase no soporta UTF-8, por lo que si se utiliza esta extensión para la conexión, antes de realizar cualquier escritura, se deben convertir las cadenas a ISO-8859-1. No obstante lo mejor ees prescindir de la extensión sybase y emplear el controlador ``sqlsrv`` proveido por Microsoft, dicho controlador por defecto funciona con UTF-8.

UTF-8 a nivel del navegador

Para asegurar que las salidas de PHP al navegador sean en UTF-8 se debe utilizar la función mb_http_output().

Se le debe indicar al navegador mediante la respuesta http que la página debe considerarse como UTF-8. Actualmente, es común establecer la codificación de caracteres en una cabecera de la Respuesta:

header('Content-Type: text/html; charset=UTF-8');

Un mecanismo histórico que se ha utilizado ha sido establecer una etiqueta meta en la sección head del html.

<?php
// Indicar a PHP que las cadenas de todo el archivo son en UTF-8
mb_internal_encoding('UTF-8');

// Indicar que las salidas también serán en UTF-8
mb_http_output('UTF-8');

// He aquí una cadena en UTF-8
$string = 'Êl síla erin lû e-govaned vîn.';

// Realizar una operación sobre la cadena con nua función *multibyte*
// Nótese como se hace una substracción a un caracter no-Ascii para fines demostrativos
$string = mb_substr($string, 0, 15);

// Conectar a una base dadtos para almacenar la nueva cadena
// Nótsee el `charset=utf8mb4` en la cadena de conexión (DSN)
$link = new PDO(
    'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
    'your-username',
    'your-password',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_PERSISTENT => false
    ]
);

// Almacenar la cadena resultante en la base de datos
// La base de datos y sus tablas están en codificación utf8mb4
$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 almacenada para comprobar que está correcta stored correctly
$handle = $link->prepare('select * from ElvishSentences where Id = ?');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();

// Almacenar el resultado en un objeto que será enviado en el 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 de UTF-8</title>
    </head>
    <body>
        <?php
        foreach($result as $row) {
            print($row->Body);  // Esto debería mostrar correctamente en el navegador la cadena en UTF-8
        }
        ?>
    </body>
</html>

Lecturas recomendadas (en español e inglés):

Internacinalización (i18n) y localización (l10n)