viernes, 29 de febrero de 2008

Metadata en nombres de archivos PHP

Por suerte con PHP no hay restricciones o estandares a seguir con respecto al nombrado de los archivos PHP. Algo que no es nada nuevo es que podemos usar el nombre del archivo para comunicar más información que solo el nombre del archivo. Algo muy usado es ponerle "archivo.class.php" o directamente "archivo.class" a los archivos PHP que contienen una o más clases. Se puede notar que ni siquiera es requerido que el archivo tenga extensión ".php".

A continuación voy a mostrar algunos ejemplos de nombrado de archivos que pueden ser de utilidad al implementar aplicaciones.


1. Paquetes codificados en el nombre del archivo

PHP no soporta paquetes, lo que es un dolor de cabeza para la implementación de aplicaciones de cierto tamaño. Hay muchas formas de simular la presencia de paquetes en PHP, en este caso se hace con el nombrado de los archivos.

Los paquetes son una setructura jerárquica similar a una estructura de directorios, lo paquetes pueden contener archivos y otros paquetes. Por ejemplo, en Java, los paquetes coinciden con la estructura de directorios en donde se encuentran los archivos (paquetes físicos), en cambio, en C# se pueden definir una especie de paquetes lógicos llamados "namespaces".

Usando el nombre de los archivos para codificar paquetes lógicos nos evitamos problemas como tener dos archivos con el mismo nombre, o simplemente tener un poco más de orden cuando trabajamos con gran cantidad de archivos, y se podrían tener una inmensa cantidad de archivos, en el mismo directorio, perfectamente distinguidos entre si y fáciles de encontrar a simple vista.

Digamos que en nuestro proyecto tenemos 3 paquetes: "core", "core.utils", "core.db". Por otro lado tenemos 100 archivos, 20 del paquete "core", 40 de "core.utils" y otros 40 de "core.db". Y llamamos a los archivos dentro de estos paquetes de la siguiente forma:

- core.UnArchivo.php
- core.OtroArchivo.php
- ...
- core.utils.UnArchivo.php
- core.utils.OtroArchivo.php
- ...
- core.db.MySQL.php
- core.db.PostgreSQL.php
- ...

En principio, estos nombres pueden servir para ver de forma simple que archivos están en que paquete, simplemente listando los archivos ordenados por nombre.

Otra ventaja, ya desde el punto de vista del sistema que estemos construyendo, es por ejemplo hacer un script para cargar o incluir todos los archivos de determinado paquete, por ejemplo, la siguiente función incluye todos los archivos del paquete:

function includeDB( $path )
{
$d = dir($path);

while (false !== ($entry = $d->read())) // Por cada entrada del directorio
{
if (is_file($path . "/" . $entry)) // Si la entrada es un archivo
{
if ( strcmp("core.db", substr($entry,0,7)) == 0 ) // Si el nombre empieza con "core.db"
{
include_once($path . "/" . $entry); // Lo incluye para usarlo
}
}
}
}

Explicación del código:

$path es la ruta del directorio que contiene los archivos. No se verifica que sea correcta, esta verificación se podría agregar para hacerlo mas robusto.

$entry son los archivos del directorio.

Lo demás ya se explica con los comentarios en el código.


Tambien se podria usar el nombrado para hacer un class loader (como el del codigo anterior) mas inteligente, sin necesidad de incluir todos los archivos del sistema, si no incluyendo solo los que se van a usar.



2. Decir que contiene el archivo

Un archivo php puede contener cualquier cosa, ya que no es mas que un archivo de texto.

Pensando en alguno de los posibles contenidos que puede tener un archivo php podría nombrar:

- scripts (en el sentido de tener un código que se ejecuta automáticamente al cargar el archivo)
- funciones
- clases
- interfaces
- paginas (código HTML mezclado con script PHP)

Entonces, podríamos utilizar el nombre del archivo para decir que tipo de contenido tiene, en los casos anteriores podríamos tener los siguientes nombres:

- DownloadFile.script.php
- String.functions.php
- Invoice.class.php
- Clonable.interface.php
- Contacts.page.php


También, esta notación puede servir para saber a simple vista que tiene cada archivo, o también para cargar los archivos por su tipo, por ejemplo si quiero cargar todas las clases de un determinado directorio, puedo usar el código anterior y modificarlo un poco y buscar los archivos que tienen "class" adelante del ".php" como en el siguiente ejemplo:

function includeDB( $path )
{
$d = dir($path);

while (false !== ($entry = $d->read())) // Por cada entrada del directorio
{
if (is_file($path . "/" . $entry)) // Si la entrada es un archivo
{
if ( preg_match("/(.*)class\.php$/", $entry, $matches) ) // Si el nombre tiene "class" antes del ".php"
{
include_once($path . "/" . $entry); // Lo incluye para usarlo
}
}
}
}


Una posible aplicacion de esto es si uno tiene scripts de testing para correr, y algun otro script encargado de correr los tests, y los test se llaman por ejemplo "paquete.Nombre.test.php", puedo buscar todos los tests, incluirlos, correrlos y mostrar los resultados. A simple vista se puede ver el poder con el que se cuenta simplemente por nombrar los archivos de cierta forma.


3. Paquetes y contenido

Y podemos también decir en que paquete esta un archivo y ademas poner que tipo de contenido tiene, por ejemplo:

- core.db.MySQL.class.php
- core.String.functions.php
- ...


Conclusión:

Podemos utilizar los nombres de los archivos para poner metadatos que nos dan información sobre el archivo, que lugar ocupa o que responsabilidad tiene en el sistema y que tipo de contenido tiene. Esa información la podemos utilizar para hacer sistemas mejores, mas eficientes, mas potentes, etc, ademas de que es una buena forma de ordenar los archivos cuando tenemos un gran numero de ellos. Y como vimos es muy simple hacer scripts que procesen los nombres de los archivos, utilizando funciones del file system como dir() para leer nombres de archivos y funciones de strings o expresiones regulares para filtrarlos.

miércoles, 27 de febrero de 2008

Consultas sobre archivos de un directorio en PHP

Problema: necesidad de obtener los nombres de archivos de un directorio filtrados por alguna condición.
Solución: utilizar expresiones regulares para las condiciones sobre los nombres de archivos del directorio.

function getFileNames($path, $match = null, $groups = null)
{
if (is_dir($path))
{
$res = array();
$d = dir($path);

while (false !== ($entry = $d->read()))
{
if (is_file($path . "/" . $entry))
{
$matches = null;
if ($match)
{
if (preg_match($match, $entry, $matches))
{
if (!$groups) $res[] = $entry;
else
{
$gentry = "";
foreach($groups as $i)
{
$gentry .= $matches[$i];
}
$res[] = $gentry;
}
}
}
else // Si no paso match, le entrego derecho la entrada.
{
$res[] = $entry;
}
}
}
$d->close();
return $res;
}
else
{
throw new Exception("El directorio: $path no existe.");
}
}


Explico el código:

function getFileNames($path, $match = null, $groups = null)

path: directorio del cual se quieren leer los nombres de archivos
match: expresión regular con la cual se filtran los nombres de archivos (si no se pasa nada, se devuelven los nombres de todos los archivos del directorio)
groups: solo se usa si match no es null, y se usa para seleccionar los grupos de la expresión regular cuando coinciden los nombres de los archivos con match. Más adelante explicaré como es que se usa, pero la idea es poder sacar pedazos del nombre del archivo y devolver eso, por ejemplo si tenemos infomración codificada en el nombre como: paquete.Clase.class.php, y me interesa devolver solo el nombre de la clase, o sea "Clase". Si groups es null se devuelven los nombres de todos los archivos que coincidan con la expresión regular match.

if (is_dir($path))
Verifica si path es un directorio válido, si no lo es lanza una excepción.

$res = array();
Se crea la lista de nombres a devolver, todos los nombres de archivos que coincidan con los criterios serán incluidos en res.

$d = dir($path);
Se abre el directorio para comenzar a leer sus entradas. Para entender como trabaja dir() visita el manual.

while (false !== ($entry = $d->read()))
Recorre cada entrada del directorio, estas son archivos y subdirectorios, entry es el nombre del archivo o subdirectorio actual en la recorrida.

if ($match)
{
if (preg_match($match, $entry, $matches))
Si match no es null, verifico que entry coincida con él. Para entender como trabaja preg_match, visita el manual. En matches se guardan los grupos definidos en match que coincidieron con entry, y si groups no es null los voy a usar para armar los nombres que voy a devolver.

if (!$groups) $res[] = $entry;
else
{
$gentry = "";
foreach($groups as $i)
{
$gentry .= $matches[$i];
}
$res[] = $gentry;
}
Si groups es null, simplemente agrego entry a la solución, o sea, devuelvo el nombre del archivo así como está y se que ese nombre coincide con la expresión regular match.
Si groups no es null, busco en matches los grupos indicados por groups, concatenando dichos grupos, la concatenación de todos los grupos seleccionados es el nombre a incluir en la solución. De esta forma, si los archivos tienen nombres por ejemplo "paquetes.Clase.class.php", podría devolver "Clase.php", luego mostraré ejemplos de como se hace esto.

else // Si no paso match, le entrego derecho la entrada.
{
$res[] = $entry;
Si match es null, simplemente agrego a la solución el nombre del archivo sin procesarlo.

$d->close();
return $res;
Cierra el directorio y devuelve la solución.


Algunos ejemplos de uso:

Para entender que hacen necesitas entender las expresiones regulares usadas, si no sabes mucho de expresiones regulares te aconsejo que busques en google algún manual de expresiones regulares.

En estos casos usaremos como directorio "." que es el directorio actual. Puedes cambiarlo y utilizar otro directorio si lo deseas.

getFileNames("."); // Todos los archivos, no hay ninguna restricción.

getFileNames(".", "/\.php$/i"); // Todos los archivos con extensión ".php"

getFileNames(".", "/^utils\.(.*)\.php$/i"); // Todos los ".php" del paquete utils (pensado para nombres con la forma "paquete.Class.php", en este caso paquete es "utils"). Observar el "(.*)", eso es un grupo y se podría usar el parámetro groups para obtenerlo, como veremos en el siguiente ejemplo.

getFileNames(".", "/(.*)\.php$/i", array(1)) ); // Todos los ".php", pero sin el .php (ver que la regexp match tiene un grupo y en el array selecciono ese grupo). El grupo 0 (cero) coincide con el nombre del archivo.

getFileNames(".", "/^utils\.(.*)\.php$/i", array(1,5,0)) ); // Todos los php del paquete utils, idem anterior, ahora sin el "utils." Observar todo lo que se pasa en groups, ver que el grupo 5 no existe y por lo tanto no es considerado, luego si tengo un archivo "utils.Class.php", el resultado de esto es "Classutils.Class.php", porque concatena el grupo 1 ("Class") y el grupo 0 (que es el nombre del archivo), y ver que se respeta el orden establecido en groups.


En conclusión, no solo se logró hacer una función que resuelve el problema, si no que se obtuvo una solución que puede hacer un preproceso de los nombres a devolver (gracias a groups), lo que permite extraer información presente en los nombres de los archivos y concatenarla a gusto.

Siéntanse libres de utilizar este script, modificarlo a gusto, hacer comentarios y mejoras sobre él. Y si lo incluyen en algún proyecto no se olviden de hacer referencia a este blog y/o a este post.