Publi

Almacén de configuración para pequeños proyectos en PHP

Configuración de proyectos en PHP

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.

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: unsplash-logoMr Cup / Fabien Barral

También podría interesarte....

Leave a Reply