Cargador de clases en PHP4
En este artículo voy a presentar una clase que permite independizar en gran medida una aplicación de las ubicaciones en las que están las clases que la componen. En lenguajes como Java o C# esto es algo completamente transparente, pero no así en PHP4.
En PHP4 cuando se requiere utilizar una clase es necesario haberla incluido antes en nuestra aplicación con un require
o un include
, indicando la ruta del archivo en la que está amalcenado. Conforme crece el tamaño de las aplicaciones es habitual que también crezca el número de clases existentes y las dependencias que hay entre los diferentes programas que forman la aplicación y ellas. Esto produce que si en algún momento necesitamos mover una clase de sitio tengamos que tocar un buen número de archivos, y casi siempre con la mala suerte de que siempre se olvida alguno que va a fallar en el momento más inesperado.
Una solución habitual suele ser centralizar en un único archivo la inclusión de todas las clases que forman la aplicación, sin embargo, normalmente en cada petición de página se suele necesitar únicamente un subconjunto de todas las clases disponibles, por lo que considero que esto es muy ineficiente, aparte de que nos obliga a hacer un mantenimiento bastante tedioso de este archivo.
La solución que suelo utilizar es disponer de una clase, de nombre ClassLoader, que me abstrae de la ubicación real de las clases y que es la encargada de incluirlas cuando se lo solicito. De esta forma, desde el código de la aplicación se le pide que se cargue una determinada clase y ella hace la inclusión real del archivo, para lo cual es necesario saber la ubicación de cada clase. En esta ocasión esta información se obtiene de forma automática a partir de unas indicaciones, para lo cual la clase realiza una búsqueda de archivos susceptibles de ser clases a partir de unas ubicaciones que hay que indicarle y almacena esta información en caché, de forma que no sea necesario realizar la búsqueda en cada petición de página.
Antes de entrar en más detalle hay que aclarar que habitualmente es aconsejable hacer que en cada archivo haya una única clase, y que el nombre de la clase coincida con el del archivo, principalmente por cuestiones de facilidad de mantenimiento y crecimiento de la aplicación. En mi caso si un archivo contiene una clase de nombre MiClase
, el archivo que la contendría se llamaría MiClase.class.php
. Pese a esto, nada nos obliga a ello, y el sistema que explico a continuación funcionaría con archivos que contuviesen varias clases, aunque insisto, no es lo más aconsejable. En cuanto a los nombres de las clases, dado que en PHP4 no hay namespaces, lo que suelo hacer es hacer que los nombres de las clases sean más largos, por ejemplo, lo que en .NET podría ser una clase Contents.DAO.Board, en PHP4 lo renombraría como una clase ContentsDAOBoard, almacenada en el archivo ContentsDAOBoard.class.php.
Volviendo a la clase ClassLoader, voy a explicar a continuación su funcionamiento, para lo cual comenzaré mostrando un sencillo ejemplo de su utilización. Imaginémonos que tenemos una aplicación dividida en frontend y backend, y que las clases que forman el primero están en la carpeta /includes del web y las del segundo en /admin/includes.
<?php include_once('ClassLoader.class.php'); $classLoader =& new ClassLoader(); $classLoader->addPath(dirname(__FILE__).'/includes', 1); $classLoader->addPath(dirname(__FILE__).'/admin', 1); $classLoader->start(); $classLoader->includeClass('MyClass.class.php'); print_r($classLoader->_classes); ?> |
Lo primero que hay que hacer es crear una instancia de ClassLoader, para lo cual es necesario incluir el archivo donde está definida, a partir del momento en que creemos la clase ya no será necesario saber la ubicación exacta del resto de clases que utilicemos. Una vez incluido el archivo que contiene la clase, creamos una instancia con el nombre $classLoader
. El constructor de la clase tiene 4 parámetros posibles con valores por defecto, y que en la mayoría de los casos no es necesario utilizarlos. Estos parámetros son:
- $exp: indica en segundos el tiempo de validez de la información de clases encontradas, de forma que no sea necesario regenerarla en cada petición de página. Por defecto son 3600 segundos.
- $cache_name: nombre del archivo en que se guardará la información de las clases encontradas, por defecto
classLoader.dat
- $cache_folder: carpeta del sistema en el que se guardará el archivo con la información de las clases, por defecto
/tmp
. - $pattern: patrón de expresión regular que deben cumplir los archivos para que se considere que contienen una clase, por defecto todos los archivos acabados en
class.php
, para lo cual se utiliza el patrón\.class\.php$
Tras crear la instancia $classLoader
le indicamos todas aquellas rutas en las que queremos que localice archivos de clases, indicando para cada una su ruta completa y si la búsqueda ha de ser recursiva (1) o no (0). Al indicar las rutas completas es conveniente no hacerlo escribiendo directamente el path, sino utilizar algún otro mecanismo como $_SERVER['DOCUMENT_ROOT']
o dirname(__FILE__)
, de forma que podamos mover la aplicación de una ubicación a otra, o pasarla de desarrollo a producción sin tener que tocarla.
Una vez indicadas todas las rutas es el momento de realizar la activación de la clase llamando al método start
, el cual es el encargado de recuperar la información de caché si es que existe y no ha expirado, o de realizar la búsqueda de clases en las rutas indicadas en caso contrario. A partir de este momento cada vez que queramos incluir una clase basta con llamar al método includeClass
indicándole el nombre del archivo que la contiene, por ejemplo $classLoader->includeClass('MyClass.class.php');
.
Para terminar, merece la pena hacer mención de dos cuestiones que pueden suceder y que suelen hacer perder bastante el tiempo si no estamos al corriente de ellas. Por un lado cuando creemos una nueva clase o la movamos de ubicación, es necesario eliminar siempre el archivo de caché, de forma que se vuelva a regenerar en la siguiente petición. Como esto suele ser algo habitual en el desarrollo, habitualmente es aconsejable indicar un tiempo de expiración de 0, con lo que en cada petición se regeneraría la información de clases, por supuesto en función del tamaño del proyecto. Una vez que la aplicación está estable o se pasa a producción, se pueden indicar tiempos mayores.
Por otro lado hay que tener en cuenta también la visibilidad de la instancia $classLoader que creamos y desde dónde queremos acceder a ella. Por ejemplo, si quisiéramos acceder a ella desde una función, o desde el método de otra clase, sería necesario hacerla visible mediante global
, o utilizar la matriz de globales de la forma $GLOBALS['classLoader']->includeClass('MyClass.class.php');
. En el caso de que necesitemos crear la instancia desde dentro de una función o método, hay que hacer que se cree como global, para lo cual lo se puede crear la instancia a través de la matriz de globales, de la forma $GLOBALS'[classLoader'] =& new ClassLoader();
.
Este es el código de la clase por si quieres echarle un vistazo antes de descargarlo.
<?php /** * Class Loader for PHP applications * * Provides an easy way for make the PHP code independent from the physical * location where are located the php files containing the classes * definitions of your app. * * It uses an internal structure with the real location of every class available. This * structure is build searching in configurable locations for files with name ".class.php", * both configurable, and saving it to file, to avoid the search in any execution. * * The main advantage that offers is that it allows us to free the application from * the specific location of the classes, so we can move the classes between different * folders or rearrange them without modify the code of the application * * LICENCE * ======== * copyright (c) 2000 Patxi Echarte [patxi@eslomas.com] * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details at * http://www.gnu.org/copyleft/lgpl.html * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * @package ClassLoader * @version $Id: ClassLoader.class.php,v 1.4 2006/02/23 $ * @author Patxi Echarte <patxi@eslomas.com> * */ class ClassLoader{ /** * Almacena cada ruta sobre la que hay que buscar clases indicando * si la búsqueda debe ser recursiva (1) o no (0) * [[path,recursive{0,1}]] */ var $_paths = null; /** * Patrón que deben cumplir los archivos para considerarse como clases */ var $_patternClassName = ""; /** * Matriz con las clases encontradas, se guarda como clave el nombre * del archivo (NO el de la clase almacenada en el archivo) y como * valor el del path donde está ubicada */ var $_classes = null; /** * Tiempo en segundos de validez de la información obtenida */ var $_expirationTime = 3600; /** * Nombre del archivo donde guardar la información de caché */ var $_cacheFileName = ""; /** * Constructor de la clase * * @param $paths * @param $pattern * @param $cache_name * @param $cache_folder */ function ClassLoader($exp=3600, $cache_name='classLoader.dat', $cache_folder='/tmp', $pattern='\.class\.php$') { $this->_expirationTime = $exp; $this->_cacheFileName = realpath($cache_folder).'/'.$cache_name; $this->_patternClassName = $pattern; } /** * Añade un path * @param string $path ruta completa desde la raíz del sistema * @param bool $recursive (1|0) que indica si hay que buscar clases * de manera recursiva en el path */ function addPath($path, $recursive=1) { if(ereg('\/$',$path)) $path = substr($path,0,-1); if(!file_exists($path)) trigger_error("No existe el path indicado: [$path]", E_USER_ERROR); else $this->_paths[] = array(realpath($path), $recursive); } /** * Pone en marcha el cargador de clases, para lo cual se buscan las clases * disponibles o se recuperan de caché */ function start() { if(!$this->_getCachedData()){ $this->_rebuildClassesInfo(); $this->_setCachedData(); } } /** * Busca clases en las ubicaciones que se han indicado */ function _rebuildClassesInfo() { $this->_classes = array(); foreach($this->_paths as $path_info){ $this->_searchDir($path_info[0], $path_info[1]); } } /** * Función recursiva, que dado un directorio busca en él y en todos sus hijos (si así se indica) * archivos que cumplan con el patrón indicado, almacenando cada coincidencia en la matriz de clases */ function _searchDir($directory, $recursive=1){ if ($id_dir = @opendir($directory)){ while (false !== ($file = readdir($id_dir))){ if ($file != "." && $file != ".."){ if($recursive && is_dir($directory.'/'.$file)){ $this->_searchDir($directory.'/'.$file, $recursive); } else{ if(ereg($this->_patternClassName, $file)) { if(isset($this->_classes[$file]) && $this->_classes[$file] != ''){ trigger_error("Se ha encontrado un archivo de clase " ."duplicada [$directory/$file]: ".$this->_data[$file], E_USER_ERROR); } else $this->_classes[$file] = realpath($directory.'/'.$file); } } } } closedir($id_dir); } } /** * Devuelve el path del archivo de clase indicada */ function getPathForClass($cln){ return $this->_data[$cln]; } /** * Incluye el archivo necesario para tener disponibles la clase/s que contiene */ function includeClass($cln){ if(!isset($this->_classes[$cln]) || $this->_classes[$cln]==''){ trigger_error("La clase indicada no está definida: ".$cln); } else{ include_once($this->_classes[$cln]); } } /** * se realiza la inclusión de todas las clases encontradas */ function includeAllClasses(){ foreach($this->_classes as $name => $path) include_once($path); } /** * Comprueba si hay información en caché y si es válida, en cuyo caso la carga * y devuelve true, eoc devuelve false */ function _getCachedData() { if( ! file_exists($this->_cacheFileName) || (filemtime($this->_cacheFileName) < time()-$this->_expirationTime) ){ return false; } $fp = fopen($this->_cacheFileName, 'r'); $this->_classes = unserialize(fread($fp, filesize($this->_cacheFileName))); fclose($fp); return true; } /** * Almacena la información de clases en caché para no tener que realizar * la búsqueda de clases en cada petición de páginas */ function _setCachedData() { if($fp = fopen($this->_cacheFileName, 'w')){ flock($fp, LOCK_EX); fwrite($fp,serialize($this->_classes)); flock($fp, LOCK_UN); fclose($fp); } } } ?> |
Muy bueno!
¿El metodo _getCachedData() no deberia terminar con un return 1?
Pues en parte tienes razón, el programa funciona correctamente ya que la comprobación booleana que se hace más arriba se sigue cumpliendo aunque no devuelva nada, que se considera diferente que false. En cualquier caso lo correcto es poner lo que pone en el comentario del método, return false para el caso de no acierto en caché y return true para los aciertos en caché, problemas del copy&paste.
El asunto es que esta clase la he resumido de la que utilizo normalmente, que está integrada con otras piezas, y al sacarla la tendría que haber probado algo más, pero cuestiones de falta de tiempo no me lo han permitido 🙁
Aparte de lo que comentas he añadido una modificación para evitar posibles problemas al utilizar la clase en sistemas windows, por lo que si es tu caso te recomiendo que vuelvas a copiar el código completo de la clase.
Copio y quedo a la espera de tu proximo articulo.
Saludos.
Excelente tu clase tengo que desarrollar una aplicación en 3 capas y el acoplamiento me mataba, gracias por el post