Los bundles representan la modularidad de Symfony, en otros sistemas se denominaría plugin, es parecido, incluso mejor. En symfony todo es un bundle, desde las funcionalidades del propio core hasta el código de nuestras aplicaciones. Es fácil implementar nuevas features empaquetadas en bundles third-party o incluso redistribuir nuestro propio código, simplifican enormemente la tarea de escoger y mantener las funcionalidades que queremos implementar en nuestras aplicaciones.

Un bundle es un simple conjunto de ficheros y directorios que implementan una única feature. Se puede crear el BlogBundle, el BackendBundle… Cada uno en su propia estructura de directorios, donde se almacenarán todos los sources, templates y configuraciones relativas a nuestra utilidad.

Los bundles son declarados en el método registerBundles() de la clase AppKernel, /app/AppKernel.php, en este fichero se implementan todas las features en uso, incluyendo las del propio core de Symfony. Estos se pueden implementar siempre y cuando estén disponibles mediante autoloading, estén donde estén ubicados.

Creando nuestro primer bundle

El sistema dispone de una herramienta para generar automáticamente la estructura de un nuevo bundle e inyectar su configuración a la aplicación ya existente. Este paso es innecesario, solo lo expongo para entender como funciona el sistema de adición de bundles, será más cómodo dejar a Symfony que cree automáticamente el nuevo bundle. Para hacer un pequeño test, cread el bundle a mano, borradlo, y, posteriormente recreadlo mediante el comando de consola de Symfony. Lo primero será crear el directorio /src/BackendBundle y dentro el fichero BackendBundle.php

1
2
3
4
5
6
7
8
// src /BackendBundle/BackendBundle.php
namespace BackendBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class BackendBundle extends Bundle
{
}

Esta es la pieza de código que marcará la existencia de nuestro bundle. Sencillamente necesitamos añadirla dentro de la clase /app/AppKernel.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...

// register your bundle
new BackendBundle\BackendBundle(),
);
// ...

return $bundles;
}

Tras inyectar el bundle al Kernel necesitamos que el sistema resuelva las rutas que definamos en el nuevo apartado, de manera que editamos el /app/config/routing.yaml para añadir el apartado backend. Ya de paso le indicamos un prefijo para el bundle, una palabra que se antepondrá a todas las rutas de los controllers de este Bundle, de manera que si definimos @Route(“/test”, name=”test_list”) en la acción de un controller, tendremos que acceder con la url http://url/prefijo/test.

1
2
3
4
5
6
7
app:
resource: "@AppBundle/Controller/"
type: annotation
backend:
resource: "@BackendBundle/Controller/"
type: annotation
prefix: "/backend/"

A partir de ahora, con crear las carpetas necesarias dentro del nuevo bundle, como Controller, Entity, etc… Y añadiendo al sistema las rutas al nuevo apartado podremos dividir nuestro código en las piezas necesarias para su reutilización. Podéis hacer un test creando un controller que en su acción devuelva una response con texto plano como en el ejemplo list del source BlogController.

Si has seguido el test creo que será mejor borrar la carpeta /src/BackendBundle, borrar la entrada de /app/AppKernel.php y dejar el fichero routing.yml con la última modificación, así nos ahorraremos un paso.

Creación automática del bundle

Todos los pasos anteriores los podemos dejar en manos de Symfony. Invocamos el comando de consola generate:bundle, encargado de generarlo y activarlo en el fichero AppKernel.php, de la siguiente manera.

1
php bin/console generate:bundle --namespace=BackendBundle

Tras ejecutar el comando, el sistema nos preguntará la información relevante para generar el set de directorios y configuraciones necesarias. Preguntas cómo si el bundle va a ser compartido entre múltiples aplicaciones, su carpeta raíz, sistema de configuración, etc.. dejaremos todo como indican los valores por defecto, propuestos entre corchetes, solo tendremos que pulsar enter para confirmar este valor. Al final nos mostrará un resumen de operaciones y nos dirá que el bundle se ha creado con éxito.

Al igual que en caso de crearlo manualmente, en el fichero /app/config/routing.yml podremos proponer un prefijo backend para las rutas a los contenidos de este apartado. Debemos revisar que esta configuración sea correcta o en caso contrario pegar la propuesta anterior. La configuración del archivo /app/config/routing.yml está unas lineas más arriba.

En la carpeta /src/BackendBundle/Controller se ha generado automáticamente un DefaultController.php con el que podemos hacer nuestro primer test navegando a http://localhost:8000/backend/, recordad levantar el servidor antes si es que no lo habíais hecho ya.

Como veis, un simple Hello World, vamos a aplicar la herencia de twig para dejar la primera página del backend dentro del layout que ya hemos creado. Lo primero será duplicar el archivo base /app/Resources/views/base.html.twig y llamarle /app/Resources/views/backend.html.twig. Ahora, en el fichero /src/BackendBundle/Resources/views/Default/index.html.twig extendemos el backend layout de la siguiente manera.

1
2
3
4
5
6
7
8
{% extends 'backend.html.twig' %}

{% block body %}
Hello World!
{% endblock %}

{% block stylesheets %}
{% endblock %}

Si navegamos nuevamente a la url del backend el “Hello World” ya estará integrado en nuestro template base.

Si nos vamos con el explorador a la carpeta /src de nuestro proyecto, podremos ver la estructura que ha sido generada. La estructura básica de un Bundle sería la siguiente, aunque en nuestro caso la mayoría de las carpetas no han sido creadas.

  • Controller/
    Contiene todos los controllers del bundle.

  • DependencyInjection/
    Guarda ciertas clases extendibles para la inyección de dependencias, que podrían importar configuraciones de servicios, registrar pasos de compilación y más cosas. Esta carpeta no es obligatoria.

  • Resources/config/
    Almacena las configuraciones específicas al bundle, configuraciones de ruta, de servicios, etc… (p.e. routing.yml).

  • Resources/views/
    Guarda los ficheros de template organizados en carpetas por controllers (p.e. blog/index.html.twig).

  • Resources/public/
    Contiene los recursos públicos, assets (css, imágenes …), es copiada o enlazada simbólicamente en el directorio público web mediante el siguiente comando de consola

    1
    php bin/console assets:install
  • Tests/
    Se meten aquí todos los sources de tests.

Este nuevo bundle será el encargado de mantener toda la lógica que necesitamos para crear el apartado de administración, a partir de ahora casi todo lo que hagamos acabará alojado en esta carpeta.

Bundles 3rd. Party

La modularidad de los bundles va más allá de poder dividir nuestro código e independizarlo para copiarlo en otros proyectos, nos da la posibilidad de crear bundles para ser publicados, de manera que otros usuarios los instalen y mantengan muy sencillamente en sus proyectos mediante Composer u otras herramientas. Hay una web “no-oficial” que se encarga de publicar y recoger información de los bundles públicos creados para Symfony, KnpBundles.com.

Instalando KnpMenuBundle


Vamos a centrarnos un poco en la instalación de uno de estos bundles como ejemplo, KnpMenuBundle, cuyo uso es preconizado en el propio manual de Symfony. Este paquete se encargará de las tareas de mantenimiento y gestión de menús de una web. Para obtener el paquete solo debemos indicar a Composer la nueva dependencia del proyecto.

1
composer require knplabs/knp-menu-bundle

Tras la instalación del paquete necesitaremos implementar su uso en nuestro proyecto, de manera que editamos el fichero app/AppKernel.php para incluir su carga al sistema.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...

new Knp\Bundle\MenuBundle\KnpMenuBundle(),
);

// ...
}

// ...
}

El bundle ya usa una configuración por defecto incluida en sus propios sources, si deseamos utilizar otra o modificar la actual, podemos hacerlo sobre nuestro propio fichero de configuración app/config/config.yml incluyendo y modificando lo siguiente.

1
2
3
4
5
6
7
8
9
# app/config/config.yml
knp_menu:
# use "twig: false" to disable the Twig extension and the TwigRenderer
twig:
template: KnpMenuBundle::menu.html.twig
# if true, enables the helper for PHP templates
templating: false
# the renderer to use, list is also available by default
default_renderer: twig

Prerrequisitos, Main menú

Antes de empezar a crear el main menu vamos a setear el nombre de la ruta por defecto del backend, en la clase /src/BackendBundle/Controller/DefaultController.php anotamos el nombre de la ruta.

1
2
3
4
5
6
7
/**
* @Route("/", name="backend_home")
*/
public function indexAction()
{
return $this->render('BackendBundle:Default:index.html.twig');
}

Ahora necesitamos preparar la plantilla del layout del backend para recibir el menú, hay que hacerlo con sumo cuidado ya que la estructura de menú de sb-admin2 es grande y compleja. Yo he adaptado un poco el HTML para respetar la caja de búsqueda y para que el menú se represente correctamente debajo.

Localizad las etiquetas con mucho cuidado en el /app/Resources/views/backend.html.twig antes de hacer el reemplazo, si no, fallará el javascript encargado de hacer las animaciones del menú. Os pongo justo el lugar donde yo reemplacé el código del template y un enlace AQUI al source ya modificado. Hay que centrarse en las etiquetas comentario navbar-top-links y navbar-static-side para utilizar de referencia.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    <!-- /.navbar-top-links -->

<div class="navbar-default sidebar" role="navigation">
<div class="sidebar-nav navbar-collapse">
<div class="sidebar-search">
<div class="input-group custom-search-form">
<input type="text" class="form-control" placeholder="Search...">
<span class="input-group-btn">
<button class="btn btn-default" type="button">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
{{ knp_menu_render('BackendBundle:Builder:mainMenu', {'allow_safe_labels': true}) | raw }}
</div>
<!-- /.sidebar-collapse -->
</div>
<!-- /.navbar-static-side -->
</nav>

Dentro de nuestro recién creado /src/BackendBundle vamos a crear una carpeta denominada Menu, dentro, crearemos la clase Builder, esta será la encargada de generar los menús que precisemos, cada menú será un método de la clase. Es decir, si nuestro Bundle va a disponer de un menú principal y un menú de categorías, necesitaríamos crear dos métodos dentro de la clase Builder, mainMenu y categoriesMenu. En nuestro caso generaremos la clase Builder con un único menú principal, mainMenu.

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
// src/BackendBundle/Menu/Builder.php

namespace BackendBundle\Menu;

use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class Builder implements ContainerAwareInterface {

use ContainerAwareTrait;

public function mainMenu(FactoryInterface $factory, array $options) {
$menu = $factory->createItem('root', array(
'currentClass' => 'active',
'childrenAttributes' => array(
'class' => 'nav',
'id' => 'side-menu'
),
));

// Hijo de primer nivel.
$menu->addChild('Home', array('route' => 'backend_home'));

// Configuración mediante métodos.
$menu['Home']->setLabel('<i class="fa fa-dashboard fa-fw"></i> Home');
$menu['Home']->setExtra('safe_label', true);

// Hijo de primer nivel.
$menu->addChild('First entry', array(
'route' => 'blog_list',
'routeParameters' => array(
'id' => 1
),
'label' => '<i class="fa fa-table fa-fw"></i> First entry',
'extras' => array(
'safe_label' => true
)
));

// Hijo de primer nivel, submenu desplegable.
$menu->addChild('Second entry', array(
'uri' => '#',
'attributes' => array(
'aria-expanded' => 'true'
),
'label' => '<i class="fa fa-user fa-fw"></i> Second entry<span class="fa arrow">',
'extras' => array(
'safe_label' => true
),
'childrenAttributes' => array(
'class' => 'nav nav-second-level',
'aria-expanded' => 'true'
)
));

// Añadir hijos a este submenu
$menu['Second entry']->addChild('Submenu entry', array('uri' => '#test'));

// ... Añadir más hijos
return $menu;
}

}

Utilizando la plantilla por defecto, knp_menu.html.twig, KnpMenuBundle generará anidamientos de tags “ul”, “li” y “a” para “escribir” el HTML del menú. Estos elementos se pueden formatear de múltiples formas, yo he elegido utilizar un setup en formato array desde el addChild() y he incluido un pequeño ejemplo de la otra manera en el caso Home. El otro método es muy similar, una vez añadamos un elemento al menu, este será accesible como el índice de un array en el mismo objeto, y, nos dará acceso a los métodos y propiedades de este elemento de menú a fin de poder setearlo o modificarlo. En este caso los métodos son setters y getters sobre los índices de los arrays de config utilizados, la propiedad attributes sera accesible mediante la función setAttributes, label será con setLabel(), etc…

  • route
    Se puede indicar como destino del enlace el nombre de una ruta definida en el sistema con el componente de route, como las definidas como anotación en las acciones de un Controller.

  • routeParameters
    Añade el parámetro a la petición formada.

  • uri
    Se utiliza para configurar como destino del menú un enlace HTML normal.

  • label
    Define el texto del menú generalmente contenido en el enlace a. En este caso le pasamos también el código del elemento que dibuja el icono del menú, por lo que además hará falta definir el atributo “extras safe_label” para evitar que se parsee el HTML y cambie por HTMLEntities.

  • childrenAttributes
    Da acceso a los atributos HTML del nodo contenedor de los hijos (ul), si creamos un elemento li con hijos, estos serán contenidos en un ul. En el caso del nodo root es lo mismo, nos da acceso a los atributos del ul dentro del que se engloba el menú.

  • extras
    Es un apartado para la configuración específica, en este caso contiene la configuracion safe_label con valor true, indica que la etiqueta contiene HTML.

aria-expanded es un atributo de nodo contenedor ul, <ul aria-expanded="true">, que indica si esta porción del menú aparecerá desplegada por defecto o no. Es interesante aprovechar para echar un ojo a la documentacion de sb-admin2 y así no sentirnos demasiado desorientados a la hora de trabajar con él.

A partir de aquí MetisMenu, implementado por sb-admin2, se encargará del resto. Incluso detectará si la url accedida es la definida en algún nodo para presentar esa porción del menú desplegado.

Ya solo nos falta acceder a la url del backend http://localhost:8000/backend/ para ver nuestro menú perfectamente representado. A partir de ahora solo será necesario refinarlo según nuestras necesidades, también creo, que si es necesario más de un menú complejo, será bueno crear clases para cada uno dentro de un subdirectorio de Menu e instanciarlos desde cada método del Builder, para evitar la excesiva complejidad de los métodos. Para probar que la sección se mantiene desplegada al acceder a la url contenida podemos probar con http://localhost:8000/backend/#test

That’s all folks!!

Otra vez cierro esperando poder continuar en breve, he visto un poco por encima la modularización del proyecto, y, la instalación de librerías de terceros bajo el concepto Symfony “Bundles”, en la próxima iteración intentaré empezar a poner pegamento sobre las piezas que hemos ido construyendo, tenemos bastantes partes del global, poco a poco intentaremos llegar a un backend flexible y escalable bajo el que desarrollar cualquier proyecto que deseemos.