miércoles, 12 de octubre de 2016

Carga automática de clases. Parte II.

Tras haber visto un caso muy básico de cómo crear un atuloader en php llegamos al caso más habitual en el que tenemos una estructura de carpetas. Si organizamos el caso básico donde todo estaba en el mismo directorio podríamos tener la siguiente estructura.
[code] Directorio: Proyecto:
    Directorio: Entities
        Account.php
        Output.php
    autoload.php
    test_main_autoload.php [/code]

Parte II. Autoload: Implementación de PSR-4.


El primer pensamiento sería añadir a la función spl_auload_register del caso básico la nueva ruta para los requires, pero volveríamos al problema inicial si tenemos x directorios. Crear código que fuese comprobando en cada subdirectorio antes de pasar al siguiente si existe el fichero y si es así requerirlo no es algo muy eficiente; esto lo descartamos directamente.

Lo primero que debemos hacer, para encarar una solución óptima, es hacer que cada fichero sepa en qué directorio está. ¿Cómo? Utilizando namespaces.

¿Qué son los namespaces? .Digamos que es la forma de paquetizar ficheros en un mismo ámbito, es decir, es un espacio que creamos bajo un mismo nombre para agrupar clases, variables, funciones,… Es similar a los packages de java (sólo similar). Cada namespace puede tener el nombre que queramos poner.

En este caso pondremos a cada namespace un nombre base e iremos añadiendo la ruta dónde se encuentra cada directorio simulando una jerarquía de namespaces.

Veamos el fichero principal que está en el namespace base.

[code] namespace blog\autoload; use blog\autoload\Entities AS ent; require_once "autoload.php"; try { $account = new ent\Account(5); $output = ent\Output::getOutput(ent\Output::CONSOLE); $account->addAmount(2000); $account->payCommision(); $account->addAmount(50); $account->recoverAmount(20); $output->print($account->getTotal()); } catch (Exception $e) { echo($e->getMessage().PHP_EOL); } [/code]
Vemos que lo primero que se hace, en la primera línea, es asignar el fichero al namespace base blog\autoload (podemos poner el nombre que queramos) y a continuación utilizamos el namespace blog\autoload\Entities (base + primer directorio) y le damos un alias para acortarlo.

Ahora vamos a ver las clases Account y Output pertenecientes al namespace blog\autoload\Entities.

Clase Account:
[code] namespace blog\autoload\Entities; class Account { private $commision; private $amount; public function __Construct(int $percentage) { $this->commision = $percentage; } public function addAmount(int $amount) { assert($amount >= 0); $this->amount += $amount; } public function payCommision() { $this->amount = ( ( 100 - $this->commision ) * $this->getTotal() ) / 100 ; } public function recoverAmount(int $amount) { assert($amount >= 0); $this->amount -= $amount; } public function getTotal() : int { return $this->amount; } } [/code]
Clase Output:
[code] namespace blog\autoload\Entities; class Output { const CONSOLE = 1; const FILE = 2; private function __construct() { // Configure output } public static function getOutput(int $type) : Output { // Aquí deberíamos tener un factory method para devolver el objeto output adecuado y Outpt debería ser una clase abstracta. // Queda pendiente para otro post donde se muestre cómo generar objetos. if($type === 1) return new Output(); else throw new Exception("Wrong ouput selected."); // Si no existe un output adecuado deberíamos devolver un output base en vez de lanzar una excepción. // No tener un output adecuado no debería para la ejecución. } public function print($val) { echo("Total amount is: ".$val.PHP_EOL); } } [/code]
Vemos que ambos ficheros pertenecen al namespace blog\autoload\Entities, y es en el directorio Entities dónde se encuentran.

Hay que tener en cuenta un detalle antes de continuar; aunque el namespace contenga una ruta realmente no estamos asignando una ruta, estamos asignando un nombre que casualmente coincide con la ruta. Si hacemos que un fichero use el namespace vendor\dir1 no estaremos usando a su vez vendor\dir1\dir2 aunque el directorio dir2 esté dentro de dir1, son nombres de namespaces distintos, repito, no rutas, un namespace no contiene a otro.

Antes de ver el autoload analicemos los detalles y el patrón a seguir.

  • Podemos decir que el fichero ejecutable está en el namespace base, por lo que puede tener cualquier nombre, en este caso hemos decidido que sea blog\autoload\.
  • Cada clase debe tener el mismo nombre que el fichero que lo contiene (una clase por fichero). 
  • Cada fichero deberá pertenecer al namespace compuesto por el nombre del namespace base más las barras correspondientes y los directorios en los que está cada fichero. En este caso al estar en el directorio Entities que cuelga del directorio base su namespace es blog\autoload\Entities.
  • El nombre de las clases tiene la estructura namespace\subnamespace\..\Class. Por ejemplo la clase que llega al autoload realmente es la clase blog\autoload\Entities\Account. Es decir, su namespace más el nombre de la clase.
  • Se observa que el nombre de la clase es namespace_principal\directorio1\..\Clase, por lo que si eliminamos el namespace base, y añadimos un .php al final, tenemos exactamente dónde está el fichero guardado respecto al directorio base. Ejemplo: __DIR__.\Entities\Account.php.

Siguiendo estos puntos ya tenemos un patrón: el nombre del namespace indica dónde está cada fichero respecto a un namespace base.

Pues ya es hora de ponernos a construir un fichero autoload. ¿O no? ¿Para qué crear el autoload si ya alguien lo ha creado por nosotros?

La gente de PHP-FIG ya ha creado un autoloader que podemos utilizar. Lo podéis encontrar en su recomendación estándar 4 (PSR-4 o PHP Standard Recommendation 4).

Aquí lo hemos adaptado a nuestro ejemplo modificando las variables $prefix y $base_dir y añadiendo un echo para imprimir la clase requerida.
[code] // Implementation of PSR-4 spl_autoload_register(function ($class) { echo("Requiriendo fichero: ".$class.".php".PHP_EOL); // project-specific namespace prefix $prefix = 'blog\\autoload\\'; // base directory for the namespace prefix $base_dir = __DIR__.'/'; // does the class use the namespace prefix? $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { // no, move to the next registered autoloader return; } // get the relative class name $relative_class = substr($class, $len); // replace the namespace prefix with the base directory, replace namespace // separators with directory separators in the relative class name, append // with .php $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; // if the file exists, require it if (file_exists($file)) { require_once $file; } }); [/code]
Con esto ya tenemos nuestro fichero autoload.php que hemos requerido anteriormente en nuestro fichero principal.

Lo ejecutamos y obtenemos el siguiente resultado.
[code] Requiriendo fichero: blog\autoload\Entities\Account.php Requiriendo fichero: blog\autoload\Entities\Output.php Total amount is: 1930 [Finished in 0.4s] [/code]
La ejecución ha sido satisfactoria.

De esta forma, requiriendo el autoload y usando un namespace (blog\autoload\Entities), podemos autocargar todas las clases que contenga este sin tener que hacer un require por cada fichero.

Código en GitHub

No hay comentarios:

Publicar un comentario