Archivos de Categoría: Cómo hacer un framework ligero en PHP

Cómo hacer un framework ligero en PHP (III) – Control de errores y excepciones

El control de errores… uno de mis temas favoritos y sobretodo uno de los temas peor resueltos en PHP como lenguaje y especialmente por parte de la inmensa mayoría de desarrolladores de PHP.

Uno puede pensar que tampoco es muy importante, que total, si peta pues peta, que eso no pasa casi nunca y ya está pero en webs de alta disponibilidad o aplicaciones con mucha concurrencia de usuarios pasan cosas extrañas y si no tenemos un buen control de errores nos podemos volver locos buscando por qué en algunas ocasiones falla nuestra aplicación.

Los puntos principales a tener en cuenta son:

Exterminio absoluto de die salvo necesidad imperiosa

Qué tiempos aquellos en los que hacíamos al principio de las páginas lo siguiente:

<?php
   $db = @mysql_connect('localhost', 'root', '') or die('Error conectando a servidor de BBDD');
   @mysql_select_db('mi_base_datos', $db) or die('Error seleccionando BBDD');
   $sql = 'SELECT * FROM tabla';
   $rs = @mysql_query($sql, $db) or die('Error SQL '. $sql. ' Mensaje:'.mysql_error());
?>

Este código puede parecernos bien, sirve para cuando das clase de iniciación a la programación de webs (y no queremos asustar a la gente con PDO y demás abstracciones) y todos hemos empezado haciendo cosas de este tipo.

Aparentemente, nos avisa de cualquier error conectando a la BBDD, el operador @ nos suprime los warnings de PHP si algo falla y tenemos salidas limpias de la aplicación.

Pero este código tiene varios problemas:

- No es demasiado mantenible, aunque podemos poner esto en funciones o clases y quedaría algo mejor.

- No lanza excepciones de ningún tipo y es difícil recoger el error y hacer un redirect a las tan de moda actualmente páginas de error 500 y error 400 (Como por ejemplo esta).

- Y LO PEOR DE TODO. Estamos devolviendo un Status Code 200 a nivel de Apache. Y diréis y qué más me da. Pues supongamos que tenemos un blog humilde como éste y que Google no nos tiene demasiado aprecio y pasa una vez a la semana o menos. Supongamos que en ese momento que pasa Google, nuestro ISP ha recibido un ataque de esos maravillosos que tiran todo el MySQL y en ese momento, nuestro fantástico control de errores reacciona y nuestras páginas lucen esplendorosas con el ‘Error conectando a servidor de BBDD’. Pues bien, Google interpretará ese mensaje de error como una página perfectamente válida (debido al status 200), todo nuestro site tendrá el mismo contenido duplicado y nuestro flojo pagerank se hundirá estrepitosamente. Ya no nos gusta tanto, ¿verdad?

Sin embargo, die o exit a veces pueden ser buenas opciones si estamos desarrollando una API de conexión y en algunos casos queremos devolver un mensaje concreto a los que consuman nuestros datos.

Y si nuestra página no es muy grande y no queremos matarnos, bastaría con lanzar un header con el status code correspondiente (500 para errores, 400 para bad requests, etc) antes de hacer el die o exit y Google ya no indexará ese mensaje de error como una nueva versión de la página.

En cualquier caso, recomiendo imperiosamente hacer lo que comento en los dos puntos siguientes y así nuestra aplicación quedará molona y no quedará ningún error por tratar.

Convertir los warnings y otros errores de PHP en excepciones

PHP nunca se ha caracterizado por tener un buen control de excepciones como Java. Cuando un fopen o un mysql_connect fallan, PHP devuelve un warning pero sigue ejecutándose el script. Esto en pequeñas aplicaciones es sencillo de controlar pero en aplicaciones grandes se vuelve algo inmanejable.

Para conseguir convertir los errores de PHP en Exceptions hay que hacer lo siguiente:

<?php   
   set_error_handler('myErrorHandler');
   function myErrorHandler($code, $error, $file = NULL, $line = NULL) {
      throw new Exception($error . ' encontrado en '. $file.', línea '.$line);
   }
 
   try {
        $fd = fopen('ficheronoexiste', 'r');
   } catch(Exception $ex) {
         echo 'Error cacheado '. $ex->getMessage().PHP_EOL;
   }
?>

Con este código veremos que en vez de obtener el clásico
PHP Warning: fopen(ficheronoexiste): failed to open stream: No such file or directory in fichero.php on line 8

Se captura la excepción correctamente y entramos al catch, para acabar viendo
Error cacheado fopen(ficheronoexiste): failed to open stream: No such file or directory encontrado en fichero.php, línea 8

Esto no parece un gran avance pero si somos algo ingeniosos podemos capturar los errores de PHP y notificar por mail, realizar algún handling mejor y hacer redirects limpios a páginas de error 500.

Tener un único punto de try / catch y intentar tener varios tipos de excepciones

Vale, no es necesario tener un único punto de try/catch pero muchas veces se abusa sin saber cuando no es necesario. Con un único try / catch en nuestro archivo index.php si se lanza una excepción desde cualquier clase de las profundidades, ese error sube y es capturado correctamente desde el archivo base.

Además puede ser interesante hacer algo como lo siguiente:

<?php
   // Creamos una exception especial
   class PHPException extends Exception { }
 
   // Deberíamos hacer que nuestra clase de BBDD en vez de lanzar Exceptions genéricas lance DBExceptions...
   class DBException extends Exception { }
 
   // Separamos las excepciones de código
   set_error_handler('myErrorHandler');
   function myErrorHandler($code, $error, $file = NULL, $line = NULL) {
      throw new PHPException($error . ' encontrado en '. $file.', línea '.$line);
   }
 
   try {
 
    // Todo el código que queramos instanciando N clases, con herencias y todo lo que queráis
 
   } catch(PHPException $ex) {
       // Informamos al equipo de desarrollo, pasando por ejemplo el print_debug_backtrace()
       // headers y enviar a alguna página de error "bonita"
   } catch(DBException $ex) {
       // Informamos al DB Admin via mail por ejemplo
       // headers y enviar a alguna página de error "bonita"
   } catch(Exception $ex) {
       // Aquí llega cualquier otro tipo de Exception, veremos lo que hacemos
       // por norma general header 500 y página bonita
   }
 
?>

Urgh… esto empieza a parecerse a Java pensarán algunos. Pues… sí y no. En Java hay que controlar los N-mil tipos de Exceptions que pueden saltar por leer un fichero (aunque creo que en Java6 esto ha mejorado).

En cambio en PHP, es algo diferente. Cualquier tipo Exception o hija de Exception (que ahí esta la gracia) entra en el tercer catch, y las dos primeras, que son algo especiales, las tratamos algo diferente.

Queda claro que se puede tener un control de excepciones bastante digno en PHP, algo menos verbose que en Java y prácticamente igual de potente y funcional. Si revisáis muchos proyectos open-source veréis que casi nadie hace esto y es difícil capturar los errores correctamente. Afortunadamente, muchos frameworks ya hacen esto por nosotros y poco a poco las cosas van mejorando.

Espero que os haya gustado, próximamente más sobre los frameworks ligeros y un ejemplo de cómo se comunican controlador -> modelo -> vista.

Cómo hacer un framework ligero en PHP (II) – Parseo de URL

Seguimos con los posts sobre cómo hacer un framework ligero en PHP.

En esta ocasión trataremos el tema del parseo de la URL, muy habitual en el patrón MVC y por tanto en todos los frameworks existentes.

Por otro lado, aunque había anunciado que también trataría el tema del __autoload, prefiero dejarlo para más adelante y intentar poner un ejemplo usando ya los namespaces, tema que siendo sinceros llevo algo verde.

Para parsear la URL, tenemos que fijarnos en dos claves importantes dentro del array superglobal $_SERVER:

$_SERVER['SERVER_NAME'] : Contiene el dominio que está ejecutando la página, sin la parte ‘http://’ por ejemplo www.ricardclau.com
$_SERVER['REQUEST_URI'] : Contiene el resto de la URL solicitada, con los parámetros $_GET si los hay. Por ejemplo la página wordpress de este artículo contendría “/2011/02/como-hacer-un-framework-ligero-en-php-ii-–-parseo-de-url/”

Y a partir de esto, podremos determinar el entorno en el cual estamos ejecutando, el idioma de la página si queremos hacer una aplicación multiidioma (pudiendo ser parte de una o de otra, esto ya va a gustos), el controlador a ejecutar y su acción y parámetros. ¡Vamos a ello!

Obtención de entorno de ejecución

Para esto podemos usar varias técnicas, una de las más aceptadas es obtener el TLD del servidor y según eso, definir una constante ENVIRONMENT que podremos usar en toda nuestra aplicación.

Es decir, para las terminaciones de dominio: COM -> PRODUCCION, TEST -> INTEGRACION, DEV -> DESARROLLO

Obtención de idioma (opcional)

Aquí hay dos corrientes, ambas bastante difundidas, yo me quedo con la segunda pero es algo muy personal.

- Utilizar el $_SERVER['SERVER_NAME'] con un primer elemento que indique el país o idioma que queramos usar. Es decir es.ricardclau.com sería la versión en castellano y ca.ricardclau.com la versión en catalán. Para ello, bastaría con coger lo que va delante de ricardclau.com y evaluar posibilidades.

- Utilizar la primera parte del REQUEST_URI para el idioma (e incluso para la parte de administración). En este caso www.ricardclau.com/ca sería la versión en catalán, www.ricardclau.com/es la versión en castellano y incluso nos permitiría jugar con www.ricardclau.com/admin para gestionar la zona de administración y tener un front-controller específico, con cosas como www.ricardclau.com/images para gestionar temas de redimensionamiento automático (como veremos en próximos posts), etc…

Yo suelo utilizar esta opción en las aplicaciones que desarrollo, principalmente por el juego que da pero ambas son perfectamente válidas.

Resto de la URL para tener MVC

Aquí de nuevo cada framework lo hace un poco diferente. En este sentido me gusta la versión Zend que se basa en
/controlador/accion/param1/valor1/param2/valor2 y además permite internacionalizar de manera que por ejemplo www.ricardclau.com/ca/contacte, www.ricardclau.com/es/contacto y www.ricardclau.com/en/contact vayan al mismo controlador.

Mi querido Kohana tiene un modelo de rutas bastante diferente y al final para conseguir algo como esto tuve que hacer un pequeño hack que en próximos días publicaré.

En cualquier caso, una vez obtenidos el controlador y la acción (pudiendo definir unos valores por defecto, como public para el controlador y index para la acción) otra acción habitual es agrupar esos params, $_POST y $_GET y pasarle a la acción del controlador un array $params con el array_merge de los 3.

El resultado final sería algo así como:

<?php
 
// SIEMPRE TRY / CATCH !!!!!
try {
 
    $s_server = $_SERVER['SERVER_NAME'];
    $q_string = $_SERVER['REQUEST_URI'];
 
    // Limpieza de Query String con el filtro de PHP
    // Además, eliminamos la primera /
    // Y de paso, quitamos la última / si existe...
    // así /public/index = /public/index/
    $q_string = trim(filter_var($q_string, FILTER_SANITIZE_URL), '/');
 
    // Obtención de entorno -> Obtención de TLD
    $entorno = substr($s_server, strrpos($s_server, '.') + 1);
 
    // Entornos válidos
    $entornos_validos = array(
                  'local'   => 'LOCAL',
                  'dev'     => 'DEVELOPMENT',
                  'test'    => 'TESTING',
                  'com'     => 'PRODUCTION',
                  );
    if(!array_key_exists($entorno, $entornos_validos))
         throw new Exception('Entorno inválido');
 
    // Definimos constante
    define('ENVIRONMENT', $entornos_validos[$entorno]);
 
    // Obtenemos los trozos del querystring
    // 1. Eliminamos la parte de $_GET si existe, partiendo por ?
    if(strpos($q_string, '?') !== FALSE)
        $q_string = substr($q_string, 0, strpos($q_string,'?'));
    // 2. Nos montamos un array con los trozos
    $q_pieces = explode('/', $q_string);
 
    // Obtención de idioma
    // Versión 1: es.ricardclau.com
    $idioma = substr($s_server, 0, strpos($s_server, '.'));
    // Versión 2: www.ricardclau.com/es
    $idioma = array_shift($q_pieces);
 
    // Idiomas válidos (y dejamos uno por defecto incluso!)
    $idiomas_validos = array('es', 'ca', 'en');
    $idioma_defecto = 'en';
    if(!in_array($idioma, $idiomas_validos))
         $idioma = $idioma_defecto;
 
    // Y ya tenemos la constante
    define('LANGUAGE', $idioma);
 
    // Construcción MVC (por defecto, controlador public, acción index)
    $controller = (empty($q_pieces[0])) ? 'public': $q_pieces[0];
    $action = (empty($q_pieces[1])) ? 'index': $q_pieces[1];
 
    // Resto de parámetros, los recogemos como /clave1/valor1/clave2/valor2
    $params = array();
    for($i=2;$i<count($q_pieces);$i++) {
            $params[$q_pieces[$i++]] = $q_pieces[$i];
    }
 
    // Unimos estos parámetros con lo que pudiera venir por $_POST y $_GET
    // (Faltaría hacer algo de saneamiento, que podemos hacer aquí o con validadores)
    $c_params = array_merge($params, $_POST, $_GET);
 
    // Instanciamos controlador y ejecutamos la acción más sus parámetros
    // Debido a la falta de namespaces, para no colisionar las clases de controladores
    // se suelen concatenar los nombres con la palabra Controller, a las acciones se les pone
    // la palabra Action, etc... con los namespaces esto está cambiando
    $cname = $controller . 'Controller';
    $actionname = $action . 'Action' ;
    $c = new $cname;
    $c->$actionname($c_params);
 
 
} catch (Exception $ex) {
   // Aquí depende mucho de lo que queramos hacer...
   echo $ex->getMessage();
   // O incluso mejor, si venimos de una clase de las profundidades, veremos
   // la secuencia de clases, aquí no veremos nada ya que todo pasa en el index.php
   debug_print_backtrace();
}
?>

Lógicamente esto tiene multitud de puntos a cambiar, mejorar, pero espero haber aportado algo de luz a cómo los frameworks hacen magia con nuestras URLs para pasarlas a nuestros controladores.

Cómo hacer un framework ligero en PHP (I) – Introducción y .htaccess

Esta serie de posts pretende enseñar cómo podemos hacernos un bootstrap ligero para nuestras aplicaciones PHP que nos sirva de base sobre la cual empezar a trabajar.

Personalmente, soy partidario de no re-inventar la rueda y intentar usar convenciones y frameworks robustos y probados en mil aplicaciones. Básicamente pienso que si algo es usado, revisado y creado por muchas personas probablemente será mucho mejor que cualquier cosa que creemos solos en nuestro garaje, por muy buenos que nos creamos.

Sin embargo, también creo que está bien saber cómo funcionan estos frameworks, tanto a nivel laboratorio como para poder trabajar mejor con ellos. Últimamente hay mucha gente que programa en Zend Framework, en Symfony, o en cualquiera de los grandes pero realmente no tiene ni idea de cómo van y lo que es peor, bastante poca idea de la base de PHP.

Para mí, el bootstrap de una aplicación web debería como mínimo hacer lo siguiente:

- Tener un autoload de clases, modelos, controladores, helpers, etc basado en una convención de ficheros y directorios que podamos definirnos.
- Parsear la URL según un esquema (Lo habitual suele ser algo estilo http://dominio/controlador/accion/param1/valor1/param2/valor2 aunque hay variantes) y redirigir la petición al controlador correspondiente.
- Configuración de la aplicación a nivel de parámetros de PHP (y ligado con esto la gestión de entornos).
- Establecer la comunicación con la BBDD, Memcached y resto de recursos que usamos en la aplicación a partir de ficheros de configuración.
- Control de sesiones (en BBDD, Memcache o nativas), especialmente en aplicaciones que comparten HTTP y HTTPS.
- Tener gestión de idiomas, ya que hoy en día es casi obligatorio en cualquier web que se precie.
- Control ROBUSTO de errores y excepciones (supongo que todos hemos sufrido las carencias de PHP, especialmente en versiones anteriores y muy especialmente las herencias de malos programadores que un día decidieron hacer un framework super-molón de la muerte que nadie más podría tocar).

Es bastante interesante también tener lo siguiente:

- Usar algún sistema de plantillas como Smarty o los nuevos que lo quieren destronar como Twig (aunque PHP plano nos puede valer muchas veces).
- Control de llamadas AJAX / No AJAX / Otros mimetypes.
- Permitir usar el framework con CLI, muy útil para Unit Testing pero también en procesos que pueden durar mucho rato, como un envío masivo de mails, crons de limpieza, etc…
- Sistema fácil de añadir librerías ya existentes para tareas habituales como: tratamiento de imágenes, gestión de mails, generación de PDFs…

Como veis, hay bastantes cosas y muchas veces sale a cuenta coger algo testeado y comprobado por mucha gente.

En los próximos artículos iremos desglosando todo esto y quién sabe… ¡a lo mejor hasta queda algo digno y presentable!

Para empezar, hablaremos del archivo .htaccess. Existen muchas versiones, muchas de ellas muy profesionales, pero la base de un .htaccess para un framework PHP debería ser algo similar a esto (cogido de Kohana):

# Turn on URL rewriting
RewriteEngine On
 
# Installation directory
RewriteBase /
 
# Protect hidden files from being viewed
<Files .*>
	Order Deny,Allow
	Deny From All
</Files>
 
# Protect application and system files from being viewed
RewriteRule ^(?:application|modules|system)\b.* index.php/$0 [L]
 
# Allow any files or directories that exist to be displayed directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
 
# Rewrite all other URLs to index.php/URL
RewriteRule .* index.php/$0 [PT]

Con estas líneas poco inteligibles (no soy un gran experto pero podéis buscar más información en Google) básicamente le estamos diciendo a nuestro apache que si recibe una URL que pide algo inexistente en el disco, nos envíe la petición al archivo index.php que intentará tratar esa petición.

Además, en el caso de Kohana, prohibe ver las carpetas application, modules y system (donde está la base del framework) y protege también los archivos ocultos Linux (que empiezan por .)

Hoy en día todos los hostings de medio pelo permiten subir un archivo .htaccess para este fin. Si el vuestro no lo hace, sinceramente, dejad de trabajar con ellos.

Por hoy lo dejamos, el próximo día trataremos el parseo de la URL y un posible autoload!