Un punto importante en nuestros proyectos es la gestión de configuración. Cuando estamos delante de un gran proyecto, o empezándolo, seguro que queremos utilizar la gestión de configuración que tiene nuestro framework. Aunque, seguro que si es un proyecto pequeño, en el que, tal vez incluyamos uno o dos módulos con composer y no nos apetece engordar mucho el código con muchas dependencias, ni meter un framework muy complejo nos gustaría utilizar otra cosa. Algo sencillo, que tenga poco código.
Mi opción para esos casos es una clase estática que haga de almacén de valores. Toda la configuración la pondremos en un array dentro de dicha clase, que será accesible en cualquier punto de mi código, pero incluyo algunas utilidades para que sea relativamente fácil insertar y eliminar elementos.
Tabla de contenidos
Mis necesidades en la configuración
Lo que suelo necesitar en estos casos es un array multidimensional, a modo de árbol, en el que pueda construir categorías, subcategorías, sub-sub-categorías y demás. El objetivo es clasificar todos los elementos de configuración, ya sean de base de datos, usuarios, cachés, sistema, y dentro de cada uno de ellos podré introducir más cosas.
Además, toda la configuración, puede que esté almacenada en un archivo php, un json, un XML, ya depende de cada uno de nosotros.
Algo que me gusta mucho, y suelo necesitar en mis proyectos es que el acceso a cada uno de los elementos de la configuración sea rápido y como programador no me cueste demasiado. Es interesante que pueda acceder a los distintos elementos sin tener que utilizar muchos símbolos. Por ejemplo, si en el array de configuración, tengo una clave llamada Database, que luego tiene una clave llamada Main, y ésta tiene una clave llamada Server, a mí, como programador me baste decir Database.Main.Server para poder acceder al valor encerrado en la configuración.
Para ello, me he basado en un antiguo post en el que trataba el tema de lectura, escritura y eliminación de elementos de un array multidimensional usando separadores.
También es interesante este post en el que trato el tema de jerarquías en un array de PHP.
Clase Almacén estática
Antes de nada, construiremos una clase almacén con los elementos básicos que necesito:
- Crear un nuevo nodo, ya sea array o valor, con independencia de la profundidad, si está en una rama interna deberán crearse todas las ramas intermedias hasta llegar. Si el array que introducimos tiene varias dimensiones, éstas deberán figurar como una ramificación también
- Acceso a un nodo
- Eliminación de un nodo
- Obtención del array completo
En principio, podemos crear esta clase Almacen de escasas 80 líneas para ello:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | <?php class Almacen { protected static $storage = array(); protected const DefaultSeparator = '.'; protected const ReturnFullArray = false; public static function set($path, $content, $sep=self::DefaultSeparator) { $_data=&self::$storage; if (!$path) { $_data = $content; return true; } $components = explode($sep, $path); $ccount = count($components); for ($i=0; $i<$ccount; $i++) { $key = $components[$i]; if ($i == $ccount-1) $_data[$key] = $content; elseif (!isset($_data[$key])) $_data[$key] = array(); $_data =& $_data[$key]; } return true; } public static function del($path, $sep=self::DefaultSeparator) { $_data = &self::$storage; $components = explode($sep, $path); $ccount = count($components); for ($i=0; $i<$ccount; $i++) { $key = $components[$i]; if (!isset($_data[$key])) return false; elseif ($i == $ccount-1) { unset($_data[$key]); return true; } $_data =& $_data[$key]; } } public static function exists($path, $sep=self::DefaultSeparator) { if (!$path) return true; /* Siempre existe la raíz. */ return (self::get($path, false)); } public static function get($path, $default=null, $sep=self::DefaultSeparator) { $data = &self::$storage; if (empty($data)) return $default; /* ¿Nos interesa devolver el array completo? */ if (!$path) return (self::ReturnFullArray)?self::$storage:false; $components = explode($sep, $path); foreach ($components as $k) { if (!isset($data[$k])) return $default; $data=&$data[$k]; } return $data; } }; class Config extends Almacen { }; $config = array('Database' => array('Main' => array('server'=> 'database.domain.com', 'user' => 'usuario', 'pass' => 'password'), 'Archive' => array('server' => 'archive.domain.com', 'user' => 'arcuser', 'pass' => 'arcpass')), 'User' => array('Session' => array('cache' => '120M', 'lifetime' => 3600, 'reload' => 600), 'Database' => array('table' => 'USERS', 'extinfo' => 'USER_EXT')), ); Config::set('', $config); Config::set('App.CLI.enabled', true); Config::set('App.Cache', array('engine' => 'Redis', 'server' => 'localhost')); Config::set('App.Paths', array('static' => 'st/', 'temp' => 'var/tmp')); Config::set('App.Cache.expiration', 100); /* Borramos una clave configuración */ Config::del('Database.Archive.pass'); /* Borramos una rama entera */ Config::del('App.Paths'); echo "Main database server: ".Config::get('Database.Main.server')."\n"; echo "App Cache engine: ".Config::get('App.Cache.engine')."\n"; |
Además, en el ejemplo he creado una clase derivada llamada Config. A partir de ahí podremos incluir la configuración de nuestra aplicación y cargarla en la parte del código que queramos.
Gestor de almacenes
Pero el acceso a las variables no resulta del todo cómodo al tener que andar con un get() y un set(). Además, en una aplicación normalmente tenemos varios almacenes. De esta forma, podemos crear una clase un poco más grande que gestione varios almacenes. Ahora los métodos no serán estáticos, por lo que tendremos más posibilidades.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | <?php class Almacen implements ArrayAccess { protected $storage = array(); protected const DefaultSeparator = '.'; protected const ReturnFullArray = false; public function set($path, $content, $sep=self::DefaultSeparator) { $_data=&$this->storage; if (!$path) { $_data = $content; return true; } $components = explode($sep, $path); $ccount = count($components); for ($i=0; $i<$ccount; $i++) { $key = $components[$i]; if ($i == $ccount-1) $_data[$key] = $content; elseif (!isset($_data[$key])) $_data[$key] = array(); $_data =& $_data[$key]; } return true; } public function del($path, $sep=self::DefaultSeparator) { $_data = &$this->storage; $components = explode($sep, $path); $ccount = count($components); for ($i=0; $i<$ccount; $i++) { $key = $components[$i]; if (!isset($_data[$key])) return false; elseif ($i == $ccount-1) { unset($_data[$key]); return true; } $_data =& $_data[$key]; } } public function exists($path, $sep=self::DefaultSeparator) { if (!$path) return true; /* Siempre existe la raíz. */ return ($this->get($path, false, $sep)); } public function get($path, $default=null, $sep=self::DefaultSeparator) { $data = &$this->storage; if (empty($data)) return $default; /* ¿Nos interesa devolver el array completo? */ if (!$path) return (self::ReturnFullArray)?$this->$storage:false; $components = explode($sep, $path); foreach ($components as $k) { if (!isset($data[$k])) return $default; $data=&$data[$k]; } return $data; } /* Implementamos ArrayAccess */ public function offsetSet($offset, $valor) { $this->set((is_null($offset))?'':$offset, $valor); } public function offsetExists($offset) { return ($this->exists($offset)); } public function offsetUnset($offset) { $this->del($offset); } public function offsetGet($offset) { return $this->get($offset); } }; class AppGestor { protected static $instance = null; protected $almacenes = null; /* ¿Queremos permitir que se defina un almacén completo? */ protected $allowFullSet = true; public function __construct() { $this->almacenes = array('config' => new Almacen); } public static function getInstance() { if (!self::$instance) self::$instance = new self; return self::$instance; } public function __set($element, $value) { if ((isset($this->almacenes[$element])) && ($this->allowFullSet) ) return $this->almacenes[$element][] = $value; else throw new Exception('No deberías hacer esto...'); } public function __get($element) { if (isset($this->almacenes[$element])) return $this->almacenes[$element]; } }; function App() { return AppGestor::getInstance(); } $config = array('Database' => array('Main' => array('server'=> 'database.domain.com', 'user' => 'usuario', 'pass' => 'password'), 'Archive' => array('server' => 'archive.domain.com', 'user' => 'arcuser', 'pass' => 'arcpass')), 'User' => array('Session' => array('cache' => '120M', 'lifetime' => 3600, 'reload' => 600), 'Database' => array('table' => 'USERS', 'extinfo' => 'USER_EXT')), ); App()->config = $config; App()->config['App.CLI.enabled'] = true; App()->config['App.Cache'] = array('engine' => 'Redis', 'server' => 'localhost'); App()->config['App.Paths'] = array('static' => 'st/', 'temp' => 'var/tmp'); App()->config['App.Cache.expiration'] = 100; /* Borramos una clave configuración */ App()->config->del('Database.Archive.pass'); /* Borramos una rama entera */ unset(App()->config['App.Paths']); echo "Main database server: ".App()->config->get('Database.Main.server')."\n"; echo "App Cache engine: ".App()->config->get('App.Cache.engine')."\n"; var_dump(App()->config['App.Paths']); |
Aquí nos aprovechamos de la implementación de ArrayAccess, que hace que podamos acceder a un objeto como si fuera un array. De este modo, podemos utilizar el almacén de configuración de manera más intuitiva, como si fuera un array, y para no complicarlo mucho, podemos poner la ruta completa dentro del índice.
Dentro de la gestión de almacenes, la clase AppGestor define los métodos __get y __set para poder acceder a los diferentes almacenes. Ahora mismo sólo tenemos el almacén de configuración, aunque podemos crear la cantidad de almacenes que consideremos oportuna.
Foto principal: Mr Cup / Fabien Barral
Pingback: Almacén de configuración para pequeños proyectos en PHP | PlanetaLibre /