Hace unos días comencé a refactorizar una configuración que tenia hecha para Slim framework (v3), esta permite cargar los controladores dinámicamente en función de la ruta de la petición, sin tener que declararlos previamente en el router. Ahora es un objeto tipo Factory que instancia, configura y retorna una Slim\App lista para usar. Aunque es algo más engorroso, creo que es mejor no referenciar directamente el contenedor DI dentro del controlador, ya que esta dependencia podría ser considerada un anti-pattern (Service Location). Así que añadimos un interface para el controller, que garantice la ejecución del método pasando sus dependencias como parámetros. He cambiado la forma en que implementaba el routing dinámico para hacerlo un poco más accesible, poder reutilizarlo o mejorarlo de una manera más sencilla.

[Github: English readme]

Instalación

Se puede obtener de las siguientes maneras:

Uso

Tras hacer el require del autload, mediante una llamada estática, podemos pedirle al objeto Factory que configure y retorne una instancie nueva del framework, con la que nosotros podremos continuar haciendo los procesos normales de la aplicación, middleware. inyectar dependencias al container DI, etc..

1
2
3
4
5
6
7
8
9
10
11
require __DIR__ . '/../vendor/autoload.php';
$settings = require __DIR__ . '/../src/settings.php';

$app = \mbarquin\SlimDR\Factory::slim()
->withGroup('api')
->withVersionGroup('v1')
->withContainer($settings)
->withNamespace('\\MyApp\\Controllers')
->getApp();

$app->run();

El método estático slim() retorna el propio objeto Factory, casi todos los métodos (withGroup(), withVersionGroup(), withContainer() y withNamespace()) de esta clase, al ser llamados, van a devolver la instancia de Factory. Esto es lo que nos permite concatenar las llamadas sin llegar a tener que almacenarlo en una variable. Finalmente el método getApp() nos devuelve una instancia nueva de Slim\App. Estoy haciendo pruebas para pasarle como parámetro a la función slim() una instancia Slim\App ya creada y hacer solo su setup a través de getApp().

  • slim (\Slim\App $app=null)
    Genera y retorna una instancia nueva del objeto Factory, en caso de obtener una aplicación Slim como parámetro guarda una referencia a ella para poder devolverla tras su configuración con getApp(), en lugar de crear una nueva.

  • withGroup ($mainGroup)
    Configura la primera agrupación en la url, se atenderán todas las llamadas que incluyan este prefijo, http://www.myserver.com/grupo/controller. Si no se configura este grupo principal, se utilizara el secundario o ninguno, según el caso. Esta función retorna el propio objeto Factory.

  • withVersionGroup ($versionGroup)
    Configura el grupo (o prefijo) secundario, http://www.myserver.com/grupo/gruposcundario/controller - http://www.myserver.com/api/v1/controller, generalmente es usado para agrupar las llamadas de una versión determinada de la API. Si no se configura, el router usara el prefijo principal o ninguno, según el caso… http://www.myserver.com/api/controller o http://www.myserver.com/controller. Esta función retorna el objeto Factory en si mismo.

  • withContainer ($settings)
    A través de esta función el objeto recibimos el contenedor, o el array con la configuración, para utilizar como parámetro en el constructor de Slim\App. Estoy haciendo pruebas con la posibilidad de pasar como parámetro a la función Factory::slim() una instancia Slim\App ya creada, sólo para su configuración, así este paso no es obligatorio, en otro caso se generará una excepción desde el constructor de Slim.

  • withNamespace (‘\\MyApp\\Controllers’)

  • Obligatorio*. Es el namespace de los controladores, se utiliza para obtener el controller a mediante el autoload. La clase debe poder ser instanciada a traves de su namespace+nombre, \MyApp\Controllers\test. Esta función retorna el objeto Factory en si mismo.

  • getApp ()
    Instancia, configura con todo lo anterior y retorna una nueva Slim\App con routing dinámico.

Ahora el framework espera instanciar controllers que extiendan de SlimDR\ParentController o que implementen el SlimDR*ControllerInterface*. Esta implementación es para evitar la dependencia global del container DI como en un** Service Location**, nuestros controladores deben proveer un camino por el que comunicar al sistema cuales son sus dependencias. De esta manera dependeremos solo de los objetos que realmente vamos a utilizar.

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
/**
* Test Controller file
* SlimDR example controller
*
* PHP version 5.6
*
*
* @category SlimDR
* @package Test
* @subpackage Controller
* @author Moises Barquin Salgado <moises.barquin@gmail.com>
* @copyright Moises Barquin Salgado 2016
* @version GIT: $Id$
*/

namespace MyApp\Controller;

use mbarquin\SlimDR\ParentController;

/**
* Class test, must implements Controller interface
* It's extended from mbarquin\SlimDR\Parentcontroller
*/
class test extends ParentController
{
/**
* Array with actions dependencies, in the form [ method => [dependencies]]
* @var type
*/
protected $dependencies = array(
self::GET => array ('db', 'logger')
);

/**
* GET method as controller action,
* Params which are not request, reponse and args must be declared on
* dependencies array.
*
* @param Psr\Http\Message\ServerRequestInterface $request Request object
* @param Psr\Http\Message\ResponseInterface $response Reponse object
* @param array $args Request params
* @param \stdClass $db Database object
* @param \Monolog\Logger $logger Logger object
*/
public function get($request, $response, $args, \stdClass $db, \Monolog\Logger $logger)
{
print_r($args);
}
}

Tras una petición url con los grupos adecuados, Slim buscará en el controller la función con el mismo nombre del método HTTP usado en la llamada. Si la llamada es al controller test con el método PUT, Slim tratara de invocar la función put (minusculas) dentro de este controlador, lo mismo GET, POST o cualquiera de los métodos soportados por Slim. Esto es lo consideraríamos como las acciones de nuestro Controller.

Estas “acciones” del controlador deben esperar recibir los parámetros $request, $reponse, $args y cada una de las dependencias en el mismo orden que han sido declaradas en el array $dependencies. $request y $reponse serán los objetos habituales que utiliza Slim en las closures para resolver las rutas. $args será un array con los parámetros obtenidos de la url, declarados tras el nombre del controller en la forma http://url/group/vgroup/controller/param1/paramN.

La propiedad privada $dependencies debe ser un array, como índices debe tener el nombre del método de la request en minusculas, o lo que es lo mismo el nombre de la función llamada en el controller. Cada elemento indexa un segundo array, sus elementos serán las claves dentro del container DI con las que obtener esos objetos. Los nombres de los métodos HTTP ya están definidos como constantes en el parent controller a fin de facilitar el uso.

Estas instancias serán pasadas al método del controller,

1
2
3
4
5
6
$dependencies = array(
self::POST => array('dep1', 'db', 'dep3'),
self::GET => array('dep1'),
self::PUT => array('dep3')
...
);

Estas dependencias deben ser inyectadas al contenedor DI antes del run de la aplicación, o mediante algún middleware previo a la resolucion de la url. De esta manera en el momento de la ejecución de nuestro controller, el objeto Factory, estáticamente, podrá obtener estas clases para ser utilizadas por el método concreto que se invoque.

1
2
3
4
5
6
7
$container       = $app->getContainer();

$container['db'] = function ($c) {
$db = new stdClass();

return $db;
};

En breve intentare mejorar un poco la librería para que se pueda realizar la configuración del routing en los propios settings de Slim, ya de paso lo subiré a la versión 1.0 estable y terminaré de generar los tests.