Sunteți pe pagina 1din 284

9 788426 721600

Programación PHP Profesional


con Slim, Paris y Twig
Programación PHP Profesional
con Slim, Paris y Twig
Joseluis Laso
Programación PHP Profesional con Slim, Paris y Twig

Primera edición, 2014

© 2014 Joseluis Laso

© MARCOMBO, S.A. 2014


Gran Vía de les Corts Catalanes, 594
08007 Barcelona
www.marcombo.com

«Cualquier forma de reproducción, distribución, comunicación pública o transformación de


esta obra solo puede ser realizada con la autorización de sus titulares, salvo excepción
prevista por la ley. Diríjase a CEDRO (Centro Español de Derechos Reprográficos,
www.cedro.org) si necesita fotocopiar o escanear algún fragmento de esta obra».

ISBN: 978-84-267-2160-0
DL: B-15822-2014

Impreso en
Printed in Spain
Dedico este libro con todo mi cariño a mis seres
queridos y a todas aquellas personas que me
han ayudado a materializar este proyecto.
Sobre mí
Nací en Valencia aunque de ascendencia castellana.
Empecé mis primeros pinitos en la informática con una
calculadora SHARP PC-1500A que consiguió mi padre
al adquirir un equipo informático a la empresa INGEL-
MO sobre los años ochenta, equipos que por aquella
época costaban alrededor de un millón de pesetas: una
CPU Z80 con 64 Kb de RAM; dos disqueteras de 5 1/4”
de 360 kB: una para el programa y otra para datos; y
una impresora matricial para confeccionar los docu-
mentos de reparto y facturas. El programa: un desarro-
llo a medida (según lo vendían) costaba aproximada-
mente lo mismo que el hardware (¡Qué tiempos aquellos!). Cuando exprimí la
calculadora al máximo me pasé directamente a programar «el bicho» que he
descrito. No he parado desde entonces, pasando por aplicaciones de escritorio
hechas en Turbo Pascal, Delphi, Access, Velneo hasta por fin aplicaciones web
desarrolladas en PHP y MySQL. De la mano de Symfony 1.4 entré por la puer-
ta grande en la programación orientada a objetos, aunque pronto se nubló mi
éxtasis al tener que desarrollar plugins y themes para Wordpress, que, aunque
teniendo mérito como desarrollo comunitario, es lo que en nuestro mundillo se
conoce como código espagueti.

Esta vasta experiencia y mis muchos deseos de aprender, unidos a las innu-
merables charlas a las que he asistido sobre Symfony y las incontables entre-
vistas de trabajo que he protagonizado, en las que me han preguntado sobre
patrones de diseño, programación en equipo, buenas prácticas, etc., me han
motivado a contar mi experiencia a nivel de desarrollo en PHP para que aque-
llos que tengan interés en aprender a programar para un mundo real encuen-
tren en este libro el apoyo técnico necesario para empezar esta andadura.
Índice de contenidos
1. Introducción .................................................................................................................... 1
Presentación ................................................................................................................................ 2
Composer ...................................................................................................................................... 2
Slim .................................................................................................................................................. 4
Entonces, ¿qué vamos a ver en este libro? ...................................................................... 5
Conclusión..................................................................................................................................... 6

Parte I: MVC
2. Patrón MVC ....................................................................................................................... 9
Introducción...............................................................................................................................10
¿Por qué esta elección?..........................................................................................................11
Conclusión...................................................................................................................................13
3. El controlador: Slim .................................................................................................... 15
Introducción...............................................................................................................................16
¿Cómo funciona? ......................................................................................................................17
¿Qué hace Slim? ........................................................................................................................19
Declaración de rutas ...............................................................................................................20
¿Atención duplicada? .............................................................................................................21
Conclusión...................................................................................................................................22
4. El modelo: IdiORM & Paris ....................................................................................... 23
¿Qué es un ORM? ......................................................................................................................24
¿Por qué un ORM?....................................................................................................................25
Métodos disponibles ..............................................................................................................30
Rizando el rizo ..........................................................................................................................32
Relaciones entre tablas .........................................................................................................33
Conclusión...................................................................................................................................35
5. La vista: Twig ................................................................................................................ 37
¡Ay, la vista, la vista! ................................................................................................................38
Vamos por partes .....................................................................................................................39
¿Qué es Twig? ............................................................................................................................41
¿Cómo funciona? ......................................................................................................................42
Tipos de datos ...........................................................................................................................45
Toma de decisiones .................................................................................................................46
Iteraciones ..................................................................................................................................46
Funciones de Twig ...................................................................................................................48
Extender Twig con funciones propias.............................................................................49
Macros ..........................................................................................................................................49
Resumen ......................................................................................................................................52
Conclusión...................................................................................................................................59

Parte II: Herramientas


6. Composer ....................................................................................................................... 63
Introducción...............................................................................................................................64
Repositorios privados ............................................................................................................67
Conclusión...................................................................................................................................69
7. Git como SCM ................................................................................................................ 71
Introducción teórica ...............................................................................................................72
Explicación práctica ................................................................................................................73
Instalación...................................................................................................................................76
Comandos básicos: init y clone ..........................................................................................77
Funcionamiento en el día a día ..........................................................................................78
Ejemplos prácticos de uso....................................................................................................80
Buenas prácticas ......................................................................................................................88
Comandos más usados de git ..............................................................................................89
Rizando el rizo ..........................................................................................................................91
Conclusión...................................................................................................................................92
Parte III: MVC a fondo
8. Controller: Slim ............................................................................................................ 95
Introducción...............................................................................................................................96
Cómo implementar una ruta simple ................................................................................99
Personalizar errores en Slim............................................................................................ 102
Ganchos (Hooks).................................................................................................................... 106
Conclusión................................................................................................................................ 109
9. Vamos con el modelo................................................................................................111
Introducción............................................................................................................................ 112
Configurar la instancia........................................................................................................ 112
Recordando un poco ............................................................................................................ 112
Una consulta sencilla ........................................................................................................... 113
Una consulta completa ....................................................................................................... 113
Usando tablas de bases de datos diferentes .............................................................. 115
Extender IdiOrm&Paris...................................................................................................... 116
Diagrama de tablas de My-simple-web ......................................................................... 118
SluggableInterface ................................................................................................................ 124
Conclusión................................................................................................................................ 149
10. Añadiendo Twig ......................................................................................................143
Introducción............................................................................................................................ 144
Parte de administración de datos .................................................................................. 146
Conclusión................................................................................................................................ 157
11. Formularios ..............................................................................................................159
Introducción............................................................................................................................ 160
Formulario de contacto ...................................................................................................... 161
Formularios en el backend ................................................................................................ 166
Conclusión................................................................................................................................ 171
12. Validación ..................................................................................................................173
Introducción............................................................................................................................ 174
Extender BaseModel............................................................................................................ 174
¿Cómo usar la validación? ................................................................................................. 177
SluggableInterface ................................................................................................................ 178
Conclusión................................................................................................................................ 181
13. i18n: Internacionalización ..................................................................................183
Introducción............................................................................................................................ 184
Sistema de trabajo ................................................................................................................ 184
Conclusión................................................................................................................................ 188
14. Agregando nuevos componentes.......................................................................189
Introducción............................................................................................................................ 190
Anotaciones para rutas ...................................................................................................... 190
EntityManager........................................................................................................................ 199
Paginación y búsqueda ....................................................................................................... 205
Conclusión................................................................................................................................ 217
15. Aplicación de ejemplo ...........................................................................................219
Presentación ........................................................................................................................... 220
¿Cómo empezar? ................................................................................................................... 220
¿Qué es un fork? .................................................................................................................... 222
Pull request.............................................................................................................................. 222
Conclusión................................................................................................................................ 226

Parte IV: Para subir nota


16. TDD..............................................................................................................................229
Presentación ........................................................................................................................... 230
¿En qué consiste el TDD? ................................................................................................... 230
¿Para qué sirven los test? .................................................................................................. 230
Ejemplo práctico ................................................................................................................... 234
Conclusión................................................................................................................................ 240
17. Caso práctico ............................................................................................................241
Presentación ........................................................................................................................... 242
E-commerce básico ............................................................................................................... 243
Manos a la obra ...................................................................................................................... 245
El reto ......................................................................................................................................... 256
Conclusión................................................................................................................................ 258

Índices
Convenciones
He seguido las siguientes convenciones a la hora de escribir este libro para fa-
cilitar el seguimiento del código fuente de los ejemplos y su lectura en general:
» Nombres de archivo en cursiva: index.php
» URL y enlaces en gris y subrayados: http://www.joseluislaso.es
» Contenido del los archivos en letra Andale Mono a 9: <?php echo
"hola"; ?>
» Salida del terminal en vídeo inverso con Courier a 11:
mkdir prueba

» Comando embebido en la línea en letra Andale Mono a 9: cd carpeta


» Notas en el texto:

Esto es importante, ¡Tenlo en cuenta!

» Además utilizaré PSR0 a 2, por ello verás que no cierro nunca las eti-
quetas <?php, además de cuestiones de formateo de código1.
» En ocasiones omitiré parte del código que por motivos didácticos no
es importante; este será sustituido por tres puntos seguidos, en todo
caso el contenido completo está en github.
» Me he permitido las siguientes licencias al escribir:
» bbdd por base de datos para abreviar
» palabras en inglés de uso normal en programación en letra cursiva
» *nix indica cualquier tipo de sistema POSIX como Linux, Unix,
BSD, Darwin.

1
https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
Agradecimientos
» Al creador de SlimFramework, Josh Lockhart.
» Al creador de IdiORM&Paris, Jamie Matthews.
» Al creador de Twig, Fabien Potencier.

¡Muchas gracias a todos por vuestros generosos aportes!

» A Fran Moreno, a quien debo mi iniciación en Symfony.


» A Julio Antúnez, por exigir lo mejor de mí mismo.
» A los compañeros de Marcadotecnia, Middion y Onbile, por el excelente
trato recibido.
» A la comunidad Symfony por inculcar en mí las ganas de superación.
» A la editorial Marcombo y en especial a Jeroni por haber creído en mí.
» A la empresa OVH por facilitarme acceso a su API y permitirme publicar
ejemplos utilizándola.
» A fotoestudicatala.com por la fotografía del autor.
» A Óscar Ruiz Casanova por el diseño de la foto de portada.

Todos los nombres propios de programas, sistemas operativos, frameworks,


etc., que aparecen mencionados en este libro son marcas registradas de sus
respectivos autores o propietarios. Los logotipos o emblemas mostrados son
propiedad de sus respectivos propietarios y se muestran con el único fin de
ilustrar el texto.
1 Introducción
Contenido
» Presentación
» Composer
» Slim
» Entonces, ¿qué vamos a ver en este libro?
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Presentación
Este no pretende ser un libro convencional en el que se expone mucha teoría
y se dan pocos ejemplos. De hecho te supongo unos conocimientos mínimos
que no voy a abordar. En los puntos en los que sea importante daré referencias
a sitios de donde puedes sacar información complementaria. De todas mane-
ras puedes plantearme las dudas que te surjan de la lectura de esta obra.

Puedes contactar conmigo a través de mi página web: http://www.joseluislaso.es


o del blog donde escribo: http://www.jaitec.net.

Como hilo conductor y soporte del código y ejemplos nos basaremos en una
aplicación real que he creado para este libro. Estoy trabajando en ella aún, así
que no está completa al 100% (¿qué aplicación lo está, verdad?). De esta manera
fijaremos los conceptos con código real. Esta aplicación se llama My-simple-web
y la tienes a tu disposición en github [http://github.com/jlaso/my-simple-web] y
puedes verla en funcionamiento en [http://mysimpleweb.com.es].

Antes de empezar con el código de My-simple-web, vamos a instalar lo mínimo


para tener una pequeña aplicación de ejemplo con la que mostrarte los primeros
pasos.

Con ello pretendo:


» Introducir composer
» Introducir Slim

Como ves nos hemos puesto manos a la obra de inmediato ;<).

Composer
Composer es el gestor de dependencias de paquetes por excelencia de PHP.
A lo largo del libro vamos a utilizar composer muy a menudo, por lo que te
recomiendo que si aún no lo tienes instalado en tu equipo acudas a la URL
oficial1 y sigas los pasos dependiendo de la configuración de tu sistema.

Habitualmente suelo renombrar el archivo composer.phar a composer, le doy


permisos de ejecución y lo muevo a una de las rutas del path, te menciono
esto porque verás que mis instrucciones de ejemplo solo llevan composer y
no php composer.phar, en todo caso ten en cuenta que son equivalentes.

1
http://getcomposer.org

2
1. Introducción

Empecemos creando nuestro primer proyecto


Creamos una carpeta. Nos movemos dentro de ella y creamos el archivo
composer.json. En un sistema *nix esto quedaría así:

cd ~
mkdir prueba
cd prueba
nano composer.json

Dentro del archivo escribimos lo siguiente:


{
"require": {
"slim/slim": "2.*"
}
}

Este archivo le indica a composer los requerimientos que tiene nuestro proyecto.
Los paquetes o módulos que utilizará composer para descargar están en
http://packagist.org, más adelante veremos cómo utilizar una estructura más
compleja para indicar otro origen. En principio y para este primer acercamiento
nos sobra con lo que hemos escrito.

Ahora ejecutamos el comando:


composer install

Si todo va bien veremos en pantalla la siguiente traza:

-> composer install


Loading composer repositories with package information
Installing dependencies
- Installing slim/slim (2.2.0)
Loading from cache

Writing lock file


Generating autoload files

Cualquier mensaje de error que se pudiera presentar es autoexplicativo. En


todo caso repasa bien la estructura del json que has creado pues es común
tener errores de sintaxis: comillas sencillas en lugar de dobles, coma en el
último elemento del array, etc.

3
Programación PHP profesional con Slim, Paris y Twig

Te reproduzco a continuación un error muy común trabajando con composer,


ocasionado por el exceso de una coma en el último elemento de la declaración2.

Como puedes ver el error es autoexplicativo y podemos subsanarlo


inmediatamente. Ahora ya tenemos una estructura con la que poder ir trabajando
a lo largo de los ejemplos del libro.

Slim
Te dije al principio de este capítulo que quería introducir dos conceptos al
instalar los ejemplos sencillos, el primero era composer, que ha quedado visto;
vamos a por el segundo.

Slim es un pequeño framework que nos permite orientar nuestra programación


hacia el patrón modelo-vista-controlador (MVC). Por sí mismo no nos da soporte
para todo el patrón, de hecho él mismo solo implementa el controlador, por eso
necesitamos otros dos componentes. Pero la base subyacente a este patrón

2
PHP es más permisivo en esto.

4
1. Introducción

es el controlador, veremos enseguida que todas las peticiones que realizan los
usuarios de la aplicación entran por Slim y son resueltas por Slim, utilizando al
modelo para recuperar y/o persistir datos en la base de datos y al motor de la
vista para generar el resultado que se va a mostrar al usuario.
Brevemente, ya que vamos a ver con frecuencia este tema a lo largo del libro,
Slim hace lo siguiente:
» Recoge la petición (request) desde una interacción del usuario en el
navegador.
» Manipula los datos necesarios poniendo en juego al modelo.
» Utiliza los datos que sean precisos para pasárselos al motor de vista y
genera una respuesta HTML adecuada.
» Devuelve la respuesta al usuario (response).

Entonces, ¿qué vamos a ver en este libro?


Veremos la manera más adecuada de utilizar el patrón MVC para nuestro propio
beneficio, haciendo aplicaciones robustas, fáciles de mantener, sin código
duplicado, y fiables ante los fallos, teniéndolos controlados de antemano.
Procuraremos encapsular todos los métodos posibles haciendo que su ámbito de
trabajo se ciña únicamente a lo que deben hacer, e intentando también que las
cuestiones que se hayan de repetir se hagan llamando a método y no repitiendo
código, de manera que al final nuestro código sea fácil de mantener, fácil de
estudiar, fácil de implementar y en resumen fácil de entender.
Evidentemente, si estás acostumbrado a trabajar en lo que comunmente
se conoce como PHP plano o código espagueti, es fácil que los conceptos
que introduzca a lo largo del libro te resulten algo complicados. Solo puedo
decirte que no desistas. Intenta localizar algún buen libro sobre programación
estructurada en PHP y aprende lo básico como mínimo.
Evidentemente las cosas se pueden hacer de múltiples maneras, y más aún
en programación. Esta que vamos a abordar se ha demostrado que es más
tolerante a fallos, más fácil de mantener y la corriente actual de programación
va hacia ella. Por esto:
» Es más fácil que encuentres trabajo en este ámbito si es que lo estás
buscando.
» Es más fácil que encuentres programadores con este perfil si lo que
buscas es ayuda o colaboración.
Aun así no te recomiendo en ningún caso la lectura de este libro si al menos
no has visto y utilizado las clases de PHP, conoces aunque no hayas usado

5
Programación PHP profesional con Slim, Paris y Twig

la programación orientada a objetos (POO u OOP), sabes lo que son métodos


estáticos, públicos y privados. Conoces, has oído hablar o leído acerca de los
métodos mágicos de PHP.
Idealmente sería genial que hubieras manejado algunos de los patrones de
diseño más comunes ya que a lo largo del libro utilizaremos alguno de ellos:
Singleton, Observer, Factory, etc. No me voy a detener en este tipo de cosas
llegado el punto pues el ámbito del libro en términos de dificultad es de un nivel
intermedio, aunque espero poder abarcar este tema en una segunda revisión o
en otro libro más avanzado.
No quiero desanimarte con su lectura, pero sinceramente me dolería mucho que
aparcaras el libro en un rincón por no tener la base suficiente, base que estoy
seguro de que podrás adquirir revisando las cuestiones que te comento. ¡Ánimo!

Conclusión
He dividido el resto de capítulos del libro en cuatro partes, en la primera daremos
un vistazo muy somero a los componentes que forman parte del modelo MVC
que he elegido para este libro. La segunda parte trata de las herramientas
imprescindibles necesarias para programar en PHP de manera profesional. La
tercera es un repaso en profundidad al patrón MVC usando como material de
trabajo el código de la aplicación de ejemplo My-simple-web.
Por último y como cierre, la última parte trata de complementos que debes
conocer para pasar al siguiente nivel en la programación profesional en PHP.
No pretendo sentar cátedra con los complementos de esta última parte,
simplemente son pinceladas de cuestiones que alguno de nosotros tenemos
asumidas como obvias pero que a los compañeros que empiezan seguramente
les sonará a chino.
Para las próximas revisiones quiero agregar más código en lo referente
a ejemplos prácticos resueltos. Me dejo algunas cosas en el tintero que
probablemente añada en próximas revisiones, sobre todo guiado de los
comentarios que tengas a bien hacerme llegar a través de mi página web
http://www.joseluislaso.es.

6
Parte I: MVC
2 Patrón MVC
Contenido
» Introducción
» ¿Por qué esta elección?
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Sin duda existe mucha literatura acerca del patrón modelo-vista-controlador (MVC)
y no se trata de repetir aquí todos esos conceptos técnicos. Como este libro pre-
tende ser algo eminentemente práctico vamos a enfocar el tema de esa manera.

En este patrón, como su nombre indica, intervienen tres partes:


» El modelo es quien representa el estado que tiene la aplicación en un
momento determinado y normalmente se asocia con la base de datos.
» La vista muestra el resultado al usuario que visita nuestra web.
» Y el controlador organiza todo el conjunto para que la petición que hace
el visitante de nuestra web sea atendida por el método adecuado, se
utilicen y/o persistan los datos adecuados y se muestre el resultado en
forma de una respuesta.

Este es uno de los patrones más extendidos en el desarrollo web actualmente.


Al tener separadas cada una de las capas, la lógica de negocio y la interfaz de
usuario son independientes.

Podemos simplemente sustituir el modelo: si estamos utilizando mysql y quere-


mos escalar a Postgres o en el caso de la vista aislar toda la presentación de la
interfaz de usuario y que los maquetadores la manejen sin complicados lenguajes
de programación.

Este libro trata de eso y por eso su título incluye cada una de las distintas partes
de ese patrón: Slim para la parte del controlador, Paris para la parte del modelo y
Twig para el motor de la vista.

10
2 Patrón MVC

¿Por qué esta elección?


A nivel personal he realizado varios proyectos utilizando estos componentes y
aunque he de decir que el resultado siempre es mejorable (¿cuándo no?), la
curva de aprendizaje es muy rápida y siempre quedan aplicaciones muy robus-
tas y fáciles de mantener con poco esfuerzo.

Retomando la cuestión principal vamos a analizar un poco más en detalle qué


hace MVC.

En nuestro caso el cargador de la aplicación (index.php o bootstrap en adelan-


te) inicializa todo lo necesario para que el controlador (Slim en nuestro caso)
pueda hacerse cargo del control. En el mismo bootstrap por supuesto se iniciali-
zan las cuestiones relacionadas con el acceso a la bbdd y el motor de plantillas.

No me quiero extender mucho en el tema ya que veremos el código del bootstrap


más adelante en profundidad. Simplemente retén en este punto que se crea una
instancia de Slim y que la asignamos a la variable $app, a partir de ahí todos los
archivos que contienen definición de rutas (que controlan la atención a las rutas)
harán referencia a esta variable para decirle a Slim que ha de hacerse cargo de
una ruta determinada y qué tiene que hacer con ella.

11
Programación PHP profesional con Slim, Paris y Twig

Extracto de la inicialización de Slim en el bootstrap (index.php):


// ...

// Prepare app
$app = new \Slim\Slim(...);

// Routes definition for controller part of MVC @1:


require_once '../app/controller/autoload.php';

// Run app
$app->run();

Ejemplo 1: Extracto de inicialización del bootstrap (index.php)

Quedémonos con el require que carga las rutas y veamos por ejemplo la definición
de la ruta home que se hace en el archivo app/controller/frontend/home.php:
@1:
$app->get('/', function () use ($app) {
$app->render('frontend/home/index.html.twig');
})->name('home.index');

Ejemplo 2: app/controller/frontend/home.php

Como ves se ha definido la atención mediante una closure o función anónima1.


Vamos a desmenuzar lo que hace esta primera ruta. Por claridad, no quiero em-
pezar a lanzar conceptos sin más, recuerda en todo caso que esta ruta es muy
sencilla. Además con respecto al código fuente de My-simple-web aún la he sim-
plificado más por motivos pedagógicos.
Como mencioné antes, $app es la instancia de Slim, por eso a lo largo de los ar-
chivos del controlador (donde se definirán las rutas) vamos a ir viendo esta misma
estructura. Solo cambiará lo que haya dentro del controlador y qué método llame-
mos según si la ruta sea GET, POST o ambas.
Insisto en el hecho de que esta es una muy ligera aproximación, no quiero que lo
entiendas todo ahora mismo, solo que veas un atisbo de lo que es el MVC.
Espero no haberte liado mucho en esta primera aproximación, en la tercera parte
vamos a ver de nuevo los componentes del MVC en detalle, basándonos directa-
mente en la aplicación My-simple-web.
1
Closure o función anónima: http://php.net/manual/es/functions.anonymous.php

12
2 Patrón MVC

Conclusión
Aunque corto, he querido en este primer capítulo darte una idea de lo que es el
patrón que vamos a usar como base para nuestra aplicación de ejemplo y espero
que para todos tus desarrollos.

A lo largo de los siguientes capítulos vamos a ir desgranando cada uno de los


componentes para entender el todo a través de sus partes.

13
3 El controlador:
Slim

Contenido
» Introducción
» ¿Cómo funciona?
» ¿Qué hace Slim?
» Declaración de rutas
» ¿Atención duplicada?
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
He elegido Slim1 para mis proyectos porque representa la concentración de un
gran número de funciones en unos pocos Kb.

Está claro que hay otros frameworks, como pueden ser: silex, cakePhp, co-
deIgniter, etc. Por no hablar del todopoderoso Symfony o del omnipresente
Zend, aunque, es cierto que para proyectos no muy grandes ubicados en ser-
vidores comerciales económicos es implanteable usar frameworks altamente
consumidores de recursos. Aun así trabajar con ellos es muy enriquecedor a
nivel profesional.

Sin duda la elección del framework es muy personal, aunque si estás empe-
zando a desarrollar a nivel profesional estoy seguro de que quedarás muy sa-
tisfecho con este.

Además he adornado el pastel con dos componentes imprescindibles: un motor


de plantillas que se está extendiendo como la espuma: Twig, y un ORM muy
ligero y con muy buen resultado: IdiORM/Paris.

La documentación sobre Slim es abundante. Además no es difícil de manipular


para añadirle mejoras.

También puedes, como todos los proyectos que están en github, mandar un
pull request con tus mejoras para que el propietario las incluya en el proyecto,
si lo estima oportuno.

1
http://www.slimframework.com

16
3. El controlador: Slim

En los proyectos más serios y sobre todo a nivel profesional utilizo Symfony,
pero he de reconocer que a efectos de hosting y para proyectos más comunes
es más fácil manejar un proyecto que no sea Symfony por motivos obvios: en
un hosting compartido no puedes alterar la configuración de php ni agregar
nuevos componentes al sistema operativo, cosas que a buen seguro necesita-
rás si usas este framework.

¿Cómo funciona?
Como he dejado caer en el capítulo anterior, Slim se encarga de recoger la
petición, procesar los datos necesarios (de la bbdd o no) y encargarle al motor
de plantillas que genere una vista que luego devolverá en forma de respuesta.
En algunos casos (ajax, api, webservices) no interviene el motor de plantillas,
aunque sí se genera una respuesta, de todas formas por el momento nos va-
mos a centrar en peticiones convencionales en las que sí que hay respuesta
HTML al uso.

En condiciones normales un desarrollo web se puede acometer de estas dos


maneras (grosso modo):
» Invocar todo el núcleo de la aplicación (funciones comunes, cabeceras,
pies, etc.) en cada uno de los archivos de los que se componga nuestra
aplicación, con un include o require.
» Establecer un único punto de entrada en la aplicación. Un único archi-
vo php es el encargado de poner en marcha toda la maquinara. Por
tanto todas las rutas empiezan en el mismo sitio.

Como vas a comprobar, este libro va de buenas prácticas y de cómo aumentar


tu productividad en el desarrollo web, por tanto vamos a descartar directamente
la primera opción. Si te has tenido que enfrentar a un desarrollo y has hecho
justo lo que ahí se enumera habrás podido comprobar en tus carnes lo difícil
que es mantener el código una vez te has desvinculado de él durante un cierto
espacio de tiempo. Si por el contrario has tenido que evaluar alguna aplicación
hecha así también has podido padecer depurando o revisando código.

Es cierto que la primera posibilidad deja muy claro dónde se encuentra la aten-
ción de cada ruta, pero tiene dos inconvenientes importantes:
» Tenemos que repetir código en cada uno de los archivos, pues tendre-
mos mucho código en archivos separados que será necesario incluir.
En la mayoría de los casos, suele consistir en un archivo grueso con
todas las funciones comunes y tal vez un par de ellos más para mostrar
la cabecera y el pie.

17
Programación PHP profesional con Slim, Paris y Twig

» El otro inconveniente, quizás el más importante de cara al posiciona-


miento de la web en los buscadores. Las rutas no quedan bien, ahora
que están de moda las rutas amigables (url-friendly), todas nuestras
rutas serán de este tipo: http://mi-web.com/home.php?data=prueba
Así pues, contando con que un único archivo de entrada para nuestra aplica-
ción será lo conveniente (más que nada por descarte), vamos a ver cómo sería
posible a nivel técnico hacer esto, tenemos dos opciones:
» La primera sería invocar todas las rutas añadiendo el nombre de ese
archivo único: http://my-simple-web.com.es/index.php/articulo/1, esto
básicamente queda descartado de la lectura del punto anterior.
» La segunda opción es utilizar el mecanismo que nos proporciona el
servidor web en lo relacionado a reescritura de urls, cuando en la ruta
no exista un archivo físico se va a generar internamente la ruta con
un archivo físico por defecto, por tanto http://my-simple-web.com.es/
articulo/1 se convierte internamente a la ruta anterior, aunque esta no
se muestra en el navegador del usuario.

Esta reescritura de URL es posible gracias al módulo rewrite en Apache2.

¿Cómo vamos a instruir a Apache para que haga esto?


1) Escribiendo un archivo .htaccess con esta regla.
2) O escribiendo estas indicaciones en el archivo que define el servidor virtual
(vhost) de nuestro dominio.

Particularmente no tengo preferencia por ninguna de las dos opciones pero, lo


cierto y verdad, es que en la mayor parte de los casos nos vamos a encontrar
con la primera ya que salvo que el servidor sea dedicado o virtual no podremos
controlar el archivo vhost.

Por lo tanto, vamos a ver cómo se haría esto:


RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

Ejemplo 3: archivo web/.htaccess

Con este archivo le indicamos a Apache que cualquier ruta que le llegue (que
vaya a ser atendida dentro de esta carpeta, evidentemente), deberá ser
2
No quiero dejar de lado otros servidores web, pero este es el más extendido al nivel
en el que nos vamos a mover.

18
3. El controlador: Slim

verificada, y en caso de no terminar en un archivo físico se le añadirá al


principio index.php

Posteriormente veremos con detalle cómo es ese index.php por dentro. Pero
quédate en este momento con el hecho de que es el cargador de la aplicación
y el encargado de unir todos los servicios, entre ellos a Slim con el resto.

Ahora viene cómo decidir el tratamiento que va a hacerse de la URL, pues en


la mayor parte de los casos necesitaremos parámetros y otras indicaciones que
nos permitan saber qué quiere hacer el usuario. Cuando la ruta termina en un
archivo físico, esto no es un problema, pues en /novedades/marzo/2014/articu-
los.php?art=libro-de-php queda claro que existe un archivo en la carpeta nove-
dades/marzo/2014 y que este va a necesitar un parámetro con el nombre art.

Pero ahora que no necesitamos los archivos físicos, podemos usar toda la ruta
para saber qué quiere hacer el usuario. Por convención se usa la misma estruc-
tura de carpetas, aunque no son necesarias, porque los buscadores indexan el
contenido como si de archivos físicos se tratase, organizados jerárquicamente
en carpetas.

Podríamos tener perfectamente esta ruta: novedades-marzo-2014, el problema


es que el guion se utiliza para separar las palabras cuando conviertes una URL
con espacios.

Sea como sea, el hecho es que nuestro Slim, una vez tenga el control, va a
tomar la URL que le viene y va a hacer algo con ella que enseguida vamos a
descubrir.

¿Qué hace Slim?


Aunque en breve veremos un capítulo dedicado al controlador en profundidad,
voy a describirte básicamente el proceso que hace Slim.

19
Programación PHP profesional con Slim, Paris y Twig

Slim ha ido recogiendo durante el bootstrap3 todas aquellas rutas que hemos
ido declarando y cuando se lanza $app->run() lo que hace es matchear
la ruta que le viene en la petición con la expresión de una de esas definicio-
nes, por supuesto también tiene que hacer coincidir el método de la petición
(GET,POST, etc.).

Podemos definir más de una ruta que pueda atender la misma URL, pero clara-
mente se atenderá la primera definida. ¿Por qué querer definir rutas así? Luego
lo veremos.

¿Qué pasa si una ruta no es reconocida? Sencillamente se lanza una excep-


ción de ruta no encontrada (404) que Slim nos permite personalizar. Hablando
de eso, también podemos personalizar la pantalla de errores, lo veremos en la
página 102.

Declaración de rutas
Ruta lógica versus ruta física
Básicamente lo que hace Slim es mapear las rutas de las URL (rutas físicas)
con una rutas internas que son atendidas por un segmento de código.

Para que los programadores nos entendamos mejor y sobre todo para poder
reutilizar las rutas, se implementa un mecanismo de ruta lógica que mapea
esa URL real con un nombre descriptivo. De esta manera como humanos re-
cordaremos mejor la secuencia articulo_prensa_introduccion que /blog/
articulos/index/search/introduccion-a-la-empresa, (está exagerado para que lo
veas más claro).

3
No lo volveré a mencionar, recuerda que bootstrap en nuestro caso es el index.php
o cargador de la aplicación.

20
3. El controlador: Slim

Además, si por motivos de SEO, usabilidad, url-friendly o cualquier cosa que


esté relacionada con las URL cambiamos la ruta física, solo necesitamos alte-
rarla en la instrucción que le dice a Slim qué ruta tiene que atender (el $app->
get(...)), porque en el resto de los lugares donde hayamos hecho referencia
a esa ruta mediante su nombre lógico no será necesario.

Para referirnos a las rutas lógicas necesitamos usar dos métodos que nos van
a permitir convertir las rutas lógicas a físicas: uno para el caso de la vista y otro
para el caso del controlador.

No creo que tenga que recordarte que en las etiquetas HTML o en las redirec-
ciones PHP tenemos que indicar una URL física y correcta.

De esta manera, en un twig usaremos la extensión urlFor de esta manera:


{{ urlFor('articulo_prensa_introduccion') }} para generar esa URL
para un enlace, una imagen, etc. y en los controladores que deban redirigir al
usuario a otra URL haremos uso del método $app->urlFor('articulo_pren-
sa_introduccion'); de la misma forma.

A lo largo de todo el código de la aplicación My-simple-web veremos ejemplos


de ambos casos. Quería introducirte aquí el tema porque puede parecer un
poco lioso al principio hablar de dos tipos de rutas cuando en realidad estamos
hablando de lo mismo, salvo por el hecho de que etiquetamos con un nombre
la ruta real4.

Manos a la obra
Veremos a lo largo del libro cientos de veces la declaración de rutas, pero te
presento aquí el mecanismo para que te vaya sonando.

Slim permite ir declarando rutas mediante:


// siendo $app una instancia de Slim tal que $app = Slim::getInstance();
$app->get('/ruta/fisica', 'funcion_atiende_ruta')
->name('ruta.logica.opcional');

¿Atención duplicada?
Imagínate que quieres que la aplicación atienda rutas como: /es/quiero-que-
me-conozcas o /es/contacto, de tal manera que quiero-que-me-conozcas y
contacto son slugs que están guardados en la bbdd en la tabla de páginas
estáticas (argucia muy útil para delegar el mantenimiento de ese contenido en

4
No confundir con slug ya que slug es parte de la ruta real.

21
Programación PHP profesional con Slim, Paris y Twig

el administrador de la web y no en el programador), pero no queremos que se


interprete /formalizar/pedido en el mismo método. Lo que tenemos que hacer es
declarar la ruta que se atenderá como $app->get('/:lang/:slug') y dentro
de la función que atiende la ruta cuando detectemos que el :slug pasado no coin-
cide con los que tengamos declarados o almacenados en la bbdd ejecutaremos
la orden $app->pass(), que obligará a Slim a seguir procesando el matching
del routing. Evidentemente tendrá que existir otra declaración que sea capaz de
atender /formalizar/pedido. En otro caso se lanzará una NotFoundException.

Conclusión
Me doy por satisfecho si en este punto del libro he sabido transmitirte, dentro
del concepto de patrón MVC, cuál es el cometido del controlador (Slim en nues-
tro caso), el cual vamos a usar a lo largo de todo el texto.

En la tercera parte trataremos en profundidad el controlador y volveremos a


todo lo aquí expuesto de nuevo con mucho más detalle y viendo código en
abundancia.

22
4 El modelo:
IdiORM & Paris

Contenido
» ¿Qué es un ORM?
» ¿Por qué un ORM?
» Métodos disponibles
» Rizando el rizo
» Relaciones entre tablas
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

¿Qué es un ORM?
Un ORM es una capa de abstracción sobre la de manejo de los datos que permite
manipularlos mediante una interfaz diferente a la que habitualmente conocemos,
que no es otra que el PDO que nos proporciona PHP. El uso de un ORM incluye
algunas ventajas como el mapeo de registros sobre clases de PHP.

Si tenemos que leer un registro de una tabla con las órdenes que nos provee
PHP haríamos primero un SELECT y, conociendo de antemano la estructura
de ese registro, accederíamos al campo nombre así:

$sql = "SELECT * FROM `students` WHERE `id`= 1";


$result = mysql_query($sql);
$row = fetch_result($result);
print $row["nombre"];

Pero, de esta manera, no sabemos si estamos recibiendo un registro de clase


Student, AdvancedStudent o BasicStudent, por ejemplo.

Si creamos una clase que tenga los campos necesarios y creamos una interfaz
entre la bbdd y nuestra aplicación, que solo con pedirle que recupere el registro
x, lo haga y además despliegue los campos leídos sobre las propiedades de
nuestra clase, tendremos un ORM.

Existen varias implementaciones de este patrón y en nuestro caso, IdiOrm im-


plementa el patrón ActiveRecord, que dota a la clase de los métodos para ac-
tualizar el registro, borrarlo o crearlo.

24
4. El modelo: IdiORM & Paris

En realidad nuestro ORM no nos va a exigir que creemos las propiedades que
va a tener nuestro registro, sino que mediante el uso de los métodos __get y
__set va a hacer un mapeo virtual.

Hay otras implementaciones más robustas en las que todo debe estar correc-
tamente declarado: los nombres de las propiedades, los tipos de las mismas,
incluso validaciones de blancos, etc., como pueden ser Doctrine y Propel.

Los ORM más serios incluyen mecanismos para hidratar los registros con las
respectivas relaciones. Como ya sabrás una bbdd relacional permite mante-
ner las tablas relacionadas entre ellas, Doctrine te permite cargar un cliente y
detrás todas las facturas que tenga vinculadas, direcciones de envío, etc. No-
sotros veremos cómo hacer eso más tarde con nuestro ORM empleando unas
cuantas instrucciones.

Pero ya he mencionado anteriormente que la elección de los distintos com-


ponentes que se usan en este libro no es arbitraria y siempre he optado por
elementos de calidad y de una sencillez tal que permita obtener un resultado
profesional sin una carga de servidor alta.

En todo caso, en proyectos serios o de cierta envergadura me plantearía usar


Symfony+Doctrine+Twig.

Para nuestro proyecto de ejemplo y estoy seguro de que para el 90% de los
trabajos que lleves a cabo a lo largo de tu vida profesional este es más que
suficiente, además de que aprendiendo las bases con este ORM el salto a otro
más complejo no supone tanto esfuerzo.

¿Por qué un ORM?


La verdad es que es factible acometer cualquier proyecto escribiendo directa-
mente las sentencias SQL a través de la interfaz que nos proporciona PHP, ya
sea con las sentencias mysql_x o sus equivalentes para otros PDO.

Para serte franco he visto y he hecho bastantes proyectos en los que así se ha
solucionado la parte del modelo. Pero creo que estarás de acuerdo conmigo
en que si estamos viendo cómo hacer proyectos siguiendo el patrón MVC no
queda muy apropiado utilizar sentencias directamente o no reutilizar código,
ofuscarlo, o cualquier otra mala práctica. De alguna manera, estamos sentando
las bases de la programación profesional.

Para que entiendas perfectamente lo que te digo voy a ponerte un ejemplo ima-
ginario de cómo acceder a una tabla de alumnos en la cual tenemos un ID y un
nombre. Voy a presentarte dos planteamientos, uno basado en las sentencias

25
Programación PHP profesional con Slim, Paris y Twig

convencionales que seguro que conoces, y el otro en la aplicación del ORM que
vamos a ver: Paris sobre IdiORM. El código fuente de este capítulo lo puedes
encontrar en http://github.com/jlaso/book-spt-code

Para poder llevar a cabo los ejemplos de este capítulo vas a necesitar una base
de datos mysql dentro de tu equipo con nombre “db_test”, con permisos com-
pletos para el usuario “user” con password “password”, donde veas estos litera-
les en los códigos de ejemplo sustitúyelos por los que hayas puesto en tu caso.

Al primer ejemplo lo he llamado sample_plain.php y queda así:


<?php

$hdl = mysqli_connect('localhost', 'user', 'password', 'db_test');

mysqli_query($hdl, <<<EOD
CREATE TABLE IF NOT EXISTS `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
EOD
);

$rows = mysqli_query( $hdl, 'SELECT * FROM `student`');

while ($row = $rows->fetch_assoc()){

print sprintf ('%06d | %30s' . PHP_EOL, $row['id'], $row['name']);

Ejemplo 4: sample_plain.php, acceso en plano a la bbdd

Esta sería la salida de la ejecución del ejemplo en php plano:


> php sample_plain.php
000001 | JOSELUIS
000002 | ANGEL

El código mostrado no inicializa los datos que muestro en el listado anterior,


para poder producir esa salida los he incorporado manualmente a la tabla.

Vamos a ver ahora el mismo ejemplo utilizando un poco de la magia del ORM ;<)

Voy a aprovechar y retomo el composer para que veas lo fácil que es integrar
una librería en nuestro proyecto, evidentemente si queremos trabajar con el
ORM necesitaremos las clases necesarias para esto.

26
4. El modelo: IdiORM & Paris

Antes de nada en una carpeta vacía creamos un archivo composer.json con el


siguiente contenido:
{
"authors": [
{
"name": "Joseluis Laso",
"email": "jlaso@joseluislaso.es",
"homepage": "http://www.joseluislaso.es",
"role": "Developer"
}
],
"name": "jlaso/sample-orm-spt",
"description": "ORM sample for SPT book by joseluislaso.es",
"require": {
"php": ">=5.3.0",
"j4mie/paris": "1.2.0"
}
}

Quiero que te acostumbres a ver (y entender) este tipo de archivos. Dedico un


capítulo a composer más adelante, pero interesa que vayas fijando concep-
tos: las líneas más importantes son las que están en la cláusula require, pues
indican en primer lugar la versión mínima de PHP requerida y qué módulo/s
vamos a necesitar, para que composer se los descargue de sus respectivos
repositorios. Recuerda que por defecto composer busca en packagist.org y de
ahí es donde saca la URL definitiva. Es algo así como un gestor de DNS. Por
tanto si en el futuro quieres desarrollar módulos para compartir con el mundo
debes pasar por Packagist.

Como anécdota curiosa, en el momento de escribir estas líneas estaba fuera


de cobertura y por tanto sin Internet, pero composer, como mantiene una caché
en el equipo, fue capaz de «descargar» el paquete solicitado.

Te muestro su salida por el terminal:


> composer install
Loading composer repositories with package information
The "https://packagist.org/packages.json" file could not be
downloaded: php_ network_getaddresses: getaddrinfo failed:
nodename nor servname provided, or not known
failed to open stream: php_network_getaddresses: getaddrinfo
failed: nodename nor servname provided,or not known
https://packagist.org could not be fully loaded, package information
was loaded from the local cache and may be out of date
Installing dependencies (including require-dev)
- Installing j4mie/idiorm (v1.2.3)
Loading from cache

27
Programación PHP profesional con Slim, Paris y Twig

- Installing j4mie/paris (v1.2.0)


Loading from cache

Writing lock file


Generating autoload files

Primero voy a mostrarte la salida del mismo ejemplo usando el ORM para que
veas que el resultado es el mismo:

> php sample_orm.php


000001 | JOSELUIS
000002 | ANGEL

Lo que varía evidentemente es cómo hemos llegado hasta él. Veamos juntos el
código de sample_orm.php:

<?php

// @1
require __DIR__ . '/vendor/autoload.php';

// @2
/** esta clase mapea la tabla con una clase PHP (thanks ORM) */

class Student extends Model


{

// @3
ORM::configure('mysql:host=localhost;dbname=db_test');
ORM::configure('username', 'user');
ORM::configure('password', 'password');

// @4
$allStudents = Model::factory(‘Student’)->find_many();

// @5
foreach ($allStudents as $student){
print sprintf('%06d | %30s' . PHP_EOL, $student->id, $student->name);
}

Ejemplo 5: sample_orm.php, acceso mediante ORM a la bbdd

28
4. El modelo: IdiORM & Paris

NOTA: aunque no es lo mismo me refiero a Paris y a IdiORM como ORM


indistintamente, normalmente no vamos a hacer uso de las funciones de
IdiORM directamente, salvo casos muy concretos, como la inicialización, el
log, o acceso físico a las tablas. Siempre que podamos utilizaremos Paris, es
por ello que utilizo la palabra ORM para referirme al sistema completo, que no
es otro que un mapeo de los registros en objetos: Object Relational Mapping.

Repasando el código:

@1:

Antes que nada necesitamos incluir en nuestro código los vendors que nos ha
creado composer en base a nuestros requerimientos del archivo composer.
json. Nos ha facilitado la tarea y ha creado un autoload.php que con solo in-
cluirlo nos deja accesibles todas las clases (composer hace uso extensivo de
los namespaces de PHP 5.3). En todo caso a partir de ese require podemos
invocar los métodos y clases de Paris e IdiORM.

@2:

Ahora vamos a ver quizás lo más importante. Necesitamos crear una clase
que extienda de Model la clase que nos aporta Paris. Esta herencia nos va a
permitir emplear todos los métodos del ORM sobre nuestra clase y nos va a
asociar cada campo de la tabla a una propiedad «pública» del objeto (puedes
ver que en realidad no existe tal propiedad y se hace uso de los métodos má-
gicos __get y __set).

En principio no se necesita declarar nada más en la clase, esa herencia nos


permite hacer uso de la potencia del ORM.

@3:

A continuación configuramos el ORM para el acceso a nuestra DB y tabla, el


código es autoexplicativo.

@4:

Y ya por fin llegamos el meollo del asunto: el acceso a los registros. Puedes ob-
servar que en realidad no hemos hecho uso directamente de la clase Student
que creamos, sino que mediante un patrón factoría1 invocamos su creación a
través de la superclase Model.

1
factory pattern

29
Programación PHP profesional con Slim, Paris y Twig

La gracia de todo esto es que con la simple línea: $allStudents =


Model::factory(‘Student’)->find_many(); hemos leído todos los registros
de la tabla.

Está claro que internamente al final el ORM hace un SELECT como habíamos
hecho en el ejemplo plano anterior, pero creo que estarás de acuerdo conmigo
en que es mucho más clara la sentencia anterior que el SELECT en plano de
SQL. Este, en cuanto tenga un par de condiciones WHERE tienes que analizarlo
mentalmente cada vez para saber qué pretende el programador obtener en esa
línea, por no hablar del mantenimiento futuro.

@5:

Si lo anterior no te ha convencido espero que esto lo haga (he prescindido del


sprintf por claridad):

foreach ($allStudents as $student){

print ($student->id . $student->name);

Ejemplo 6: bucle recorriendo registros obtenidos mediante el ORM

Y comparando:

while ($row = $rows->fetch_assoc()){

print ($row['id'] . $row['name']);

Ejemplo 7: bucle recorriendo registros obtenidos con sentencias Mysql

Sin palabras.

Métodos disponibles
A lo largo de los ejemplos del libro y el código de nuestra aplicación My-simple-
web verás instrucciones del ORM que seguramente no te costará seguir pues
el autor de IdiORM&Paris ha sido muy oportuno con los nombres de las mis-
mas, identificando plenamente la acción que realizan.

No obstante, para evitar malentendidos o para que lo tengas como referencia


he creado esta tabla para ti:

30
4. El modelo: IdiORM & Paris

Método Explicación Ejemplo


Sin duda la madre del cor-
$cliente =
dero, permite instanciar
::factory($class) Model::factory('Cliente')
una clase mediante su ->create_one();
nombre.
Devuelve una instancia $cliente =
create_one() limpia de la clase invoca- Model::factory('Cliente')
da mediante ::factory. ->create_one();
Recupera un registro de la
$cliente =
base de datos que corres-
find_one($id) Model::factory('Cliente')
ponda con el identificador ->find_one(11);
pasado.
$cliente =
Impone una condición a la Model::factory('Cliente')
where($fl d,$cond)
búsqueda. ->where('id', 10)
->create_one();
Las distintas condiciones
pueden ser xxx: $clients =
- eq => igual que Model::factory('Cliente')
where_xxx($cond)
- gte => mayor o igual que ->where_gte('cp', 46000)
- lte => menor o igual que ->find_many();
- lt => menor que
Permite introducir con- $people = ORM::for_
sultas que no se puedan table('person')
->where('name', 'Fred')
hacer con las órdenes del
->where_raw('(`age` = ?
where_raw($raw) ORM o una consulta OR
OR `age` = ?)',
que no se puede hacer de array(20, 25))
otra manera, ya que los ->order_by_asc('name')
where se suman (and). ->find_many();
$people =
Permite efectuar consul- ORM::for_table('person')
->raw_query(
tas directas, sobre todo
'SELECT p.* FROM person p
raw_query($raw) para aquellas que no se
JOIN role r ON p.role_id = r.id
pueden realizar mediante WHERE r.name = :role',
la sintaxis del ORM. array( 'role' => 'janitor' ))
->find_many();

Persite la entidad en la
base de datos, si el re-
save() gistro es nuevo utilizará $cliente->save();
un INSERT y si ya existía
hará un UPDATE.

31
Programación PHP profesional con Slim, Paris y Twig

Método Explicación Ejemplo


Indica si un campo ha if($cliente->is_
sido cambiado o no des- dirty('nombre')){
is_dirty($field)
de la última escritura en // ....
bbdd o su creación. };

Model::factory('Cliente')
Permite borrar varios re-
delete_many() ->where('borrado', 1)
gistros al mismo tiempo. ->delete_many();

Devuelve el número de Model::factory('Cliente')


count() elementos que produce ->where('borrado', 0)
una consulta. ->count();

Permite utilizar como va- $usuario->


set_expr($fl d,$val) lor de un campo una ex- set_expr('last_logged_in' ,
presión de MySQL. 'NOW()');

Devuelve una entidad en


as_array() $usuario->as_array();
forma de array asociativo.

Rizando el rizo
El ORM nos permite hacer prácticamente todas las consultas que podamos hacer
con sentencias SQL directas. No obstante, las mezclas imposibles de obtener en el
ORM debidas a su naturaleza, se pueden obtener mediante raw_query o where_
raw, este sería el caso de un where con condiciones OR, que veremos después.

Pero lo más importante: puedes crear relaciones entre las tablas (lo vemos
enseguida).

Acceder a un registro mediante su ID es tan sencillo como:

$student = Model::factory('Student')->find_one(1);

Y por qué no: hacer filigranas con la clase que deriva de Model. He dicho que
solo tiene que extender de Model, para un funcionamiento normal, es decir,
acceder a registros, persistir, etc. La implementación de los métodos mágicos
__get y __set ayudan al ORM a mapear los campos y hacer el trabajo sucio
por nosotros. Pero si necesitamos validar el objeto antes de persistirlo pode-
mos hacerlo, si queremos rellenar unos valores por defecto cuando creamos un
registro nuevo, podemos hacerlo.

Veamos la sencillez y claridad de distintos ejemplos de inserción, actualización y


borrado de un registro en la tabla. Y lo vamos a comparar con su hermano «plano».

32
4. El modelo: IdiORM & Paris

Plano ORM
mysqli_query(
$hdl, $student = Model::factory('Student')
"INSERT INTO `student` ->create();
Insertar (`name`) VALUES $student->name = 'JOSELUIS';
('JOSELUIS')" $student->save();
);

$name = "JOSELUIS";
$rows = mysqli_query($hdl,
sprintf("SELECT * FROM `student` $name = "JOSELUIS";
WHERE `name` = '%s'", $name)); $student = Model::factory('Student')
if ($student = $rows ->where('name', $name)
->fetch_assoc()){ ->find_one();
mysqli_query($hdl, if($student instanceof Student){
sprintf("UPDATE `student` $student->name = $name . '-' .
Update SET `name` = '%s-%d' date('U');
WHERE `id` = '%d'", $student->save();
$name, date('U'), }else{
$student['id'])); throw new \Exception('Alumno ' .
}else{ $name . ' no existe');
throw new \Exception( }
'Alumno ' . $name .
' no existe');}

// delete
$name = "JOSELUIS";
$rows = mysqli_query($hdl,
sprintf("SELECT * FROM `student` $name = "JOSELUIS";
WHERE `name` = '%s'", $name)); $student = Model::factory('Student')
if ($student = $rows ->where('name', $name)
->fetch_assoc()){ ->find_one();
mysqli_query($hdl, if($student instanceof Student){
Delete sprintf("DELETE $student->delete();
FROM `student` }else{
WHERE `id` = '%d'", throw new \Exception('Alumno ' .
$student['id'])); $name . ' no existe');
}else{ }
throw new \Exception(
'Alumno ' . $name .
' no existe');
}

Relaciones entre tablas


La ventaja de utilizar bbdd relacionales es que podemos crear relaciones entre
las tablas. Por ejemplo, un curso puede tener varios alumnos y un profesor
puede impartir varios cursos. Técnicamente esto se puede resolver con senten-

33
Programación PHP profesional con Slim, Paris y Twig

cias SQL sencillas. Podemos por ejemplo seleccionar los alumnos que están
inscritos al curso con ID n.º 2, o podemos acceder al curso con ID n.º 2, e indi-
car al ORM que existe una relación entre cursos y alumnos, de tal manera que
con la instrucción $curso->alumnos() nos dé la lista de alumnos que están
inscritos al curso en cuestión.

Nuevamente esto plantea un reto en la construcción de las sentencias, al final,


el ORM va a hacer lo mismo que nosotros realizaríamos a mano, pero de una
manera más metódica y más estructurada, quedando al final las sentencias
más claras y más mantenibles.

Aunque ampliaré en la tercera parte este concepto quiero que te hagas una
idea de cómo sería una relación entre las tablas alumnos y cursos:

CREATE TABLE `student` (


`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`grade_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE= InnoDB AUTO_INCREMENT=1 DEFAULT
CHARSET=latin1;

CREATE TABLE `grade` (


`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

<?php

require __DIR__ . '/vendor/autoload.php';

class Student extends Model


{
public function grade() {
return $this->has_one('Grade');
}
}

class Grade extends Model {


public function students() {
return $this->has_many('Student');
}
}

34
4. El modelo: IdiORM & Paris

// ...

$grade = Model::factory('Grade')->create();
$grade->name = '1st';
$grade->save();

// insert
$student = Model::factory('Student')->create();
$student->name = 'JOSELUIS';
$student->grade_id = $grade->id;
$student->save();
$student = Model::factory('Student')->create();
$student->name = 'ANGEL';
$student->grade_id = $grade->id;
$student->save();

// retrieve all studentes in grade


$allStudents = $grade->students()->find_many();

foreach ($allStudents as $student){


print sprintf('%06d | %30s' . PHP_EOL,
$student->id, $student->name);
}

Ejemplo 8: sample_relations.php

Como ves es fácil integrar las relaciones dentro del ORM, lo veremos con más
detalle cuando veamos el modelo en profundidad, en el capítulo 9.

Conclusión
Espero haberte transmitido por qué es más ventajoso el uso de un ORM que
las sentencias SQL incrustadas directamente en el código:
» Claridad
» Mantenimiento
» Menos acoplamiento con la bbdd, al no escribir directamente senten-
cias SQL podemos migrar a un sistema NOSQL, SQLITE, etc.

Más adelante volveremos a adentrarnos en el modelo usando como base la


aplicación My-simple-web y veremos cómo hacer algunas filigranas.

35
5 La vista: Twig
Contenido
» ¡Ay, la vista, la vista!
» Vamos por partes
» ¿Qué es Twig?
» ¿Cómo funciona?
» Tipos de datos
» Toma de decisiones
» Iteraciones
» Funciones de Twig
» Extender Twig con funciones propias
» Macros
» Resumen
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

¡Ay, la vista, la vista!


Todo cuanto haga nuestra aplicación: los más complejos cálculos, los accesos
mejor planeados a la bbdd, nuestra política de seguridad, ¡todo! De nada servi-
rá si luego la presentación que le hagamos a nuestro visitante de la aplicación
web que hemos creado no está cuidada.

De nuevo, como en otros capítulos, quiero introducir este con la famosa pregunta:

¿Por qué usar un motor de plantillas como Twig?


¿Por qué no escribir HTML directamente? Es más, ¿por qué no mezclar el có-
digo PHP con el HTML, como se ha hecho siempre?

Espero que al final de este capítulo no solo las preguntas anteriores te parez-
can inoportunas sino que en algunos casos hasta las consideres aberraciones.

Te voy a resumir la historia de un programador, que va viendo que la faena se


le amontona y, en un momento dado, se da cuenta de que todas las páginas
que muestra al final cuentan con una cabecera, un cuerpo, un pie, un sidebar,
etc., y cómo trata de suplir todos estos bloques con archivos independientes
php que puede integrar como si fuera un puzle, para que al final le quede algo
parecido a esto:

<?php

include 'header.php';
include 'body.php';
include 'footer.php';

Ejemplo 9: bloques separados: header, body y footer

Y con suerte… y con esfuerzo, podría llegar a tener un sistema más sofisticado,
parecido a este:

<?php

/** define('NO_SIDEBAR', false); */


include 'view-blocks.php';

drawHeader($options);
drawBody($data, NO_SIDEBAR);
drawFooter($options);

Ejemplo 10: pintado de bloques por método

38
5. La vista: Twig

De acuerdo, cada uno hemos utilizado nuestro truquillo para no tener que repetir
el código de un bloque a otro.

Existen varios motivos para ello:


» Para no repetir código sin más.
» Para facilitar el mantenimiento del código, ya que las repeticiones in-
ducen a errores en las actualizaciones.

Lo que ocurre es que en el primer ejemplo la parte del header.php suele quedar
coja, pues termina en la etiqueta </head> o <body> que se cerrará normal-
mente en el archivo footer.php, si tenemos un IDE con realce de sintaxis (lo
cual recomiendo sobremanera) nos estará avisando todo el tiempo de que hay
etiquetas que no están cerradas o no han sido abiertas.

Para el segundo caso: utilizando métodos tendremos más éxito. Es fácil que po-
damos hasta crear temas (themes o skins) diferentes, pero a la hora de incrustar
css, javascript, etc., se nos va a hacer eterno, a no ser que incrustemos el HTML,
css y js dentro de las funciones, con la consiguiente pérdida de visibilidad.

<?php // view-blocks.php

function drawHeader($options)
{

?>
<html>
<head>
<title><?php echo $options['title']; ?></title>
</head>
<body>
<?php

Ejemplo 11: pintar bloque Header con función

De verdad te digo que si lo de arriba te parece normal el salto que te voy a pro-
poner a continuación te va a parecer de 100 metros.

Vamos por partes


Idealmente, si no tuvieramos un motor de plantillas, para llevar a cabo la se-
paración de capas en la que se basa este libro deberíamos tener los archivos
que generan la vista de manera separada, y evitar hacer en ellos cálculos

39
Programación PHP profesional con Slim, Paris y Twig

complejos y no acceder directamente a la base de datos, por lo que técnica-


mente podríamos tener un sistema parecido al anterior:

<?php

function renderView($viewFile, $options)


{
$template = file_get_contents ($viewFile);
echo processTags($template, $options);
}

function processTags($template, $options = array())


{
$found = preg_match_all("/{{(.*)}}/", $template, $matches, PREG_SET_ORDER);
if($found){
foreach($matches as $match){
$var = trim($match[1]);
if(!isset($options[$var])){
throw new Exception('Variable ' . $var . ' no existe');
}
$template = str_replace($match[0], $options[$var], $template);
}
}

return $template;
}

// Y la invocaríamos así:
renderView('demo.template', array(
'title' => 'mi web',
'content' => 'Aquí va el contenido de mi primer div',
'div_id' => 'main',
)
);

Ejemplo 12: función para generar bloque con sustitución

El archivo demo.template podría contener algo así:


<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<div id=”{{ div_id }}”>
{{ content }}
</div>
</body>
</html>

Ejemplo 13: contenido de demo.template

40
5. La vista: Twig

Y el resultado de ejecutar php demo.php en la consola sería este:

> php demo.php


<html>
<head>
<title>mi web</title>
</head>
<body>
<div id="main">
Aquí va el contenido de mi primer div
</div>
</body>
</html>

He perdido un poco más de tiempo en estos ejemplos porque quiero dejar claro
que Twig no hace magia, aunque te lo pueda parecer, es muy potente y robus-
to, pero al final son solo sustituciones y cálculos.

Si lo anterior ha quedado bien explicado, en la medida en que comprendas


cómo funciona el código y visto por qué una aproximación así es mejor que
mezclar etiquetas HTML dentro del archivo php, me doy por satisfecho.

A continuación voy a entrar de lleno en el motor de plantillas Twig, ¡Agárrate


fuerte!

NOTA
No dejes de visitar la web oficial de Twig: http://twig.sensiolabs.org/

¿Qué es Twig?
Twig es un motor de plantillas creado por Fabien Potencier, en el que su mar-
cado es totalmente simétrico y que actualmente es muy conocido porque se
integra de serie en el conocido framework Symfony. Hay más motores de plan-
tillas: Smarty, Django, etc. ¿Por qué he elegido este? Bueno, en realidad me
lo presentó un compañero (gracias Fran ;<) y me enamoró desde el principio.
Una vez que trabajas con él, la verdad es que cuesta realizar proyectos sin
contar con su inestimable colaboración.

41
Programación PHP profesional con Slim, Paris y Twig

¿Cómo funciona?
He querido introducir el concepto en el ejemplo anterior, un archivo twig no
es más que código HTML con un marcado especial, para ello Twig se reserva
unas marcas basadas en llaves (como ya he dicho, simétricas) que le permiten
llevar a cabo toda su «magia».

Modelo Ejemplo Sirve para

{{ }} {{ variable }} Imprime el contenido de variable.

{% %} {% set variable = valor %} Ejecuta la acción.

{# #} {# version 1.0 #} Marca un comentario.

Muy sencillo, ¿no? Las marcas más usuales son la pareja {{ }}, mientras que
{% %} nos permite aumentar la potencia de nuestras plantillas introduciendo
cálculos, condiciones, etcétera.

No voy a extenderme acerca de {# #}, solo decirte que su uso es altamente


recomendable y que el contenido del comentario no aparece luego en el código
fuente (HTML) de la página generada.

En este punto debes ver a la pareja {{ }} como un simple print o echo de lo


que contiene dentro. En cambio el par {% %} sirve para ejecutar acciones, cam-
biar el control de ejecución de la plantilla, declarar variables, etc. Lo que hace
Twig no es la mera sustitución como en el . En realidad Twig genera archivos
php con el contenido desarrollado para que la ejecución subsiguiente sea más
rápida. Esta caché de Twig se configura cuando se levanta su instancia en el
bootstrap.

Herencia de plantillas
Imagínate que tenemos un layout general con lo básico: etiqueta html, head,
body, los css que se cargan en todas las páginas, los javascripts, etc. Un archi-
vo que por sí mismo es un HTML bien formado.
{# base.html.twig #}

<html>
<head>
<title>{{ title }}</title>

42
5. La vista: Twig

</head>
<body>
<div>
{{ content }}
</div>
</body>
</html>

Ejemplo 14: base.html.twig básico

De nada nos valdría tanto motor de plantillas si estuvieramos en el mismo caso


anterior, repitiendo código.

Imagínate que cada vez que quieras crear una página nueva tengas que copiar
el código anterior e ir ampliando conforme lo necesitaras en esa nueva página.
Absurdo, ¿verdad?.

Pues es aquí donde entra la etiqueta {% extends %}. La vamos a ver con un
ejemplo.

Supón que tienes el archivo base.html.twig anterior, el cual va a hacer de


base. En él hemos declarado unos bloques (que pueden tener contenido a prio-
ri o no) y que dichos bloques van a ser sustituidos/ampliados en las plantillas
que extiendan (hereden de) esa plantilla base. De este modo tendremos un ar-
chivo bastante completo que hará la estructura básica del archivo HTML gene-
rado y después habrá varias capas que irán completando los huecos. Algunas
limitaciones a esto es que las plantillas hijas no pueden inventarse bloques, los
que hayan quedado declarados en el base son los que se podrán usar, pero por
otro lado dentro de un bloque sí se pueden definir más bloques.

No te lío más, vamos a verlo con un ejemplo. Tampoco se permite la herencia


múltiple con varios extends, al igual que le ocurre a php.

Como antes, tenemos un archivo base.html.twig, esta vez más completo.

{# base.html.twig #}

<html>
<head>
<title>{{ title }}</title>
{% block stylesheets %}
<style type="text/css">
body{
font-family: Arial;
}
</style>
{% endblock stylesheets %}

43
Programación PHP profesional con Slim, Paris y Twig

</head>
<body>
<div>
{% block content %}{% endblock content %}
</div>
</body>
{% block javascripts %}
<script type="text/javascript">
alert('hola');
</script>
{% endblock javascripts %}
</html>

Ejemplo 15: base.html.twig completo

Ahora creamos un archivo frontend.html.twig que será el que utilizaremos


para la parte frontal de nuestra aplicación. Se hace así porque suele diverger
de la parte backend sobre todo en css y javascripts cargados, y también suele
tener otros bloques, pero al final podemos montar la estructura que queramos.
Ten en cuenta que esto es un ejemplo didáctico, sobre todo para que veas que
la herencia es multinivel.

{% extends 'base.html.twig' %}

{% block content %}
<div id="main">
{% block main_block %}
{% endblock main_block %}
</div>
<div id="footer">
{% block footer %}
<p>Este es el footer de mi web</p>
{% endblock footer %}
</div>
{% endblock content %}

Ejemplo 16: frontend.html.twig

Como te decía, esta capa intermedia utiliza base.html.twig de base (lo extiende)
y respetando sus bloques javascript y stylesheets lo que hace es fraccio-
nar el bloque content en dos, ya que quien ha diseñado el frontend ha pensado
que el footer es tan común que lo saca fuera, pero por otro lado lo deja como
bloque porque hay una página en el frontal que no necesita footer (por ejemplo
la del vídeo promocional).

Ahora veremos el desarrollo de la página home.

44
5. La vista: Twig

{% extends 'frontend.html.twig' %}

{% block main_block %}
<div class="cointainer">
bla bla bla ...
</div>
{% endblock main_block %}

Ejemplo 17: home.html.twig

Desde luego un ejemplo muy simplificado, recuerda que lo que quiero que lle-
gues a comprender es la dinámica.

Ahora sí te puedes hacer una idea de lo que permite el uso de un motor de


plantillas como Twig, ¿verdad? Pues espérate, que esto no es todo.

Tipos de datos
Los tipos de datos que puede manejar Twig son todos aquellos que pueda
manejar PHP, hasta tal punto que cualquier clase que tenga getters sobre atri-
butos protected o private son accesibles directamente desde Twig, bien con el
nombre del método getValor() o solo mediante valor1. Veamos un ejemplo:

{# ejemplo getters #}
<pre>
class SampleClass
{
private $valor;

public function getValor()


{
return $this->valor;
}
}
</pre>

Si a esta plantilla la llamamos con una instancia de SampleClass llamada sample


el valor de sample.valor es {{ sample.valor }} o {{ sample.getValor() }}

Ejemplo 18: acceso a objeto desde Twig

Además, todos los tipos de datos escalares, los arrays convencionales y los
arrays asociativos, son accesibles directamente por su clave (numérica o aso-
ciativa) o iterando sobre ellos.

1
Evidentemente las propiedades públicas también son accesibles directamente.

45
Programación PHP profesional con Slim, Paris y Twig

Veámoslo con un ejemplo:

Si necesitas un array para gestionar diferentes paletas dentro de tu plantilla


puedes declararlo dentro de Twig de la siguiente manera:

{% set paletaRaw = [ 'rosa', 'azul', 'blanca' ] %}


{% set paletaAsoc = {
'rosa': '#FF4455',
'azul': '#4455FF',
'blanca': '#FFFFFF'
}
%}

Ejemplo 19: declaración de array asociativo dentro de Twig

Espero que te hayas hecho una idea.

Toma de decisiones
Para completar la guinda del pastel, podemos utilizar condicionales.

Por ejemplo, imagina que quiero mostrar el nombre del usuario si este ha inicia-
do sesión y un enlace al login en caso contrario.

{% if user is defined %}
Bienvenido, {{ user }}
{% else %}
<a href="login">Login</a>
{% endif %}

Ejemplo 20: condicional en twig

Tambien tenemos la condicion ternaria: compacta y muy útil:

<input type="checkbox" value="1" name="test"


{{ checked == true ? 'checked="checked"' : '' }}/>

Ejemplo 21: condición ternaria en Twig

Por supuesto se pueden anidar ambas.

Iteraciones
Para iterar sobre una lista de elementos podemos utilizar el bucle for.

46
5. La vista: Twig

{# ejemplo de un bucle for para un array #}


{% set lista = ['casa','coche'] %}

Existen estos elementos en la lista


<ul>
{% for item in lista %}
<li>{{ lista }}</li>
{% enfor %}
</ul>

{# si la lista es un array asociativo #}


{% set lista = {'casa': 10000, 'coche':5000 } %}

Los valores de los objetos son:


<table>
<thead>
<th>objeto</th>
<th>valor</th>
</thead>
{% for objeto,valor in lista %}
<tr>
<td>{{ objeto }}</td>
<td>{{ valor }}</td>
</tr>
</table>

Ejemplo 22: ejemplo de un bucle for en Twig

Recuerda que el orden en el que se indican la variable que contiene el array y la


que va a recibir el elemento en cada iteración es justo el inverso que el foreach
de PHP: foreach $items as $item versus for item in items.

Y si puede que no existan elementos y queremos mostrar un mensaje diferen-


te, añade la cláusula {% else %}:

{% set lista = [] %}

<ul>
{% for item in lista %}
<li>{{ item }}</li>
{% else %}
<li>No hay elementos</li>
{% endfor %}
</ul>

Ejemplo 23: mensaje else en un bucle for en Twig

47
Programación PHP profesional con Slim, Paris y Twig

Funciones de Twig
Como ya te he mencionado antes, la página oficial de Twig es la mejor fuente
de ejemplos de las distintas funciones que ofrece. Funciones y filtros, que aun-
que parecido, no son lo mismo.

Voy a ponerte un par de ejemplos de cada uno de ellos.

En condiciones normales Twig escapa todo el contenido de las variables que


quieres mostrar, pero qué ocurre si el contenido de una variable es precisamente
HTML. Se nos mostrará con los caracteres escapados. En ese caso y siempre
que sepamos lo que estamos haciendo, podremos utilizar el filtro raw.

NOTA
Hasta ahora no he abordado el tema del estilo.
Se aconseja dejar un espacio entre las llaves y el interior, en cambio se
sugiere que el signo | que hace de tuberia (como en *nix) entre lo que se
va a mostrar y el filtro a aplicar esté pegado a ambos.

Así:
{% set content = 'Esta <strong>letra</strong> está enfatizada' %}
Este es el contenido de la variable content:
{{ content|raw }}

Ejemplo 24: filtro raw en Twig

La diferencia entre filtro y función es básicamente a nivel de declaración (vere-


mos más adelante cómo crear nuestros propios filtros y funciones para Twig),
y a nivel de uso: a una función hay que pasarle parámetros y un filtro toma el
parámetro de la tubería, además los filtros se pueden encadenar.

En el siguiente ejemplo, como no sé si content tiene valor, le pongo el filtro


default:

{{ content|default('<strong>Null</strong>')|raw }}

Ejemplo 25: filtros encadenados en Twig

Ahora verás que las funciones son más sencillas porque son como las funcio-
nes de PHP:
{% set foo = {'foo':1, 'bar':'2'} %}

48
5. La vista: Twig

{{ attribute(foo,'bar') }}
mostraría un 2

Extender Twig con funciones propias


Para extender Twig con funciones propias es suficiente con incluirlas en el ar-
chivo lib/TwigViewSlim.php, mira cómo está declarada la función urlFor:
// ...
class TwigViewSlim extends Twig
{
private function addFunctions(\Twig_Environment $twigEnvironment)
{
// ...
$twigEnvironment->addFunction(
'urlFor',
new Twig_Function_Function(
'\Router\SlimExt::getInstance()->urlFor'
)
);
// ...
}
// ...
}

Sencillo, ¿verdad? Solo tenemos que tener una función global o un método
estático de cualquiera de nuestras clases y mediante addFunction vinculamos
la función Twig con el método global o estático que la va a resolver. Fíjate que
hay que indicar la ruta(namespace) correctamente.

Una cuestión a tener en cuenta: si el resultado de la función devuelve código


con marcado HTML y no queremos tener que añadir el filtro raw podemos indi-
carle a Twig que el resultado de la función genera código HTML y que este es
seguro, simplemente añade un segundo parámetro así:
$twigEnvironment->addFunction('_',
new Twig_Function_Function('_',
array('is_safe' =>array('html'))
)
);

Macros
Hemos visto cómo extender Twig con funciones y filtros propios, pero en oca-
siones no es necesario llegar hasta ese extremo, o el resultado que se preten-
de obtener de la función tiene tanto HTML que realizar una función para ese
menester choca un poco con la idea de la separación de capas. En todo caso,

49
Programación PHP profesional con Slim, Paris y Twig

sea por el motivo que sea, debes conocer este elemento que otorga a twig una
potencia adicional (más si cabe).

Cuando te encuentras en una o más plantillas que un segmento de código se


repite una y otra vez, con alguna variante, como por ejemplo, el nombre, altura,
etc. de un determinado elemento, podemos extrapolar ese bloque a una macro
e invocarlo cuando nos sea preciso.

En principio aunque en la documentación oficial se comenta la posibilidad de in-


cluir la macro dentro de la misma plantilla, considero que es un ejemplo didáctico
porque en la realidad si quieres una macro yo entiendo que es para compartirla
entre plantillas, así que yo obviaré esa parte en mi explicación.

Como ejemplo para ilustrar el uso de las macros vamos a partir de la creación de
un formulario en el cual tenemos un montón de labels seguidos de su correspon-
diente input, una tarea repetitiva y que además da poco juego a la hora de alterar
el comportamiento general del conjunto, como por ejemplo podría ser el hecho de
añadir una clase nueva a todos los input, o solo a aquellos que sean requeridos.

Veamos primero el ejemplo en bruto, sin emplear macros:

{# esto simula un objeto pasado a la plantilla #}


{% set data = {
'nombre': 'Joseluis',
'apellido': 'Laso',
'aficiones': 'Informática',
'libro': 'Programación PHP profesional con Slim,Paris y Twig'
}
%}

<ul>
<li>
<label for="nombre">Nombre</label>
<input type="text" name="nombre" value="{{ data.nombre }}"/>
</li>
<li>
<label for="apellido">Apellido</label>
<input type="text" name="apellido" value="{{ data.apellido }}"/>
</li>
<li>
<label for="aficiones">Aficiones</label>
<input type="text" name="aficiones" value="{{ data.aficiones }}"/>
</li>
<li>
<label for="libros">Libros</label>

50
5. La vista: Twig

<input type="text" name="libro" value="{{ data.libro }}"/>


</li>
</ul>

Ejemplo 26: formulario en Twig sin usar macros

Y ahora veamos lo que podemos hacer con una macro.

Por un lado tendríamos un archivo macros.html.twig en donde vamos a empe-


zar a guardar nuestras macros.

{% macro drawField(type, name, value) %}


<label for="{{ name }}">{{ name|capitalize }}</label>
<input type="{{ type }}" name="{{ name }}" value="{{ value }}"/>
{% endmacro %}

Ejemplo 27: macro drawField en Twig

Y por otro lado la plantilla twig que va a hacer uso de esa macro, fíjate sobre
todo en cómo se importan y que luego su uso es como el de una función con-
vencional.

{% import 'macros.html.twig' as macros %}

{% set data = {
'nombre': 'Joseluis',
'apellido': 'Laso',
'aficiones': 'Informática',
'libro': 'Programación PHP profesional con Slim,Paris y Twig'
}
%}
<ul>
{% for name,value in data %}
<li>
{{ macros.drawField(name,'text',value) }}
</li>
{% endfor %}
</ul>

Ejemplo 28: formulario en Twig empleando una macro

¿Cómo te has quedado? Chulo, ¿verdad? Hemos aunado simplicidad de uso


y facilidad de reutilización, por no hablar de un sencillo mantenimiento en caso
de querer alterar el comportamiento.

51
Programación PHP profesional con Slim, Paris y Twig

Resumen
Como corolario te muestro una lista de funciones y filtros que tiene Twig en
la actualidad2. No pretende ser un compendio exhaustivo, sino más bien una
referencia. En todo caso te aconsejo siempre que acudas a la documentación
oficial para ejemplos e información actualizada.

Filtro Comentario
Independientemente de si está activado el autoescape a nivel
de Twig podemos marcar un bloque para que escape (todo,
html, js, o falso).
autoescape-
endautoescape {% autoescape [|'html'|'js'|false] %}
Everything will be automatically
escaped in this block
{% endautoescape %}

Define un bloque susceptible de ser sobreescrito en los hijos


que extiendan de la plantilla donde está declarado, recomenda-
block-endblock ble poner en el endblock el nombre del bloque que cierra para
tenerlos casados, a menudo se cierran varios bloques de mane-
ra consecutiva (como los div en html).
Igual que {{ ... }} pero sin mostrar nada.
do
{% do 1 + 2 %}

Es un híbrido entre include y extends, de tal manera que incluye


embed-
otra plantilla que puede contener bloques y se pueden modifi-
endembed
car/extender.
Mecanismo de herencia de Twig. Extiende una plantilla. El pará-
extends metro es la plantilla padre.
{% extends "base.html.twig" %}

Utiliza un filtro para un bloque completo.


filter-endfilter {% filter raw %}
{{ 'hola y <em>adios</em>' }}
{% endfilter %}

Internamente ejecuta la función fl ush de PHP.


fl ush Esto obliga a mandar los datos al navegador aunque el servidor
no haya terminado su trabajo.

2
La actualidad se refiere a la versión que actualmente uso para My-simple-web, Twig
es un proyecto en expansión y continuamente se añaden nuevas funcionalidades.

52
5. La vista: Twig

Filtro Comentario
Igual que el bucle foreach de PHP pero con las variables inver-
tidas, {% for item in collection %}, además permite
declarar una sección para cuando no existen elementos sobre
los que iterar (else).
<select name="tallaje">
for-else-endfor
{% for talla in tallas %}
<option value="talla.id">{{ talla.name }}</option>
{% else %}
<option>No hay tallas para seleccionar</option>
{% endfor %}
</select>

Condicional.
{% if user is not null %}
if-else-endif {{ user.name }}
{% else %}
<a href="login">Inicia sesión</a>
{% endif %}

Importa de otra plantilla las macros declaradas y asigna un nombre


para acceder a ellas.
import
{% import "macros.html.twig" as macros %}
{{ macros.input("nombre","text") }}

Incrusta otra plantilla dentro de la actual, tal y como hace el


include en PHP.
include {% include "language-selector.html.twig" %}
Puede ser un array de twigs que carga el primero que exista en
el orden declarado.
Declaración de una macro.
{% macro input(field,type,value) %}
macro-endmacro <input type="{{ type }}" name="{{ field }}"
value="{{ value|default("") }}"/>
{% endmacro %}

Asigna un valor a una variable.


set
{% set iva_aplicable = 21 %}

Quita los espacios y las nuevas líneas entre el texto literal y las lla-
ves reservadas de Twig, para producir por ejemplo textos exactos.
{% spaceless %}
spaceless- Además de lo evidente, esta utilidad me ha resultado
endspaceless útil para formatear bien el código en el twig y no ge-
nerar espacios o saltos de línea en el HTML que pueden
resultar en un comportamiento inesperado.
{% endspaceless %}

53
Programación PHP profesional con Slim, Paris y Twig

Filtro Comentario
Extensión de la herencia que permite utilizar otra plantilla de un
use modo similiar a extends. Véase la documentación original en:
http://twig.sensiolabs.org/doc/tags/use.html
Muestra el contenido interior tal cual, sin interpretar.
{% verbatim %}
verbatim- {{ 'hola y adios' }}
endverbatim {% endverbatim %}
muestra:
{{ 'hola y adios' }}

Vamos a repasar ahora los filtros.

Filtro Comentario
Valor abosoluto.
{{ -234|abs }}
abs
muestra:
234
iterable|batch(n,[char])
Completa un array con elementos char para que el número de
elementos sea múltiplo de n:
{% set items = ['a', 'b', 'c'] %}
<table>
{% for row in items|batch(2, 'x') %}
<tr>
{% for column in row %}
batch <td>{{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
produce:
<table>
<tr><td>a</td><td>b</td></tr>
<tr><td>c</td><td>x</td></tr>
</table>

Pone la primera letra en mayúscula,


{{ 'buenos días'|capitalize }}
capitalize
muestra:
Buenos días

54
5. La vista: Twig

Filtro Comentario
cadena|convert_encoding(destino,origen)
Este filtro convierte una cadena desde un charset (el segundo
convert_encoding
parámetro) a otro charset (el primer parámetro):
{{ data|convert_encoding('UTF-8', 'iso-2022-jp') }}

Formatea la variable mediante el formato indicado entre parén-


tesis (con la misma sintaxis que la funcion date de PHP).
date {{ 'now'|date('d/m/Y H:i:s') }}
muestra:
14/02/2014 23:10:07

Si la variable o expresion anterior a la pipa(|) da resultado no


default definido o null se presenta el argumento de default.
{{ username|default('No name') }}

escape Escapa el contenido, es el filtro contrario a raw.


Devuelve el primer elemento de un array o una cadena.

{{ [1, 2, 3, 4]|first }} muestra 1


first
{{ { a: 1, b: 2, c: 3, d: 4 }|first }} muestra 1
{{ '1234'|first }} muestra 1

Implementa la función sprintf de php como filtro:


{% set foo = "my-foo" %}
format {{ "I like %s and %s."|format(foo, "bar") }}

muestra:
I like my-foo and bar

Igual que implode en PHP, une los elementos de la lista pasada


con el argumento de join como elemento de unión:
join {{ [1,2,3,4]|join('-') }}
muestra:
1-2-3-4

Implementa la función de php json_encode:


json_encode
{{ data|json_encode() }}

Este filtro devuelve las claves de un array asociativo.


keys {% for key in array|keys %}
...
{% endfor %}

55
Programación PHP profesional con Slim, Paris y Twig

Filtro Comentario
Devuelve el último elemento de un array o de una cadena.
last {{ [1,2]|last }} muestra 2
{{ '1234'|last }} muesta 4

Devuelve el número de elementos de un array.


length {{ [1,2]|length }}
muestra 2
Convierte a minúsculas:
{{ "Esto es una PRUEBA"|lower }}
lower
muestra:
esto es una prueba

Combina dos arrays:


merge {{ [1,2]|merge([3,4])|join(',') }}
muestra 1,2,3,4
nl2br Convierte los saltos de línea de texto (CRLF) a <br />
numero|number_format(decimales[,coma,miles])
Formatear la salida de un número, corrigiendo el número de
decimales presentados, la coma decimal y la coma de los miles.
{{ 1123.4|number_format(2) }}

number_format muestra:
1,123.40

{{ 9800.333|number_format(2, ',', '.') }}


muestra:
9.800,00

Hace que el contenido se muestre en bruto, sin escapar, con-


trario a escape.
raw
{{ "<small>" }} muestra &lt;small&gt;
{{ "<small>"|raw }} muestra <small>

Reemplaza unos caracteres por otros.


replace
{{ 'abcde'|replace('abc','123') }} muestra 123de

Invierte el orden un array.


reverse {{ [3,2,1]|reverse|join('-') }},
muestra 1-2-3

56
5. La vista: Twig

Filtro Comentario
El filtro slice extrae una secuencia de un string o un array.
{{ '12345'|slice(1, 2) }} muestra 23
slice {% for i in [1, 2, 3, 5]|slice(1, 2) %}
{# itera entre 2 y 3 #}
{% endfor %}

Ordena un array mediante la función php asort.


{% for user in users|sort %}
sort
...
{% endfor %}

Como el explode de php.


split
{% set array = '1,2,3,4'|split(',') %}

Implementación de la función de php strip_tags,


striptags
{{ some_html|striptags }}

Capitaliza la primera letra de cada palabra.


{{ 'mi artículo'|title }}
title
muestra:
Mi artículo

trim Elimina los espacios sobrantes (al principio y/o al final).


Convierte a mayúsculas.
{{ "poner en grande"|upper }}
upper
muestra:
PONER EN GRANDE

Codifica una URL.


url_encode
{{ 'http://prueba.net/mi artículo'|url_encode }}

Vamos a repasar ahora las funciones:

Función Comentario
Acceso a un array asociativo u objeto de manera programática.
{% set foo = {'foo':1, 'bar':'2'} %}
attribute {{ attribute(foo,'bar') }}
muestra 2

57
Programación PHP profesional con Slim, Paris y Twig

Función Comentario
Muestra el contenido del bloque indicado.
block
{{ block('body') }}

Devuelve el valor de una constante de php.


constant
{{ constant("ROOT_DIR") }}

Cycle alterna entre los elementos de un array:


{% set fruits = ['apple', 'orange' %}
cycle {% for i in 0..10 %}
{{ cycle(fruits, i) }}
{% endfor %}

Volcado de una variable tal y como lo haría var_dump en


dump
PHP.
Inserta tal cual el contenido de otra plantilla dentro de la ac-
include
tual, igual que el include de PHP.
Permite la inclusión de todo el contenido de un bloque para
parent extender la funcionalidad del bloque que se pretende sobre-
escribir, muy usado en los bloques stylesheets y javascripts
Genera un número aleatorio, si se le indica un parámetro lo
toma como máximo número a generar.
random {{ random() }}
o
{{ random(100) }}

Genera una lista entre los valores indicados.


range
{% for index in range(1,5) %}

Y para terminar veamos las palabras reservadas que intervienen en comproba-


ciones de condiciones.

Comprobación Comentario
Devuelve true si la variable está definida.
defined
{{ valor is defined ? valor : 'undefined' }}

Sería como hacer un módulo x en PHP.


divisibleby
{% if linea is divisibleby(3) %}
Devuelve true si la variable está vacía: cadena vacía o null.
empty
{% if valor is empty %} ... {% endif %}

58
5. La vista: Twig

Comprobación Comentario
Devuelve true si el valor es par.
even
{% if valor is even %} ... {% endif %}

iterable Devuelve true si la variable permite iterar sobre ella.


Devuelve true si la variable es null.
{% if valor is null %}
null
{% set valor = 0 %}
{% endif %}

Devuelve true si el valor a comprobar es impar.


odd
{% if valor is odd %} ... {% endif %}

Comprobación del triple igual ===,


sameas
{% if a is sameas(1) %}

Por último le toca el turno a los operadores.

Operador Comentario
Utilizado sobre todo en los bucles for y en comprobaciones de
si un elemento está contenido en una lista:
{% if 1 not in [1, 2, 3] %}
in ...
{% endif %}
{% for item in items %}
...
{% endfor %}

Utilizado para comprobar si un valor es par, impar, divisible,


is
true, false, null, etc.
matemáticos +, -, /, %, /, *, **

lógicos and, or, not, (), b-and, b-xor, b-or


comparaciones ==, !=, <, >, >=, <=, ===

otros .., |, ~, ., [], ?:

Conclusión
Este capítulo ha sido extenso pero productivo, espero que hayas asimilado
gran parte de lo expuesto y, si no, que hayas marcado estas páginas para vol-
ver a ellas cuando tengas que consultar algo sobre Twig.

59
Programación PHP profesional con Slim, Paris y Twig

Recuerda que vamos a ver en profundidad el uso práctico de lo expuesto en la


tercera parte de la obra.

60
Parte II: Herramientas
6 Composer
Contenido
» Introducción
» Repositorios privados
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Composer proporciona una manera sencilla de tener todos los requerimien-
tos de tu aplicación controlados mediante un sencillo archivo de texto .json.
Mediante su ejecutable se puede desplegar una completa aplicación en un
instante1. Como dice la documentación oficial de composer, es un gestor
de dependencias, no un gestor de paquetes, ya que no instala nada a nivel
global.

Al principio de esta obra te describí someramente los pasos para crear una apli-
cación php utilizando composer. Dando por sentado que al menos composer lo
tienes instalado (http://getcomposer.org), vamos a indagar un poco más en los
entresijos de esta estupenda herramienta.

Su funcionamiento básico presume que los paquetes están en un repositorio


git, más concretamente en un repositorio público, el cual es indexado por el si-
tio http://packagist.org. Si necesitas alguna utilidad, rutina o solo quieres revisar
código de otros programadores te recomiendo visitar su web, el buscador es
muy completo.

Enseguida comprobarás que es una especie de directorio, y que la multitud de


los paquetes residen realmente en un repositorio (packagist no lo es), normal-
mente github.com o bitbucket.org.

No es que no se puedan alojar los paquetes en otros sitios, de hecho composer


prevé esta posibilidad, pero el comportamiento básico presume lo que te he
descrito.

La utilización de los módulos que composer descarga e instala en la carpeta ven-


dor del proyecto donde se encuentre composer.json es sencillísima. Solo hemos
de añadir esta línea a nuestro archivo index.php o cualquier otro que tenga que
hacer uso de los módulos instalados: require 'vendor/autoload.php';

En este capítulo vamos a experimentar con composer haciendo algún ejemplo


simple para que veas su potencia. Como te dije en el capítulo en el que introduje
composer, yo he renombrado el archivo composer.phar, le he dado permisos de
ejecución y lo he movido a una ruta en el PATH, de tal manera que invocando
composer ya tengo acceso directamente a la herramienta. De otra manera ten-
dría que invocarlo así: php /ruta/del/composer.phar arguments.

Vamos a ver un ejemplo sencillo. Sin siquiera crear el archivo composer.json.


Esta es la instrucción: composer create-project slim/slim:

1
Un instante es una manera de hablar, la verdad es que no es una tarea inmediata.

64
6 Composer

> composer create-project slim/slim


Installing slim/slim (2.2.0)
- Installing slim/slim (2.2.0)
Loading from cache

Created project in ~/php_projects/slim


Loading composer repositories with package information
Installing dependencies (including require-dev)
Nothing to install or update
Generating autoload files

Este simple comando se ha encargado de buscar el proyecto slim/slim en pac-


kagist, y desplegarlo en la carpeta slim.

Veamos la estructura de carpetas que nos ha creado:


slim
├── Slim
│ ├── Exception
│ ├── Http
│ ├── Middleware
├── composer.json
├── index.php
├── phpunit.xml.dist
├── tests
│ ├── Http
│ ├── Middleware
│ └── templates
└── vendor
└── composer
Veamos qué fichero composer.json nos ha creado:
{
"name": "slim/slim",
"type": "library",
"description": "Slim Framework, a PHP micro framework",
"keywords": ["microframework","rest","router"],
"homepage": "http://github.com/codeguy/Slim",
"license": "MIT",
"authors": [
{
"name": "Josh Lockhart",
"email": "info@joshlockhart.com",
"homepage": "http://www.joshlockhart.com/"
}
],

65
Programación PHP profesional con Slim, Paris y Twig

"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": { "Slim": "." }
}
}

En realidad este archivo es el del proyecto slim, luego veremos cómo crear
nuestros propios composer.json. De nuevo quiero que le pierdas el miedo a
este tipo de documentos.

Por tanto no ha creado un proyecto como los que vamos a utilizar en este libro,
ya que en lugar de una aplicación con varios módulos nos ha creado una apli-
cación con un único módulo, no es que no le podamos añadir más pero va a re-
sultar más rápido empezar con un archivo composer.json propio e ir incluyendo
los módulos que vayamos precisando a medida que nuestro proyecto crezca.
Fíjate además que la carpeta Slim está en la raíz y no dentro de la carpeta ven-
dor, de alguna manera el proyecto es solo Slim.

¿Cómo se indican los paquetes?


Los paquetes que queremos incluir en nuestro proyecto se declaran dentro de la
cláusula require del archivo composer.json. Los nombres de los paquetes incluyen
el nombre del autor, barra y el nombre del módulo. Además podemos indicar la ver-
sión concreta que queremos instalar, o su mínimo de estabilidad. Veamos cómo:
{
"require":{
"slim/slim": "2.*"
}
}

Con “2.*” le estamos indicando que nos da igual qué versión siempre que su
número mayor sea 2, en todo caso se descargará la más reciente de las 2.x.

NOTA
Buenas prácticas: se recomienda incluir la carpeta vendor dentro de
.gitignore, ya que se puede recrear con el programa composer.

Install versus update


Update actualiza los paquetes a la versión más reciente que cumpla con los
requisitos indicados en composer.json, y se actualiza el archivo composer.lock

66
6 Composer

con las versiones reales instaladas. Install instala los paquetes que haya en el
archivo composer.json o a las versiones que indique el archivo composer.lock,
en caso de que exista (véase siguiente punto).

Composer.lock
Cuando composer termina de instalar escribe en el archivo composer.lock las
versiones exactas que ha descargado/instalado. Si el archivo existe, cuando se
ejecuta composer install se instalan exactamente las versiones indicadas en
él, por eso es conveniente seguir este archivo con git.

Repositorios privados
¿Qué ocurre si los paquetes que queremos utilizar no están en packagist? ¿Y
si además ni siquiera son públicos?

Este caso también está contemplado por composer, no necesitamos hacer nada
raro, podemos disfrutar de la potencia de composer aunque los paquetes no sean
públicos. Únicamente hace falta en el composer.json indicarle cuáles son los orí-
genes de los paquetes, y composer mirará ahí antes de ir a buscar a packagist.

{
"name": "jlaso/demo-composer1",
"description": "Para utilizar paquetes privados",
"repositories": [
{
"type": "vcs",
"url": "git@gitlab.com:jlaso/test-module1.git"
}
],
"require": {
"php": ">=5.3.3",
bla, bla, bla
"jlaso/test-module1": "*"
}
}

Como puedes ver es suficiente con la cláusula repositories.

Una cosa a tener en cuenta: el usuario que utiliza composer con esta configu-
ración ha de poder tener acceso al repositorio indicado, ya sea con clave SSH
o con usuario y contraseña, si no composer fallará.

Si el repositorio por ejemplo está en un servidor comercial, que solo tiene IP


(no hay un dominio apuntado) y además no se accede por ssh a través del

67
Programación PHP profesional con Slim, Paris y Twig

puerto convencional (22), la declaración sería así (para un puerto 1234, una IP
192.168.1.1, y un usuario user-ssh con permisos suficientes):

"repositories": [
{
"type": "vcs",
"url": "ssh://user-ssh@192.168.1.1:1234/var/git/test.git"
}
],

Actualizar el ejecutable composer


Periódicamente conviene actualizar el ejecutable, esto se puede hacer median-
te el comando composer self-update. Si transcurre cierto tiempo sin haber
actualizado el programa, cuando lo invoquemos nos recordará la conveniencia
de hacerlo.

Automatizar procesos en el momento de instalar o actualizar


Composer permite crear un apartado dentro de composer.json que declara los
comandos que han de ejecutarse cuando se empieza o finaliza el proceso de
instalación o actualización. De tal manera que podemos lanzar algún comando
que nos permita hacer tareas rutinarias en estos casos, como puede ser borrar
la caché.

Estas son las cláusulas que debemos utilizar para cada uno de los casos:

Clave Uso

pre-install-cmd antes de la ejecución de install

post-install-cmd después de la ejecución de install

pre-update-cmd antes de la ejecución de update

post-update-cmd después de la ejecución de update

Existen más cláusulas, que conviene que revises en la documentación oficial


para casos más específicos de uso.

Veamos cómo le indicaríamos a composer que queremos borrar la caché de


nuestro proyecto My-simple-web para los casos de terminar la instalación o la
actualización:

68
6 Composer

{
// ...
"scripts": {
"post-install-cmd": [
"rm -rf app/cache/*"
],
"post-update-cmd": [
"rm -rf app/cache/*"
]
},
// ....
}

NOTA
Esta carpeta (app/cache) no se debe seguir por el git, quizás
te preguntes entonces por qué es necesario borrarla. La expli-
cación es bien sencilla: la primera vez que nos bajemos el pro-
yecto con git clone la carpeta estará vacía pero en las diferen-
tes ejecuciones de la aplicación se van generando diferentes
archivos en app/cache que permiten su ejecución optimizada.
Por tanto durante una actualización o instalación de paquetes
querremos que la aplicación esté como cuando la clonamos
la primera vez.

Esto nos permite que al desplegar una nueva versión en el servidor que requie-
re de una actualización o instalación de paquetes borre directamente la caché
de la aplicación.

Pero no hablemos de desplegar con git pues los más puristas no lo consideran
un método de despliegue; esto seguramente daría para otro libro.

Conclusión
Espero haberte transmitido la importancia de esta aplicación en lo que a de-
sarrollo web con PHP se refiere. Encontrarás en él un valioso aliado y te
permitirá sobre todo mantener al día tus proyectos y unificar los criterios a la
hora de versiones instaladas de entre las que hayas probado. Si un paquete
que utilizas al actualizarlo deja inestable tu web siempre puedes volver a la
versión anterior y bloquear tu composer.json en esa versión estable conocida
para tu proyecto.

69
Programación PHP profesional con Slim, Paris y Twig

70
7 Git como SCM
Contenido
» Introducción teórica
» Explicación práctica
» Instalación
» Comandos básicos: init y clone
» Funcionamiento en el día a día
» Ejemplos prácticos de uso
» Buenas prácticas
» Comandos más usados de git
» Rizando el rizo
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción teórica
SCM es un sistema de control de versiones para los archivos fuentes (source
control management).

Hay diferentes SCM, entre los que destacan git y subversion. Aunque el fun-
cionamiento interno es diferente entre uno y otro, comparten la misma idea: un
repositorio es un lugar local o remoto donde se almacenan los archivos fuentes,
de tal manera que podemos hacer un seguimiento de la versión de cada archi-
vo, volver a puntos anteriores, aplicar parches, y un largo etcétera.

En este libro vamos a ver solo el caso de git, así que a partir de aquí nos cen-
traremos en este SCM. Las mayores diferencias están, por supuesto, en que
los comandos son diferentes, pero sobre todo en el tratamiento que se hace de
las ramas y de la centralización o distribución de repositorios.

Git es un SCM distribuido, se dice esto porque cada uno de los distintos repo-
sitorios que luego veremos cómo crear comparten en principio la misma infor-
mación, pudiendo hacer las veces de repositorio central cualquiera de ellos.

Para que un SCM sea efectivo debe proveer los mecanismos oportunos para
sincronizar la distinta información entre los diferentes repositorios.

Esto no se hace de manera automática y requiere normalmente la intervención


humana para llevar a cabo esa sincronización.

72
7. Git como SCM

Los ejemplos que pondré en este capítulo van a suponer la siguiente configu-
ración:
- Un repositorio central.
- Diferentes repositorios para cada uno de los desarrolladores en cada
una de sus máquinas.
- Una copia del repositorio en el servidor de desarrollo y otra en el de
producción.

Explicación práctica
Cuando un grupo de programadores colabora de manera conjunta en un pro-
yecto, existen a priori varias maneras de trabajar:
» directamente sobre una carpeta compartida en una red local o en una
carpeta en el cloud (como Dropbox o similares);
» subir y compartir directamente en el sitio FTP o soluciones parecidas;
» compartiendo los fuentes mediante un disco duro externo o dispositi-
vo removible;
» obligándose cada desarrollador a tocar solo archivos de un área de-
terminada y al final ponerlas todas en común;
» utilizar un SCM.

Después de haber trabajado con soluciones como las primeras (sí, las he pro-
bado todas a lo largo de mi vida profesional), uno se da cuenta enseguida de
las deficiencias de las mismas, entre las que podemos contar:
» concurrencia de accesos que provocan inconsistencia en los archivos;
» no se puede controlar el versionado;
» tampoco se puede volver atrás en caso de detectar un error en alguna
implementación;
» y, por supuesto, el número de colisiones y dolores de cabeza cuando
el número de programadores concurrentes aumenta es significativo.

Si unimos a esto el hecho de que posiblemente los trabajadores no están en


la misma oficina se producen unos retrasos considerables en la transmisión de
la información y la necesidad de efectuar un control rudimentario de acceso
sobre los archivos aumenta la complejidad de las tareas de una manera absur-
da, obligando permanentemente a pedir permiso para hacerse cargo de algún
archivo o pidiendo el dispositivo removible.

73
Programación PHP profesional con Slim, Paris y Twig

Git
Aunque no falto de complejidad, un sistema de control de versiones, una vez
aprendido y aplicado de forma sistemática ofrece algo de lógica al sinfín de idas
y venidas de archivos, para los casos enumerados antes.

Como no quiero que te pierdas siguiendo el hilo de mis explicaciones, te pre-


sento un esquema muy sencillo en donde he plasmado los componentes im-
prescindibles que entran en juego a la hora de tener un repositorio local y uno
remoto. A continuación te iré aclarando cada uno de los distintos componentes.

En primer lugar tenemos desglosadas las partes en las que git organiza inter-
namente la información dentro del sistema de archivos. Es necesario aclarar
que cuando hagamos un listado de las carpetas y archivos del repositorio no
veremos nada de esto. Cada una de las partes identificadas de alguna manera
son los estados por los que pasan los archivos hasta llegar a estar consolida-
dos e integrados en el repositorio.

Vamos por partes: en primer lugar veamos el área de trabajo (working directory),
que no es otra cosa que el área en la que se agregan los archivos, se modifican
o se borran. El contenido de esta área puede visualizarse haciendo un git sta-
tus, que nos listará todos los elementos con cambios, nuevos o borrados1, de

1
Los archivos borrados aparecen al hacer git status si los archivos estaban integrados
en el repositorio, ya que si son nuevos y los borramos antes de hacer un git add para
git no han existido nunca.

74
7. Git como SCM

tal manera que no están integrados dentro del repositorio hasta que no se hace
un git add, que pasa los archivos al staging area, y por fín un git commit que
nos validará los cambios y los integrará en nuestro repositorio local.

Si al final nuestros cambios han de integrarse en el repositorio remoto, algo ha-


bitual, haremos primero un git pull para actualizar nuestro repositorio con los
cambios que haya en él y por fin un git push para elevar nuestros cambios al
remoto. Es obligatorio hacerlo en este orden para resolver los posibles confl ictos
localmente.

Entre tanto tenemos un recurso importante que podremos utilizar a nuestra


conveniencia en determinados casos: se trata del stash, un área con una utili-
dad parecida al portapapeles, que nos permite introducir todos los cambios en
él y dejar el repositorio en un estado limpio para, por ejemplo, crear una rama,
descargar una nueva, etc.

La órden que introduce los cambios es git stash y la que los recupera es
git stash pop si queremos que esos cambios desaparezcan del stash o git
stash apply si queremos recuperarlos y conservarlos en el stash.

El área de stash admite órdenes adicionales que permiten listar el contenido,


entre otras.

Veamos como git sigue o no los cambios en los archivos en el working direc-
tory.

Un archivo está seguido cuando se encuentra dentro del sistema de control de


versiones. Lo que implica que git observa y retiene todos los cambios de ese
archivo. Podemos hacer que git ignore (no siga) determinados archivos si agre-
gamos su patrón al archivo .gitignore. Esto es útil con archivos de configuración
que contienen datos personales, con archivos de prueba como imágenes o
contenidos no definitivos.

NOTA
Puede que encuentres útil la orden:
git rm --cached filename

Si quieres obligar a git a que ignore el archivo filename que hasta ahora
había sido seguido, ya que con sólo la mera introducción de su patrón en
.gitignore no conseguiras “deshacerte” de él. La orden que te comento bo-
rra ese archivo de la cache del working directory para que realmente borre
el rastro de ese archivo y se pueda ignorar de manera efectiva.

75
Programación PHP profesional con Slim, Paris y Twig

Los archivos que son seguidos pueden encontrarse en estos estados:


» unmodified o committed: el archivo se encuentra actualizado: no tiene
modificaciones locales pendientes de validar.
» modified: el archivo ha sido modificado y difiere de lo que hay en el
repositorio.
» staged: el archivo fue agregado al staging area y será incluido en el
próximo commit.

Instalación
La instalación de git está muy bien documentada tanto en la página del pro-
yecto como en la red. Por regla general es suficiente con utilizar el gestor de
paquetes del sistema operativo *nix, solo en casos muy aislados me ha tocado
instalar desde las fuentes, como en alguna versión 5.x de centos. Para los ca-
sos de sistemas operativos comerciales existen instaladores gráficos.
Si no tienes la suerte de trabajar en un sistema operativo basado en POSIX,
con una terminal completa y funcional, te tocará instalar extensiones de tipo
bash. Git incorpora en su instalador para Windows esta opción por defecto, si
tienes instalado algún terminal como putty lo puedes seguir utilizando, aunque
particularmente considero mejor la implementación bash-git porque tiene un
tratamiento más natural de las carpetas y archivos y te hace sentir casi como si
estuvieras delante de una terminal *nix.
Para el caso de MacOs, puedes decidirte por el instalador gráfico o bien por las
distribuciones basadas en macports o brew, en ambos casos la instalación es
muy sencilla y se resuelve mediante un brew install git o port install
git. En este caso el terminal de este sistema operativo viene por defecto con
todo el sabor *nix.
Para distribuciones basadas en Debian (Ubuntu incluida) nos será suficiente
con hacer sudo apt-get install git-core.
En el caso de las distribuciones basadas en Fedora (Centos incluido) podre-
mos intentar hacer sudo yum install git-core. En ocasiones aisladas me
he visto obligado a instalar desde las fuentes en algunas versiones de Centos,
como he comentado más arriba.
No tengo experiencia en otras distribuciones de Linux ni otros sistemas opera-
tivos, pero la documentación por la red sobre la instalación de git es abundante
y no tendrás problemas en conseguirlo.
Las cuestiones relacionadas con Linux te aprovecharán tanto si tienes la suerte de
trabajar directamente en este sistema operativo como si tienes la suerte de tener

76
7. Git como SCM

un servidor propio, que de seguro estará basado en alguna distribución de las men-
cionadas. Como recomendación personal, si aún no te has decidido por ninguna
en concreto, es Centos para el servidor y Ubuntu para el ordenador de trabajo.

Ten en cuenta en todo caso que por regla general las comunicaciones del re-
positorio local al remoto y viceversa se hacen por ssh, con lo cual tendrás que
tomar las medidas oportunas para crear las claves en tu máquina (si no lo has
hecho ya) y autorizar esa clave2 en el servidor para evitar estar continuamente
introduciendo contraseñas.

Veamos el caso concreto para crear esas claves


Abriremos un terminal y comprobaremos que no tenemos ya las claves creadas
en la carpeta ~/.ssh, los archivos que tienes que comprobar son id_rsa.pub e
id_rsa. Si no existen ejecuta la orden ssh-keygen para crearlos.

Para que la comunicación ssh funcione de manera automática tendremos que


copiar el contenido de nuestro ~/.ssh/id_rsa.pub en una línea nueva del archivo
~/.ssh/authorized_keys del servidor, en este caso ~ en el servidor se refiere a
la carpeta home del usuario que utilicemos para conectarnos, que no tiene por
qué coincidir con el usuario que usamos en nuestra máquina local.

Para clarificar esto un poco imáginate que el usuario local que utilizo es jlaso,
entonces cuando me refiero a este archivo en mi máquina: ~/.ssh/id_rsa.pub
lo estoy haciendo a /home/jlaso/.ssh/id_rsa.pub, mientras que si el usuario que
utilizo para conectarme con el servidor se llama gituser el archivo ~/.ssh/autho-
rized_keys en el servidor lo encontraré en /home/gituser/.ssh/authorized_keys.

Comandos básicos: init y clone


Para poder empezar a utilizar git después de instalado, tenemos dos opciones:
clonar un repositorio remoto en nuestro equipo o inicializar un repositorio con o
sin contenido previo.

Si nuestro caso es empezar a trabajar con un proyecto que ya existe en un


repositorio git procederemos de la siguiente forma:

cd ~/mis_proyectos_php
git clone ruta_del_repo_remoto:/nombre_del_repo carpeta_local_opcional
cd carpeta_local_opcional

2
Me refiero a registrarla en el archivo ~/.ssh/authorized_keys.

77
Programación PHP profesional con Slim, Paris y Twig

Si por el contrario tenemos un proyecto en nuestro equipo que aún no está


versionado en un repositorio git procederemos así:

cd ~/mis_proyectos_php/proyecto_1
git init

En ambos casos tendremos un repositorio funcional, con la única salvedad de


que en el caso del clone nuestro repositorio local está «conectado» con el re-
positorio remoto de donde ha partido. Mientras que en el caso del init nuestro
repositorio es autónomo, de momento.

Lo normal es utilizar un repositorio remoto que permita compartir los fuentes con
el grupo de programación. Solo tendremos entonces que declarar un origin en
nuestro repositorio recién creado, mediante el comando git remote add origin
ruta_del_servidor_remoto:/nombre_repo.

Funcionamiento en el día a día


El proceso de trabajo diario con las fuentes de un repositorio git incluyen la
creación de ramas temporales, añadir archivos, validarlos, fusionar ramas, bo-
rrarlas, hacer releases.

Cuando vamos a empezar una nueva funcionalidad crearemos una rama me-
diante la instrucción git branch -b feature/nueva-funcionalidad.

Iremos creando archivos y modificando los que ya tenemos mediante nuestro


programa de edición de textos favorito. Cuando terminemos con la sesión de
trabajo agregaremos los archivos al repositorio lanzando git add -A. Tras lo
cual validaremos los cambios mediante git commit -e, introduciendo un texto
explicativo de la sesión de trabajo, es importante ser escueto y explicativo en
este texto pues luego nos permitirá hacer búsquedas por el mismo.

Para que nuestra rama y/o cambios estén accesibles a los demás programa-
dores o solo para que esté a buen recaudo (como copia) subiremos nuestros
cambios al repositorio central. Para ello actualizaremos primero nuestra rama
haciendo git pull origin feature/nueva-funcionalidad, y acto segui-
do actualizaremos el repositorio central con nuestros cambios mediante git
push origin feature/nueva-funcionalidad, a menos que se hayan produ-
cido confl ictos que tengamos que subsanar. Vamos a ver un diagrama de fl ujo
para todo lo que hemos enumerado.

Para los ejemplos siguientes se supone que tenemos un repositorio local en


nuestro equipo que o bien hemos clonado con el contenido de un repositorio

78
7. Git como SCM

remoto (git clone) o bien hemos inicializado desde cero (git init)3 . El resul-
tado de ambos es un repositorio funcional que nos permite ver el ciclo completo
de los cambios que se han de actualizar entre nuestro repositorio remoto y local.

Vamos a ver el ciclo completo de un cambio:

- En primer lugar nos situamos dentro de la carpeta del repositorio.

- Para ver en qué estado están los archivos (modificados, borrados, re-
nombrados o cambiados de sitio):
» git status

- Para agregar los archivos al staging area:


» git add –A

- Para validar los cambios añadidos en el paso anterior y que formen


parte del repositorio local:
» git commit –e (se abre un editor y se introduce el texto del
commit)

Hasta aquí para mantener un repositorio local. Para sincronizar los cambios re-
motos y los nuestros haremos git pull remote rama y git push remote
rama, siendo rama el nombre de la rama con la que estemos trabajando y
remote el nombre que tenga el repositorio remoto, habitualmente origin.

Hay muchos servidores en Internet para poder alojar nuestro repositorio: bit-
bucket.org (gratuito para grupos de hasta 5 personas )4, github.com (gratuito
para proyectos open-source), gitlab.com (gratuito) o un servidor propio.

Uso de ramas
Una vez se empieza a trabajar con git se ve necesario el uso de algún tipo de
instrumento que permita ir más allá de la simple incorporación de archivos al
repositorio.

Se necesita una manera de controlar la incorporación de nuevas funcionalidades


de manera sencilla, al mismo tiempo que se hace obligatorio el mantenimiento de
versiones estables del proyecto para poderlas mantener en producción.

3
En el caso de inicializar un repositorio desde cero con git init si se quieren subir cam-
bios a un repositorio remoto hay que añadir la ruta de este repositorio mediante git
remote add remote-name url
4
Estos datos son a la fecha de publicación de este libro, en ningún caso el autor ga-
rantiza la gratuidad de este servicio ya que es ajeno al mismo.

79
Programación PHP profesional con Slim, Paris y Twig

Ejemplos prácticos de uso


Nuestro propio servidor git
Vamos a ver el caso de un repositorio en un servidor propio, ya que para el
caso de servidores comerciales suele estar bien documentado en el propio ser-
vidor, en todo caso cada uno puede contar con determinadas particularidades.

Establezcamos por convenio que vamos a situar los repositorios en /var/git.


Crearemos el repositorio mediante:

git init --bare /var/git/repo-demo.git

y daremos los permisos adecuados a la carpeta según el usuario que utilice-


mos para conectarnos, normalmente por ssh.

El modificador --bare le indica a git que se trata de un repositorio maestro y


que en él no vamos a hacer directamente operaciones de git sino que va a ser
accedido de manera remota por nuestros repositorios locales.

Configurar el origen remoto en el repositorio de nuestro equipo, este paso es


común para cualquiera que sea el servidor de git: privado o comercial. Lo que
variará será la dirección URL. Vamos a suponer que nos conectamos a nuestro
servidor remoto por medio de ssh (recomendable), y que ya tenemos el reposi-
torio local operativo (ver git clone o git init) , entonces haremos:

git remote add origin usuario@ip-server:/var/git/repo-demo.git

Esta acción le ha indicado a git que cuando referenciemos el repositorio remo-


to origin en realidad nos referimos al repositorio que hay en la carpeta /var/git/
repo-demo.git que hay en el servidor ip-server y al cual nos vamos a conectar
por ssh utilizando el usuario indicado. Como es lógico si no hemos configurado
la pareja de claves privadas/públicas, en cada operación relacionada con origin
se nos pedirá la contraseña del usuario indicado en el server.

Para sincronizar una rama en nuestro repositorio local y el remoto, siempre


procederemos de la misma manera:

git pull origin rama


git push origin rama

Ten en cuenta que pull actualiza nuestro repositorio local con los últimos cambios
del remoto y push actualiza el repositorio remoto con los cambios que tengamos

80
7. Git como SCM

nuevos en nuestro local. Es obligatorio siempre hacerlo en este orden. ¿La lógi-
ca de esto? Si se producen confl ictos estos quedan en nuestro repositorio local
hasta que los subsanemos (CONFLICT).

El procedimiento de arreglo de confl ictos no es inmediato. Por ello voy a tratar


de ilustrarte el proceso utilizando un ejemplo real.

Un programador modifica un archivo prueba.php en la línea 1 y un compañero


suyo modifica el mismo archivo en la línea 50. Cuando git ve que las modifi-
caciones realizadas por dos usuarios diferentes en el mismo archivo no rompe
con el criterio de consistencia de versiones, hace una fusión automática de
ambos cambios y ninguno de los dos se entera de esto, salvo por el hecho de
que ambos verán tanto su modificación como la que ha hecho el compañero.

El caso es que ahora van a modificar los dos el mismo archivo en la misma lí-
nea, y con contenidos diferentes. El primer usuario en subir el cambio no tendrá
ningun inconveniente. El segundo cuando haga un pull de la rama en cuestión
será informado por git de que una modificación realizada por él mismo choca con
una modificación realizada por otro usuario en el mismo archivo, por lo que será
requerido por git para modificar él el archivo y validar los cambios. Para que esto
no sea una tarea ardua y penosa, git informa tanto de los archivos en confl icto al
hacer el pull, como indicando con marcas dentro del archivo lo que contiene la
rama remota y lo que tiene nuestra rama local, mediante un marcado especial.

Veamos un ejemplo a nivel práctico.

Abrimos una terminal del sistema y nos situamos en una carpeta temporal para
luego poder desechar el contenido de la misma:

> cd ~
> mkdir temp
> cd temp
> git init --bare confl ict-test
> git clone confl ict-test confl ict-test-1
> git clone confl ict-test confl ict-test-2

Hasta aquí hemos creado un repositorio maestro vacio y hemos clonado ese
mismo repositorio dos veces para simular la interacción de dos usuarios dife-
rentes.

Ahora vamos a hacer la primera prueba, creando un archivo prueba.php en


uno de ellos, subiendo los cambios al repositorio central y descargandolos en
el otro, para partir de un archivo igual en ambos usuarios.

81
Programación PHP profesional con Slim, Paris y Twig

> cd confl ict-test-1


> nano prueba.php

Y copiamos este texto:


<?php

// ejemplo para ilustrar los confl ictos en GIT y su resolucion

print "este es un sencillo ejemplo que permite ilustrar los conlfictos


en git" . PHP_EOL;

$a = 1;
$b = 2;

print "la suma de a y b es " . ($a + $b) . PHP_EOL;

print "este es un ejemplo muy sencillo" . PHP_EOL;

Ahora lo dicho: add, commit, y pull+push (aunque sabemos que es el primero


y no haría falta el pull prefiero que te conciencies de manera automática y que
asocies que antes de un push va un pull):

> git add -A


> git commit -e

(Se abre el editor de textos del sistema)


- Este es mi primer commit

(si es nano, ctrl-X y guardar // si es VI, esc, :wq)


[master (root-commit) 0c45c85] - Este es mi primer commit
1 file changed, 15 insertions(+)
create mode 100644 prueba.php
> git pull origin master
fatal: Couldn’t find remote ref master
fatal: The remote end hung up unexpectedly

(No te preocupes por este error, indica solamente que la rama no existe en el
repositorio, recuerda que está completamente vacío).

> git push origin master


Counting objects: 3, done.
Delta compression using up to 4 threads.

82
7. Git como SCM

Compressing objects: 100% (2/2), done.


Writing objects: 100% (3/3), 395 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To ~/temp/confl ict-test
* [new branch] master -> master

Nos cambiamos de carpeta y «bajamos» los cambios:

> cd ../confl ict-test-2


> git pull origin master
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ~/temp/confl ict-test
* branch master -> FETCH_HEAD

Y ahora editamos el archivo de nuevo:

> nano prueba.php

Introducimos el siguiente cambio en el primer print:


print "(CAMBIO 1)este es un sencillo ejemplo que permite ilustrar los
conlfictos en git" . PHP_EOL;

Y subimos ese cambio procediendo igual que antes (simplifico un poco la salida
del terminal):

> git add -A


> git commit -e
- Prueba de cambio 1
> git pull origin master
> git push origin master

Nos cambiamos de carpeta de nuevo:

cd ../confl ict-test-1

Pero esta vez no hacemos el pull, recuerda que estamos simulando dos usua-
rios que trabajan a la vez sobre el mismo archivo.

83
Programación PHP profesional con Slim, Paris y Twig

> nano prueba.php

Y en el último print hacemos esta modificación:


print "(CAMBIO 2) este es un ejemplo muy sencillo" . PHP_EOL;

Y subimos ese cambio:

> git add -A


> git commit -e
- Prueba de cambio 2
[master caa2162] - Prueba de cambio 2
1 file changed, 1 insertion(+), 1 deletion(-)
> git pull origin master

Ahora se nos ha debido de abrir el editor del sistema con un contenido parecido
a este:
Merge branch 'master' of ~/confl ict-test

# Please enter a commit message to explain why this merge is necessary,


# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

A lo que deberemos aceptar grabando el archivo y vemos la salida del coman-


do por el terminal:

remote: Counting objects: 5, done.


remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ~/temp/confl ict-test
0c45c85..72ccf86 master -> origin/master
Auto-merging prueba.php
Merge made by the 'recursive' strategy.
prueba.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Si te fijas, git nos ha informado de que ha hecho la fusión utilizando una estra-
tegia recursiva y que no se ha hecho necesaria la intervención humana.

84
7. Git como SCM

> git push origin master


Counting objects: 10, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 629 bytes, done.
Total 6 (delta 2), reused 0 (delta 0)
To ~/temp/confl ict-test
72ccf86..bb39d81 master -> master

Ahora ya tenemos el cambio 2 subido al repositorio central. Vamos a volver a la otra


carpeta y nos sincronizamos para hacer ahora el cambio que origine el confl icto.

> cd ../confl ict-test-2


> git pull origin master
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From ~/temp/confl ict-test
* branch master -> FETCH_HEAD
Updating 72ccf86..bb39d81
Fast-forward
prueba.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Editamos de nuevo el archivo e introducimos el siguiente cambio en el segundo


print:
print "(CAMBIO 3 del USUARIO 1) la suma de a y b es " . ($a + $b) . PHP_EOL;

Y como antes add, commit, pull y push, no te presento el terminal pues creo
que estos pasos los tendrás ya claros.

Nos cambiamos a la carpeta del otro usuario y editamos igualmente el archivo


en el mismo sitio (fíjate que nuevamente no hacemos pull aún para simular la
situación de confl icto, pues si hacemos pull y luego editamos no ocurrirá).

> cd ../conclict-test-1
> nano prueba.php

Y editamos la misma línea con este contenido:

85
Programación PHP profesional con Slim, Paris y Twig

print "(CAMBIO 4 del OTRO USUARIO) la suma de a y b es " . ($a + $b) . PHP_EOL;

Y ahora seguimos los pasos:

> git add -A


> git commit -e
[master 2b66cbe] - Prueba del cambio 4
1 file changed, 1 insertion(+), 1 deletion(-)
> git pull origin master
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ~/temp/confl ict-test
bb39d81..0b47af8 master -> origin/master
Auto-merging prueba.php
CONFLICT (content): Merge confl ict in prueba.php
Automatic merge failed; fix confl icts and then commit the result.

Como puedes apreciar, git no ha sido capaz de mezclar los cambios pues se
refieren a la misma línea. Por tanto nos lo indica en esta salida, diciéndonos
cuál o cuáles son los archivos que tenemos que revisar. En nuestro caso, por
ser sencillo, solo uno pero podrían ser varios. Es importante no perder de vista
esta salida de pantalla para poder corregir todos los confl ictos.

Editemos el archivo prueba.php y veamos su contenido íntegro:


<?php

// ejemplo para ilustrar los confl ictos en GIT y su resolucion

print "(CAMBIO 1)este es un sencillo ejemplo que permite ilustrar los


conlfictos en git" . PHP_EOL;

$a = 1;
$b = 2;

<<<<<<< HEAD
print "(CAMBIO 4 del OTRO USUARIO) la suma de a y b es " . ($a + $b) . PHP_EOL;
=======
print "(CAMBIO 3 del USUARIO 1) la suma de a y b es " . ($a + $b) . PHP_EOL;
>>>>>>> 0b47af8013943e524ed669d6b1fdbb0f85c3487c

print "(CAMBIO 2) este es un ejemplo muy sencillo" . PHP_EOL;

86
7. Git como SCM

Lo que te decía, git nos ha marcado (lo he subrayado para guiarte) dónde ha en-
contrado los confl ictos para que nosotros mismos decidamos qué se queda en
el archivo definitivo, si nuestro cambio o el que subió el compañero. En muchas
ocasiones, estas decisiones son evidentes y en otras no tan obvias. A menudo nos
tocará hablar con el otro usuario y ver cuáles son los motivos para esa diferencia
y saber así con qué quedarnos. Hasta que no se resuelvan los confl ictos no po-
dremos hacer un push e integrar todos nuestros cambios en el repositorio central.

Lo que hay entre <<<<<<< HEAD y ======= es el contenido de nuestro reposi-


torio local y lo que hay entre ======= y >>>>>>> hash-commit es lo que existe
en el repositorio central en la misma o mismas líneas.

¿Ves ahora por qué es necesario primero el pull y luego el push? Espero que sí.

Lo que vamos a hacer en este caso, ya que es un mero ejemplo pedagógico,


es quedarnos con el cambio del compañero, borrando todas las líneas que ha
añadido git para ayudarnos y despreciando nuestra línea en favor de la del otro
usuario. Ten cuidado en este punto, pues no necesariamente tiene que haber
un confl icto por archivo; yo suelo buscar la cadena ====== ayudándome del
editor para localizarlos todos.

Una vez hechos esos cambios grabamos y tendremos que hacer de nuevo todo
el proceso:

> git add -A


> git commit -e

Que nos abrirá el editor con un texto predefinido.


Merge branch 'master' of ~/temp/confl ict-test

Confl icts:
prueba.php
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
#
# Please enter the commit message for your changes.
Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch and 'origin/master' have diverged,
# and have 1 and 1 different commit each, respectively.
#

87
Programación PHP profesional con Slim, Paris y Twig

# All confl icts fixed but you are still merging.


# (use "git commit" to conclude merge)
#
# Changes to be committed:
#
# modified: prueba.php
#

[master c80a6ce] - Merge branch ‘master’ of ~/temp/confl ict-test

> git pull origin master


From ~/confl ict-test
* branch master -> FETCH_HEAD
Already up-to-date.

> git push origin master


Counting objects: 8, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 559 bytes, done.
Total 4 (delta 1), reused 0 (delta 0)
To ~/temp/confl ict-test
0b47af8..c80a6ce master -> master

Espero que con esta extensa demostración haya quedado explicado bien la
resolución de confl ictos de git.

Buenas prácticas
Hay una serie de convenciones que es recomendable seguir.

88
7. Git como SCM

Utilizaremos cinco ramas y nunca haremos commit directamente sobre master


ni develop, la rama master se usará para producción y solamente incorporare-
mos a ella aquello que esté probado y funcionando correctamente, así como
los hotfixes que arreglan bugs. Mantendremos una rama develop donde se
incorporarán el grueso de las funcionalidades.

Para cada una de las nuevas funcionalidades que tengamos que implementar
crearemos una rama que partirá de develop y terminará en develop, estas ramas
vivirán únicamente durante el proceso de desarrollo de dicha funcionalidad, bo-
rrándose inmediatamente una vez se haya integrado la rama con la de develop.

A menudo la rama develop se utiliza como fase previa a integrar en producción


(master) utilizando un servidor diferente (conocido como pre-producción) que
permite testar todas las nuevas funcionalidades antes de pasarlas a la línea de
producción.

Las ramas release sirven para unir la rama develop con la master en aquellos
momentos en los que se decida integrar una serie de funcionalidades en pro-
ducción; estas releases se etiquetarán correspondientemente (ver git tag).

Las ramas hotfix se crearán directamente desde la rama master y se integrarán


con ella una vez implementado el hotfix.

Se integrarán también en develop y en las ramas con funcionalidades operativas


en ese instante. Ten en cuenta que en cada rama git presenta un sistema de ar-
chivos diferente y los contenidos de los archivos pueden ser también diferentes,
por lo que un hotfix o release se debe propagar a todas las ramas en vigor.

Evidentemente esto son recomendaciones basadas en la experiencia y en las


convenciones usadas por otros programadores o grupos de programación. En
casos concretos y puntuales, como aquellos en los que el proyecto en el que
estés metido tenga un solo programador, tengo que reconocer que hacer todo
el git-fl ow resulta pesado y a menudo he caído en la tentación de usar única-
mente master y develop. Como siempre mi labor es enseñarte cómo hacerlo
correctamente y tú decidirás si te compensa el esfuerzo, aunque de antemano
ya te puedo adelantar que en grupos de trabajo se imponen las reglas que te
he enumerado o algunas muy similares.

Comandos más usados de git


La siguiente no pretende ser una lista exhaustiva y pormenorizada de todos los
comandos usados en git, sino más bien una referencia con los comandos que
vas a tener que usar más del 85% de las veces. Si necesitas algo especial, es
conveniente acudir a la documentación original del proyecto git.

89
Programación PHP profesional con Slim, Paris y Twig

Funcionalidad Comando
git clone url [carpeta]
Clonar un repositorio en una
carpeta local si no se indica carpeta se creará con un
nombre igual al del proyecto a clonar.
Listar las ramas que hay en el reposi- git branch
torio local y saber cuál
es la activa. aparece marcada con asterisco (*) la activa.

Listar las ramas que hay en el


git branch -r
repositorio remoto.
Actualizar el índice del repositorio
git fetch origin
local con el remoto.
git fetch origin feature/
Descargar de origin una rama x:feature/x
feature/x y llamarla en local feature/x,
y quedarnos en la rama en la que Los nombres de la rama remota y local
estábamos. pueden ser diferentes pero habitualmente
son iguales.
Ver los orígenes remotos que tene-
mos conectados a nuestro reposito- git remote -v
rio.
Agregar el repositorio remoto github.
git remote add github github.com/
com/usuario/repositorio.git a nuestro
usuario/repositorio
repositorio local con el nombre github.
Ver en qué estado se encuentran los
archivos del working dir en el reposi- git status
torio local.
Agregar todos los archivos modifica-
dos, borrados o añadidos al staging git add -A
area del repositorio local.
git commit -e (se abre editor para es-
Validar (commit) los archivos del sta- cribir mensaje del commit)
ging area. o:
git commit -m "mensaje del commit"
Cambiar la rama activa a master. git checkout master
Crear una rama desde la activa y mo-
git checkout -b rama-nueva
vernos a ella.
Crear una rama desde la activa y
git branch rama-nueva
quedarnos en la que estábamos.

90
7. Git como SCM

Funcionalidad Comando
Juntar los contenidos de la rama x con
la rama activa haciendo un merge no git merge --no-ff rama-x
fast-forward (recomendado)
Actualizar la rama activa del reposi-
torio local con los datos de la rama
«rama-remota» del repositorio remo- git pull origin rama-remota
to. Internamente se comporta como
un merge.

Combinar en la rama actual los con- git cherry-pick hash


tenidos del commit referenciado por * Este comando es muy útil y te lo explico
el hash. aparte.
Etiquetar la historia de git en el punto
git tag -a etiqueta -m nombre
actual, es útil para el versionado.
Hay otras muchos comandos que sin ser de uso cotidiano te pueden sacar de algún
apuro. Te recomiendo si te interesa el tema ponerte al día visitando el manual oficial
de git en la dirección: [http://git-scm.com/documentation]

Rizando el rizo
A continuación te voy a exponer un caso práctico para que veas cómo puede
ser útil alguna de las funciones avanzadas que hemos visto en la tabla anterior.

Mi compañero y yo estamos desarrollando funcionalidades diferentes de un


mismo proyecto, aunque en un momento determinado yo necesito uno de los
avances que él ha incorporado recientemente y que no ha integrado en una
rama común por no estar testado el conjunto.

Se trata de una función de utilidad genérica que me ahorraría un par de horas


de faena. En condiciones normales, me la pasaría por correo electrónico, la
incorporaría al archivo correspondiente y voilà, ya estaría. Pero como estamos
usado git y además no queremos tener confl ictos una vez juntemos las dos
funcionalidades vamos a hacer lo siguiente:

- Mi compañero hará un push de la rama en la que está trabajando5 git


push origin rama1
- En mi ordenador yo me descargaré la rama haciendo git fetch ori-
gin rama1:rama1 así que en rama1 estarán todos sus cambios.
5
Este paso se podría evitar si creáramos un origen en nuestro repositorio apuntando
a la máquina del compañero.

91
Programación PHP profesional con Slim, Paris y Twig

- Localizaré mediante git log el commit donde creó o actualizó ese


método que ahora necesito, anoto (mejor copiar al portapapeles) el
número hexadecimal largo que identifica el commit.
- Por fin, y estando en la rama de trabajo hago git cherry-pick hash,
siendo hash el número hexadecimal que hemos obtenido en el paso
anterior.
- Si no fuera a necesitar más la rama del compañero la borro del reposi-
torio haciendo git branch --delete rama1

Conclusión
Como en los demás capítulos de este modesto libro, no pretendo en este abar-
car todo el complejo sistema de git.

Me conformo, no obstante, con haber despertado tu curiosidad y que veas en


git un poderosísimo instrumento para organizar tu trabajo y un aliado si ya tra-
bajas en equipo.

No dejes de visitar la documentación oficial6 y si tienes ocasión échale un vis-


tazo a Pro GIT, un libro que no te dejará indiferente.

6
http://git-scm.com

92
Parte III: MVC a fondo
8 Controller: Slim
Contenido
» Introducción
» Cómo implementar una ruta simple
» Personalizar errores en Slim
» Ganchos (Hooks)
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Hasta ahora hemos visto teoría y ejemplos, y aunque estoy seguro de que lo
has entendido todo muy bien, en el fondo aún te preguntas qué es eso de mo-
delo, vista, controlador, etc.

Pues bien, aunque como te digo hemos ido viendo muchos ejemplos de código
no hemos visto nada realmente aprovechable directamente para la aplicación
que pretendemos desarrollar. En este momento, después de haber tenido una
idea somera de cada uno de los componentes vamos a empezar por fin a ver el
código de nuestra aplicación de ejemplo. Recuerda que lo que pretendemos ha-
cer es, usando el patrón MVC, una aplicación simple que presente unas cuantas
páginas al usuario, una para la home, una de contacto, en fin, lo básico.

De acuerdo, entonces, ¿qué es lo que pasa en realidad cuando llega una peti-
ción desde el navegador del usuario a nuestra aplicación web?

Para que todo funcione correctamente necesitamos que nuestro bootstrap ini-
cie todos los servicios necesarios, establezca cuáles son los patrones de las
URL y ceda el contol a Slim, que verá qué función en concreto es capaz de
atender la URL que está solicitando el usuario en esa petición, le cederá el
control a esa función y, si todo ha ido bien, a la vuelta (cuando termine el con-
trolador encargado) devolverá la respuesta al usuario.

96
8. Controller: Slim

Para que este planteamiento inicial funcione correctamente necesitamos que


un núcleo cargue todo lo que he mencionado, sea capaz de leer los patrones
de las rutas y tome la decisión adecuada. Slim es un framework muy ligero, no
por ello carente de potencia; es por ello que la implementación de las rutas es
muy sencilla. Veamos primero qué hace el cargador, núcleo o bootstrap:
» Inicializar una instancia de Slim.
» Configurarla adecuadamente.
» Levantar los servicios necesarios, como pueden ser el ORM, el motor
de Twig, etc.
» Leer todas las rutas.
» Ceder el control a Slim.

Vamos a repasar de nuevo el bootstrap de nuestra aplicación, por primera vez


lo vamos a ver completo en su extensión:
<?php
session_cache_limiter(false);
session_start();

date_default_timezone_set('Europe/Madrid');1

define ('ROOT_DIR', dirname(__DIR__));

require_once ROOT_DIR . '/vendor/autoload.php';

Twig_Autoloader::register();

// DB access
require_once ROOT_DIR . '/app/config/dbconfig.php';
ORM::configure('mysql:host='.DBHOST.';dbname='.DBNAME);
ORM::configure('username', DBUSER);
ORM::configure('password', DBPASS);

// Prepare view
\lib\TwigViewSlim::$twigOptions = array(
'charset' => 'utf-8',
'cache' => ROOT_DIR . '/app/cache',
'auto_reload' => true,
'strict_variables' => false,
'autoescape' => true
);

1
Es una buena práctica poner esta configuración en el PHP.INI si tienes acceso a él.
Desde la versión 5.3 de PHP es obligatorio indicar la zona horaria donde se ejecutan
los scripts de PHP, so pena de recibir una advertencia.

97
Programación PHP profesional con Slim, Paris y Twig

// Prepare app
$app = new \Router\SlimExt(array(
'templates.path' => ROOT_DIR . '/app/templates',
'log.level' => 4,
'log.enabled' => true,
'log.writer' => new \Slim\Extras\Log\DateTimeFileWriter(array(
'path' => ROOT_DIR . '/app/logs',
'name_format' => 'y-m-d'
)),
'view' => new \lib\TwigViewSlim(),
)
);

$languages = app\config\Config::getInstance()->getLanguageCodes();
\Slim\Route::setDefaultConditions(array(
'lang' => implode('|', $languages)
));

//@1:
// Define routes for controller
require_once ROOT_DIR . '/app/controller/autoload.php';

// Run app
$app->run();

Ejemplo 29: web/index.php o bootstrap

Todas las peticiones que haga el usuario a nuestra aplicación2 van a pasar por
aquí, este inicializador se encarga de recoger la petición y proceder en conse-
cuencia.

NOTA
Este comportamiento ocurre gracias al archivo .htaccess que te recomien-
do revisar. Todas las aplicaciones web que hago exponen solo la carpeta
web al público y tienen un .htaccess que hace que todas las peticiones
pasen a través de un solo archivo. También se puede configurar en el vir-
tualHost del servidor, pero requiere acceso a la máquina y conocimientos
más avanzados.
#web/.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

2
Excepción hecha de archivos concretos, como imágenes, css, js, etc.

98
8. Controller: Slim

@1:
Justo donde tenemos esta línea es donde se produce la lectura de las rutas:
// Define routes for controller
require_once __DIR__ . '/../app/controller/autoload.php';

Si vamos hasta ese archivo (app/controller/autoload.php):


<?php

// frontend routes
require_once __DIR__."/frontend/home.php";
require_once __DIR__."/frontend/articles.php";
require_once __DIR__."/frontend/contact.php";
require_once __DIR__.'/frontend/login.php';
require_once __DIR__.'/frontend/entity.php';

// backend routes
require_once __DIR__."/backend/admin.php";
require_once __DIR__."/backend/CRUD/list-entity.php";
require_once __DIR__."/backend/CRUD/edit-entity.php";
require_once __DIR__."/backend/CRUD/new-entity.php";
require_once __DIR__."/backend/crud.php";

Ejemplo 30: app/controller/autoload.php

Fácil, ¿verdad? Simplemente es un cargador de las distintas partes. Hay que


tratar de tener organizados los archivos, cada uno en su lugar. Si los archivos
crecen mucho de tamaño, son difíciles de mantener.

A continuación vamos a ver el contenido de alguno de ellos.

Cómo implementar una ruta simple


Veamos con detalle el comienzo del archivo app/controller/frontend/home.php:

<?php

/**
* ruta de la home (index)
*/
$app->get('/(:lang(/))', function ($lang = 'es') use ($app) {
$app->render('frontend/home/index.html.twig');
})->name('home.index');

Ejemplo 31: app/controller/frontend/home.php

99
Programación PHP profesional con Slim, Paris y Twig

NOTA
Recuerda que la disquisición entre ruta lógica y ruta física ya la hicimos
en la sección “Ruta lógica versus ruta física” en la página 20.

A primera vista estamos definiendo la ruta de la home. Por ser esta la primera
vamos a detenernos en cada parte de la definición.

$app es una variable (global no sería la palabra correcta, pues recuerda que
seguimos estando en el contexto de ejecución principal, es decir, no estamos
dentro de ninguna clase ni dentro de ninguna función) que representa la instan-
cia de Slim que se creó en el bootstrap.

Como instancia de Slim tiene unos determinados métodos, algunos de los cua-
les sirven para la definición de las rutas: get, post, delete, put, map,
via y name.

Todos ellos (menos name y via) aceptan un parámetro de cadena que repre-
senta el patrón de la ruta que pretendemos atender y la función (normalmente
una closure) que va a atender la petición, si la ruta concuerda.

El objeto que devuelve (una ruta) permite asignarle un nombre lógico mediante
name. Vamos a plantearlo así, reescribiendo el código, porque te veo un poco
perdido ;<)
<?php

/**
* ruta de la home (index)
*/
function home_route_controller($lang = 'es')
{
$app = \Router\SlimExt::getInstance();
$app->render('frontend/home/index.html.twig');
}
$home_route = $app->get('/(:lang(/))', 'home_route_controller');
$home_route->name('home.index');

Ejemplo 32: home_route_controller

Mejor, ¿verdad?
Bueno, pero estarás de acuerdo conmigo en que la otra escritura es más com-
pacta, además de que hace uso de las funciones anónimas (closures) y per-
mite la definición en línea. En todo caso, me he detenido en esta porque es la
primera, pero a lo largo de todo el texto veremos las rutas en forma compacta.

100
8. Controller: Slim

Volvamos un instante al primer parámetro de la definición '/(:lang(/))', los


contenidos que están entre paréntesis son opcionales y el patrón indica que la
ruta puede ser cualquiera de estas:
» /
» /lang
» /lang/
siendo lang además un valor variable que se va a pasar al controlador (los dos
puntos le confieren ese comportamiento).

Esta ruta, para ser la de la home, es un poco complicada, ¿no?

Lo que ocurre es que pretendí que My-symple-web fuera multiidioma, y de paso


poderte explicar cómo se puede modificar el núcleo de Slim para que haga co-
sas para las que no está preparado. Sí, ¡hay que perderle el miedo a todo! Si
no te arriesgas, no sabes si se puede hacer.

NOTA
Hay un capítulo que aborda el tema de i18n (internacionalización) con detalle.

Por defecto las rutas son de este estilo:


» /
» /contacto
» /articulos
» /articulo/1
» etc.
Para tener nuestra aplicación en varios idiomas tenemos varias posibilidades,
la más sencilla sería esta:
$app->get('/', 'home_route_controller');
$app->get('/es', 'home_route_controller');
$app->get('/fr', 'home_route_controller');

Quedando de la misma manera la función que atiende la ruta.


function home_route_controller()
{
$app = \Router\SlimExt::getInstance();
$lang = extract_lang_from_uri();
$app->render('frontend/home/index.html.twig');
}

101
Programación PHP profesional con Slim, Paris y Twig

Por simplicidad, y sobre todo para que cada vez que se tenga que añadir un
nuevo idioma no haya que rehacer la lista de rutas, se puede confeccionar el
patrón indicado antes, pero quiero que tengas claro que son equivalentes.

Con esto queda visto el modelo de ruta simple que introduje al principio de este
punto. Recuerda que más adelante retomaremos los idiomas.

Veremos luego unos cuantos ejemplos de rutas más complejas.

Personalizar errores en Slim


Error 404, not found
En el capítulo de introducción de Slim dejé caer que los errores de ruta no en-
contrada o error de servidor se pueden personalizar. Veamos cómo:
<?php

require_once __DIR__ . '/../../vendor/autoload.php';

// Prepare app
$app = new \Slim\Slim();

/**
* Not found 404 handler method
*/
$app->notFound(function(Exception $e = NULL ) use($app){
print 'p&aacute;gina no encontrada'; exit;
});
$app->get('/', function() use ($app){
print('hola'); exit;
}
);

// Run app
$app->run();

Ejemplo 33: demos/controller/error404demo.php

Este ejemplo está en el repositorio del libro3.

La página home se mostraría así:

3
https://github.com/jlaso/my-simple-web

102
8. Controller: Slim

Una página que no existe se vería así:

Este ejemplo es muy simple. Piensa que la respuesta se puede personalizar


perfectamente con marcado HTML y presentar, digamos, un error bonito, que
permita regresar a la página home, etc. Pero por motivos didácticos interesa
que veas que Slim captura el error y que lo lleva a la función que definas, en
caso de que definas alguna, si no presentará su página de errores estándar,
que es esta:

103
Programación PHP profesional con Slim, Paris y Twig

Esta captura se ha generado comentando las líneas


$app->notFound(function(Exception $e = NULL ) use($app){
print 'p&aacute;gina no encontrada'; exit;
});

del ejemplo anterior.

Error 500, genérico de servidor


Para tratar los errores 500 el principio es el mismo, aunque la función que
atiende el error tiene que averiguar cuál es la causa del mismo para mostrar un
error indicativo, en entornos reales podríamos llevar un log, enviar un correo
electrónico, etc., en lugar o además de mostrar un error al usuario.
<?php

error_reporting(E_ALL); // @1

require_once __DIR__ . '/../../vendor/autoload.php';

// Prepare app
$app = new \Slim\Slim();

$app->config('debug', false); // @2

/**
* error 500 handler method
*/
$app->error(
function (\Exception $e) use ($app)
{
$html = '<html>';
$html .= '<body>';
$html .= '<ul><li>Error <strong>' . $e->getMessage() . '</strong></li>';
$html .= '<li>en la linea ' . $e->getLine() . '</li>';
$html .= '<li>del archivo ' . $e->getFile() . '</li>';
$html .= '</ul></body>';
$html .= '</html>';
print $html;
exit;
}
);

$app->get('/', function() use ($app){


// deliberadamente provoco un error
print(1/0); exit;
}
);

104
8. Controller: Slim

// Run app
$app->run();

Ejemplo 34: demos/controller/error500demo.php

Se obtiene este mensaje de error personalizado:

A destacar del código:

@1:
Si queremos filtrar los códigos de error que queremos mostrar al usuario utili-
zaremos la función error_reporting de PHP, ya que Slim respeta su contenido a
la hora de lanzar la excepción personalizada.

@2:
Es obligatorio poner a Slim en modo no debug para que el tratamiento persona-
lizado de los errores funcione, en otro caso Slim siempre mostrará su pantalla
completa de errores, que es esta:

105
Programación PHP profesional con Slim, Paris y Twig

Ganchos (Hooks)
Vamos a abundar un poco en los entresijos de Slim y veremos cómo podemos
hacer cosas más complejas.

Slim, en sus versiones recientes, incorpora la posibilidad de establecer escu-


chadores para determinados ganchos que permiten alterar el comportamiento
de Slim sin necesidad de modificar su código.

Estos ganchos son determinados por Slim en su ciclo de petición-procesado-


respuesta. Y además nos permite a los programadores definir otros a los que
podamos llamar y/o atender desde otras partes del ciclo mencionado.

Vamos por partes: los ganchos que define Slim son estos y son llamados en
este orden:
» slim.before
» slim.before.router
» slim.before.dispatch
» slim.after.dispatch
» slim.after.router
» slim.after

Slim los invoca de forma automática en cada fase del ciclo. Si tienes curiosidad
echa un vistazo al archivo vendor/slim/slim/Slim/Slim.php. La función call es
la que se encarga de llamar a los ganchos.

Nosotros simplemente los declaramos mediante $app->hook y se invocarán en


el momento preciso según su clave.

Además, estos se pueden encadenar y se llamarán en el orden de creación o


según su prioridad si se ha definido.

Veamos un caso práctico en el que podemos usar estos ganchos.

Controlar la procedencia de los usarios (tasa de conversión)


Queremos controlar el origen de los usuarios que vienen a nuestra web y se
registran. En función de unos banners comerciales que hemos puesto en algu-
nos sitios web. Como la publicación de estos banners no es gratuita queremos
comprobar su eficiencia para relativizar cuál es el origen de nuestros usuarios
y amortizar así su coste, haciendo más hincapié en aquellos que nos han pro-
porcionado mayor número de usuarios registrados.

106
8. Controller: Slim

Aunque este es un caso eminentemente práctico y que se puede dar en la vida


real, por motivos pedagógicos está minimizado al máximo. Se trata de que
veas un posible uso de los ganchos de Slim.

Hemos previsto que cada uno de los banners tenga un código que nos permita
identificarlo, así la URL de aterrizaje para cada uno de ellos tendrá este aspecto:

http://mysimpleweb.com.es/registro?code=1

Pero al mismo tiempo queremos permitir que la variable code pueda ir en todas
las URL, pero que se materialice en la de registro, es decir, podríamos tener esto:

http://mysimpleweb.com.es/xxxxxxxx/?code=1, siendo xxxxxxxx cualquier cosa.

Resolviendolo de la manera tradicional vamos a leer en el bootstrap la variable


code de la URL e introduciremos su valor en una variable de sesión. Luego en
el proceso de registro tendremos en cuenta el valor de esa variable. Así (antes
de $app->run):

<?php

// ...

$_SESSION['code'] = isset($_GET['code']) ? $_GET['code'] : 0;

// Run app
$app->run();

Esta simple línea nos controla el valor de la variable code entremos por donde
entremos. Pero convendrás conmigo en que, aunque es una solución óptima y
muy concreta, está tan acoplada al código que difícilmente podremos variarla
sin modificar la línea que la contiene. Para este caso tan ingenuo quizás te
parezca absurdo hacerlo de otro modo, pero cuando adquieras el hábito de
hacerlo bien no entenderás por qué se podía hacer de la manera anterior.

Se trata, en todo caso, de desacoplar el código, que quede clara la intención de


lo que queremos hacer y por supuesto que sea escalable.

Vamos a crear una función que nos realizará lo anterior. Ten en cuenta que si
por ejemplo la primera vez llamamos a la URL con la variable code establecida
pero las siguientes veces no, la variable de sesión se nos borrará y no hará lo
que pretendíamos en un principio. Si por el contrario queremos filtrar los regis-
tros desde determinada IP, para que no se tenga en cuenta este parámetro si
se viene desde la IP fija de nuestra oficina, tendremos que alterar y agregar
más código a lo que en principio era una línea clara y simple.

107
Programación PHP profesional con Slim, Paris y Twig

Cualquier alteración por nimia que nos parezca enturbiará el bootstrap, y ade-
más nos planteará un momento de ejecución que a lo mejor no es el correcto.
De la manera en que lo hemos hecho en el ejemplo siempre va a suceder antes
de que Slim tome el control ( $app->run(); ).

Veamos ahora cómo hacerlo bien:

<?php
// ...
$app->hook('slim.before.dispatch', function() use($app){
if(isset($_GET['code'])){
$_SESSION['code'] = $_GET['code'];
}
}
);

// Run app
$app->run();

En todo caso conviene poner los hooks en un archivo separado para más cla-
ridad y mantener el bootstrap lo más limpio posible.

Cuestiones susceptibles de ser controladas mediante hooks son: control de


acceso, formateado de la salida, seguridad, log, debug, etc.

Middlewares
Hablando de control de acceso, hay otro mecanismo más general que suele
ser utilizado para el control de acceso a zonas restringidas de la aplicación. Las
middleware o funciones intermedias se ejecutan justo antes de la función que
atiende la ruta. El valor de devolución de la middleware motiva la ejecución o
no de la que atiende la ruta.

Este es un buen ejemplo de middleware:

<?php

// ...

function redirectIfNotLogged()
{
$app = Slim::getInstance();
if (!isLogged()) {
$app->redirect($app->urlFor('.login'));
}
}

108
8. Controller: Slim

$app->get('/:lang/admin/', 'redirectIfNotLogged', function ($lang) use ($app) {


$app->render('backend/home/index.html.twig');
})->name('admin.index');

Ejemplo 35: ejemplo de función middleware

Las middleware se pueden encadenar y se ejecutarán en el orden declarado,


siempre que el resultado sea true se seguirá la cadena, la última es la función
que realmente atiende la ruta.

Lo normal es que las middleware se reutilicen en varias rutas, por ello no sue-
len ser closures, sino funciones globales, y se usan mediante la cadena de
texto que representa su nombre.
$app->get('/ruta/', 'middleware1', 'middleware2', ... ,'funcion_de_la_ruta');

Rizando el rizo
Te animo a escoger cualquier tarea que tu aplicación web necesite hacer de
manera repetitiva y reiterada y, además, en los mismos puntos para que estas
tareas sean realizadas por ganchos. A modo de ejemplo te dejo la siguiente lista:
» Envío de correo electrónico de confirmación a un usuario al registrarse.
» Subir un comentario a Facebook cuando el usuario publique en nuestro
sitio.
» Actualizar la puntuación del usuario cuando este haga alguna acción
premiada.
» Hacer procesamiento de imágenes en el servidor cuando el usuario
suba una imagen (hacerla más pequeña, normalizarla, generar una mi-
niatura, etc.).
Algunos de estos ejemplos, conforme vaya recibiendo feedback por vuestra
parte y en la medida en que mi tiempo me lo permita, irán siendo publicados
en mi blog4.

Conclusión
Sin duda, la pieza más importante de nuestro invento, el controlador permite
poner todas las piezas en juego y hacer que una petición de un usuario pueda
recorrer todo el ciclo de manera oportuna, recabar los datos necesarios de la
bbdd, persistirlos si fuera preciso y devolver una respuesta adecuada al usuario.
4
Te recuerdo que publico con cierta frecuencia artículos de divulgación sobre progra-
mación en http://www.jaitec.net

109
Programación PHP profesional con Slim, Paris y Twig

Conforme veamos los demás componentes en profundidad sin duda verás la


importancia que tiene Slim para nuestro pequeño proyecto.

110
9 Vamos con
el modelo

Contenido
» Introducción
» Configurar la instancia
» Recordando un poco
» Una consulta sencilla
» Una consulta completa
» Usando tablas de bases de datos diferentes
» Extender IdiOrm&Paris
» Diagrama de tablas de My-simple-web
» SluggableInterface
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Ya vimos en la primera parte una ligera aproximación al modelo, si hay algo
que necesites recordar es un buen momento para darle un repaso a ese capí-
tulo. En todo caso, en este vamos a acometer el tema de manera muy práctica,
por lo que vamos a entrar en materia enseguida.

Veremos cómo se configura la instancia en el bootstrap, cómo realizar tareas


sencillas y complejas. Y por fin veremos cómo están configuradas las entida-
des del modelo que en la mayoría de los casos se materializan en una tabla en
la base de datos.

Pretendo que veas en primer lugar cómo está organizada nuestra aplicación de
ejemplo y, sobre todo, que la puedas utilizar como modelo en los desarrollos
que hagas.

Configurar la instancia
ORM::configure('mysql:host=localhost;dbname=my-simple-web');
ORM::configure('username', 'user');
ORM::configure('password', 'password');
Estas tres instrucciones son suficientes para configurar el ORM, a partir de aquí
se puede utilizar mediante las órdenes correspondientes.

Te voy a contar un truco para poder depurar las aplicaciones, en lo relacionado


con el modelo, Paris en nuestro caso.

En ocasiones si las consultas son complejas es difícil saber en qué instante


falla, o no se produce la consulta de manera adecuada.

Por ello disponemos de los siguientes métodos:


$log = ORM::get_query_log();
$qry = ORM::get_last_query();

Con el primero obtenemos un array con las consultas efectuadas y con el segun-
do solo la última consulta. Esta la puedes utilizar en el caso de querer sustituir al
gestor de excepciones por defecto de PHP, y de esta manera mostrar la última
consulta efectuada, posiblemente la que motivó el fallo que lanza la excepción.

Recordando un poco
A modo de breve recordatorio has de crear una clase que extienda de Model, la
cual en principio no es necesario que tenga propiedades ni métodos. Esta sim-
ple herencia permite al ORM instanciar un objeto de esa clase en las diferentes
llamadas que efectúes.

112
9. Vamos con el modelo

class Cliente extends Model {}

Verás más adelante que vamos a extender la clase Model que nos proporciona el
ORM con una serie de mejoras, aprovechando la herencia que proporciona PHP.

Una consulta sencilla


Aunque este tema lo hemos visto anteriormente, voy a repetirlo aquí por mo-
tivos didácticos y sobre todo por mantener un orden. Recuerda que podemos
recuperar todos los registros de una tabla mediante esta sentencia:
$clientes = Model::factory('Cliente')->find_many();

y solo uno concreto por medio de su ID (en el ejemplo el cliente 12) de esta
manera:
$cliente = Model::factory('Cliente')->find_one(12);

Una consulta completa


Pero en el mundo real no va a ser todo tan sencillo como recuperar todos los
registros o recuperar uno concreto por ID. Lo normal va a ser filtrar datos entre
fechas, entre dos valores determinados, con un código postal concreto, con un
valor aproximado en el campo nombre (LIKE). Las posibilidades son muchas
y aunque el ORM tiene ciertas limitaciones, estas quedan solventadas con los
métodos raw_query y where_raw.

Vamos a ver algunos de los casos más utilizados:

Un where normal
$usuario = Model::factory('User')
->where('email', 'jlaso@joseluislaso.es')
->find_one();
Con una condición más compleja, sumando condiciones (and):
$usuario = Model::factory('User')
->where('email','jlaso@joseluislaso.es')
->where_gte('role_id', 500)
->find_one();
Cuando queremos hacer un where con OR no podemos hacerlo directamente
mediante el método where pues las condiciones se agregan (AND) y tenemos
que utilizar un método alternativo que nos proporciona el ORM: where_raw.
$students = Model::factory(‘Student’)
->where_raw(‘name = ? OR name = ?’, array(‘JOSELUIS’, ‘ANGEL’))
->find_many();

113
Programación PHP profesional con Slim, Paris y Twig

Un join
Volvamos al ejemplo sobre alumnos y cursos, vamos a obtener un listado de
los alumnos que están inscritos en el curso 1:
$allStudents = Model::factory('Grade')
->table_alias('g')
->join('Student', array('s.grade_id', '=', 'g.id'), 's')
->where('g.id', 1)
->find_many();

foreach ($allStudents as $student){


print sprintf('%03d | %06d | %30s' . PHP_EOL,
$student->grade_id, $student->id, $student->name);
}

Ejemplo 36: sample_join.php

Un where más complejo


A menudo tendrás que resolver consultas en las que hay implicadas condicio-
nes repetitivas. El ORM permite asociar un método con un filtro de tal manera
que se encapsule parte de la consulta en ese nombre autoexplicativo1, dejando
más clara la lectura de la consulta. En el caso que te presento a continuación,
para obtener los alumnos que está cursando el primer curso es suficiente con
usar un WHERE grade_id = 1, por lo que esa condición la encapsulamos en
un método dentro de la clase donde vamos a realizar esa consulta, teniendo
en cuenta que le va a llegar la instancia del ORM como parámetro. Luego solo
tenemos que usarlo mediante filter('nombre-del-metodo').
class Student extends Model
{
public static function firstgrade($orm)
{
return $orm->where('grade_id', 1);
}
}

class Grade extends Model


{

// ...

1
Si el nombre del método no se puede asociar con lo que hace en realidad la interpre-
tación de la consulta donde está implicado ese filtro se hace farragosa.

114
9. Vamos con el modelo

$allStudentsOfFirstGrade = Model::factory('Student')
->filter('firstgrade')
->find_many();

foreach ($allStudentsOfFirstGrade as $student){


print sprintf('%03d | %06d | %30s' . PHP_EOL,
$student->grade_id, $student->id, $student->name);
}

Ejemplo 37: sample_filter.php

Rizando el rizo
Cuando veamos el apartado de extender el modelo veremos cómo hacer con-
sultas más complejas, utilizando las ventajas del ORM.

TRUCO
Se pueden indicar expresiones de MySQL directamente como valor de
los campos, mediante set_expr:
$usuario->set_expr('last_logged_in', 'NOW()');

Usando tablas de bases de datos diferentes


El ORM permite utilizar varias configuraciones para poder utilizar una fuente de
datos alternativa, de esta manera podremos atacar dos o más bbdd diferentes
en el mismo programa. Para más información mira la documentación oficial en:
http://paris.readthedocs.org/en/v1.3.0/connections.html

Hack para mysql


Si necesitas acceder a una tabla que está en otra base de datos, dentro
del mismo servidor mysql, y con las mismas credenciales de acceso que la
base de datos configurada en el ORM puedes hacer lo siguiente:
class TablaAjena extends Model // implements etc ...
{
protected static $_table = 'bbdd2.tabla-ajena';
// resto de métodos
}
Gracias a un hack de mysql si separas el nombre de la tabla del de la base
de datos con un punto podrás tener este acceso.

115
Programación PHP profesional con Slim, Paris y Twig

Extender IdiOrm&Paris
Lo cierto y verdad es que se me hace muy pesado escribir cada vez la ins-
trucción Model::factory('Entidad') ya que el IDE que utilizo (PhpStorm
para los interesados) me ayuda con el autocompletado y ‘Entidad’ es un pará-
metro literal que no se autocompleta. Me resultaría más fácil escribir algo así:
Entidad::factory(). Esta traducción es muy sencilla.

Además, el hecho de tener nuestra propia clase nos permitirá agregar al objeto
Model métodos extra, como pueden ser los relacionados con la validación e
hidratación que luego veremos en acción.

Por ello vamos a extender Model con nuestra propia clase y la vamos a dotar
de algunas mejoras.

Vamos a ver cómo hacer la primera parte, y después le vamos a ir añadiendo


filigranas:

<?php

namespace app\models\core;

use \Model;
// ...
abstract class BaseModel extends Model implements ...
{
public static function factory(...)
{
// ...
$class = get_called_class();
return parent::factory($class);
}
// ...
}

Ejemplo 38: BaseModel.php incipiente

Esta sencilla implementación nos va a permitir invocar Entidad::factory()


en lugar de Model::factory(‘Entidad’).

Vamos ahora a dotar a nuestro BaseModel de una nueva funcionalidad, como


puede ser el hacer un bind desde un formulario. En definitiva, le pasamos el
request al bind y debe ser capaz de asociar cada campo del formulario con
su correspondiente en el modelo.

Para ello definimos una interfaz que obligamos a BaseModel a cumplir.

116
9. Vamos con el modelo

<?php

namespace app\models\core;

/**
* Permite que una entidad pueda tomar directamente los valores de un
* array asociativo, por ejemplo el request e hidratarse con esos valores,
* para hacer un bind de un formulario
*/
interface BindableInterface
{
/**
* Efectúa el bind de los parámetros pasados
* @param array $array
* @return mixed
*/

public function bind(array $array);


}

Ejemplo 39: app/models/core/BindableInterface.php

Volvemos a la clase BaseModel, hacemos que implemente la nueva interfaz y


añadimos el método nuevo:
<?php

namespace app\models\core;

use \Model;
use app\models\core\BindableInterface;

abstract class BaseModel extends Model implements BindableInterface


{
// ...
public function bind(array $array)
{
foreach ($array as $key=>$value) {
$this->set($key,$value);
}
}
// ...
}

Ejemplo 40: BaseModel.php con bind

Si estás siguiendo el código fuente de la clase BaseModel en github verás que


este método no es tan sencillo. Simplemente no quiero abordar la complejidad
del resto en este momento, quédate con el hecho de que asignamos cada cam-
po que encontremos en el array pasado.

117
Programación PHP profesional con Slim, Paris y Twig

Esto plantea algunos problemas en caso de tener campos que no estén den-
tro del modelo, como el campo de comprobación de «soy humano» en el
formulario de contacto.
La mayor parte de la complejidad que ahora obviamos la vamos a ver en este
mismo capítulo, aunque más adelante he preparado uno exclusivamente con
cuestiones relacionadas con los formularios y las validaciones.
En el capítulo dedicado a los formularios precisamente pongo el ejemplo del
formulario de contacto.

Diagrama de tablas de My-simple-web


Vamos a seguir revisando las cuestiones relativas al modelo mediante el códi-
go de My-simple-web que está implicado en esta tarea.

En primer lugar quiero mostrarte el diagrama de las tablas que he previsto en


la aplicación.

Diagrama de relaciones entre las tablas de My-simple-web

118
9. Vamos con el modelo

Verás enseguida que no es una estructura muy compleja, es imposible crear


una estructura real como la de un CMS para ilustrar el uso del patrón MVC en
un libro de estas dimensiones, pero como vas a ver enseguida cada una de las
tablas/entidades implicadas tiene puntos diferentes que te ayudarán a ver en
conjunto las posibilidades totales.

La finalidad de mostrarte el diagrama es doble: por un lado para que constates


que la estructura de la aplicación es muy simple a nivel de entidades del mo-
delo y por otro que te familiarices con su estructura porque en los ejemplos de
otros capítulos vamos a hacer referencia a menudo a estas tablas/entidades.

Para automatizar la creación y manipulación de las diferentes tablas dentro de


mysql he previsto un sistema de nombres basado en el del de la clase donde
está declarada la entidad. En otros sistemas más potentes como Doctrine el
nombre de la tabla se declara explícitamente, nuestro ORM también permite
declarar este nombre como una propiedad estática en la clase, y utilizando esta
técnica es como he inyectado ese nombre en cada tabla.

Evidentemente esta es solo una convención y puedes declarar estas u otras


tablas con el nombre que quieras, la ventaja de hacerlo así es que viendo el
nombre de la tabla asocias enseguida el lugar donde está declarada la clase
que la representa.

Vamos a ir viendo una por una esas clases. Las puedes encontrar dentro del
código en la carpeta app/models.

En primer lugar la clase de la que descienden las demás, la hemos introducido


en los puntos anteriores, pero ahora te la muestro de manera completa.

BaseModel

Ya hemos estado viendo alguna de las mejoras que le hemos hecho a la clase
Model que nos aporta el ORM, y que las hemos puesto dentro de un descen-
diente directo: BaseModel, vamos a ver cómo queda la clase de manera inte-
gral para que te hagas una idea completa.

119
Programación PHP profesional con Slim, Paris y Twig

<?php

namespace app\models\core;

use \Model;
use app\models\core\BindableInterface;
use app\models\core\Form\FormListTypeInterface;

/**
* Extends Model from ORM
*
* The ORM uses magic __get and __set to map properties to table fields, for that
* I have named my other methods with underscore to permit use that names for fields
*
*/
abstract class BaseModel
extends Model
implements BindableInterface
{

/**
* Factory from extended class
*
* that permits this
* Entity::factory()->...
* or
* BaseModel::factory('Entity')->...
*
* @param string $class
*
* @return \ORMWrapper
*
*/
public static function factory($class="")
{
if (!$class) {
$class = get_called_class();
}

return parent::factory($class);
}

/**
* @param string $class
*
* @return BaseModel
*/
public static function create($class="")
{
if (!$class) {
$class = get_called_class();
}

120
9. Vamos con el modelo

return parent::factory($class)->create();
}

/**
* Bind entity data fields, post form for example
*
* @param array $array
*
* @internal param $assoc -array $array
* @return mixed|void
*/
public function bind(array $array)
{
$ent = get_called_class();
$frmLstClass = "\\{$ent}FormType";
if (class_exists($frmLstClass)) {
/** @var FormListTypeInterface $formList */
$formList = new $frmLstClass;
foreach ($formList->getForm() as $formItem) {
$field = $formItem['field'];
$type = strtolower($formItem['type']);
$ro = isset($formItem['widget']['readonly'])
&& $formItem['widget']['readonly'];
// only bind not readonly fields
if (!$ro && in_array($type,array(
'text',
'textarea',
'number',
'hidden',
))) {
$value = isset($array[$field]) ? $array[$field] : null;
if ( null !== $value ) {
$this->set($field,$value);
}
}
}
} else {
foreach ($array as $key=>$value) {
$this->set($key,$value);
}
}
}

/**
* get default create options
*
* @return array
*/

121
Programación PHP profesional con Slim, Paris y Twig

public static function _defaultCreateOptions()


{
return array(
'engine' => "InnoDB",
'charset' => "latin1"
);
}

/**
* Get the table name that corresponds to class name
* in the actual namespace system
*
* @param string $class
* @return string
*/
public static function _tableNameForClass($class)
{
// CamelCase to undescore_case
$class = strtolower(preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1_', $class));
// table name equals to PSR0 of class name
return str_replace("\\","_",strtolower($class));
}

/**
* Get the pretty name of the model
*/
public static function _entityName()
{
$class = \lib\MyFunctions::camelCaseToUnderscored(get_called_class());
$array = explode("\\",$class);
array_shift($array);

return implode("",$array);
}

/**
* Get relations
*
* @return array
*/
public function _relations()
{
return array();
}

/**
* Get name of entity in singular
*
* @return string
*/

122
9. Vamos con el modelo

public static function _nameSingular()


{
return _('BaseModel');
}

/**
* Get name of entity in plural
*
* @return string
*/
public static function _namePlural()
{
return _('BaseModels');
}

/**
* Get name of entity
*
* @return string
*/
public static function _nameEntity()
{
$str = \lib\MyFunctions::camelCaseToUnderscored(get_called_class());
return str_replace('\\','_',$str);
}

/**
* Return message "$field can’t leave blank", translated
*
* @param string $field
*
* @return string
*/
public function _cantLeaveBlank($field)
{
return sprintf(_('%s can\'t leave blank'),$field);
}

Como puedes ver, hemos hecho algo más que extender la clase básica Model
que nos proporciona nuestro ORM. Lo más destacable es que la obligamos a
cumplir con la interfaz bindable, de tal manera que cualquier descendiente po-
drá hacer directamente un $entity->bind($request).
Lo explico al principio del código fuente (en inglés2): hago uso del subrayado en
todos los métodos para dejar el espacio de nombre intacto para los nombres de
2
Es una buena práctica declarar variables y métodos en inglés ya que es un idioma más
compacto que el nuestro y además nos hacemos más universales de cara a extender
nuestro código. Normalmente también suelo escribir los comentarios en inglés.

123
Programación PHP profesional con Slim, Paris y Twig

los campos. Recuerda que el ORM resuelve los nombres de campos/propieda-


des mediante los métodos __get, __set.

Aquí puedes ver también que en el método _tableNameForClass el nombre


de la tabla se obtiene de la clase y el namespace donde esta está declarada,
tal y como te comentaba al principio de ese apartado.

SluggableInterface
En ocasiones tendremos que validar un campo que represente un slug (parte
de la url que define el item en concreto).

Estos valores deben ser únicos dentro de la tabla concreta y además no pue-
den contener espacios ni símbolos diferentes de letras, números y el guion.

Por ese motivo he creado una interfaz que integre ese comportamiento:

124
9. Vamos con el modelo

<?php

namespace app\models\core;

interface SluggableInterface
{

public static function checkSlug($slug,$id=0);

Ejemplo 41: app/models/core/SluggableInterface.php

Es una interfaz muy sencilla pues solo obliga a implementar un método con-
creto, que deberá devolver un valor positivo en el caso de que el slug pasado
exista en la bbdd para la tabla concreta. Si el ID pasado como argumento es
diferente de cero es para no tener en cuenta el registro representado por ese
ID, de esa manera podremos validar ese slug sin considerar su propio registro.
De lo contrario, siempre daría positivo para un slug preexistente.

A lo largo de las diferentes declaraciones de entidades que vamos a ir viendo,


este método estará en Article, Config y Staticpage. Estate atento.

Ahora vamos a ver todas las clases que descienden de BaseModel, que son
las que implementan de alguna manera nuestra estructura de la base de datos
(salvo alguna excepción, que veremos).

Entity_entity
Vamos a empezar viendo la entidad más sencilla, su función dentro de la apli-
cación no es otra que permitir automatizar tareas de gestión en la parte trasera
(backend). Su implementación es la más sencilla de todas las que vamos a ver ya
que incluye solo el método que permite su creación dentro de la base de datos.

Fíjate que otros ORM, como Doctrine, pueden extraer los metadatos de las
propiedades declaradas en la clase que representa la entidad, pero en nuestro

125
Programación PHP profesional con Slim, Paris y Twig

caso no existen tales propiedades, al menos no de forma declarativa. Como ya


te comenté, el ORM mapea los campos reales de la tabla física con propieda-
des de la clase que la representa, y se ayuda de los métodos «mágicos» __get
y __set para hacer esto.

<?php

namespace Entity;

use app\models\core\BaseModel;

class Entity extends BaseModel


{

/**
* Get the SQL creation sentece of this table
*
* @param array $options
* @return string
*/
public static function _creationSchema(Array $options = array())
{
$class = self::_tableNameForClass (get_called_class());

// default options
$options = array_merge(self::_defaultCreateOptions(),$options);

return

<<<EOD

CREATE TABLE IF NOT EXISTS `{$class}` (


`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`title` varchar(100) NOT NULL,
`new` tinyint(1) NULL,
`delete` tinyint(1) NULL,
`show` tinyint(1) NULL,
`list` tinyint(1) NULL,
PRIMARY KEY (`id`)
) ENGINE={$options['engine']} DEFAULT CHARSET= {$options[‘charset’]} AUTO_INCRE-
MENT=1 ;

EOD;

126
9. Vamos con el modelo

Un apunte antes de seguir: no soy partidario de abusar de la declaración en lí-


nea de cadenas haciendo uso de heredoc o nowdoc, pero he de reconocer que
cuando la cadena tiene formato su uso clarifica de manera notable el código.

No obstante he de reconocer que la solución mejor sería una serie de propieda-


des declaradas en la clase indicando type, anulabilidad, etc. de cada campo. Al
igual que he hecho más adelante con el sistema de enrutado quizás me plantee
este reto en el futuro. Aun así no pierdas de vista el hecho de que este ORM
no necesita declarar las propiedades para poder acceder a los campos de la
base de datos.

Entity_staticpage

Esta entidad permite liberar al programador del hecho de maquetar o coordinar


las típicas páginas estáticas que toda web tiene, permitiendo además delegar ese
mantenimiento en el administrador de la web, dando un aspecto más profesional.

El hecho es que páginas como la de política de privacidad, términos o acuerdos


de uso, información general, etc. son susceptibles de ser implementadas median-
te esta argucia. Por supuesto queda de tu mano o no usarla en tus desarrollos.

<?php

namespace Entity;

use app\models\core\BaseModel;
use app\models\core\SluggableInterface;
use app\models\core\ValidableInterface;
use lib\SlimFunctions;

class Staticpage
extends BaseModel
implements SluggableInterface,ValidableInterface
{
public static function checkSlug($slug, $id = 0)
{
$count = self::factory()
->where('slug',$slug)
->where_not_equal('id',$id)

127
Programación PHP profesional con Slim, Paris y Twig

->count();
$sql = \ORM::get_last_query();

return $count > 0;


}

public function validate()


{
$result = array();
if(empty($this->slug)) $this->slug = $this->titulo;
$this->slug = \lib\MyFunctions::slug($this->slug);
if (empty($this->slug)) {
$result['slug'] = $this->_cantLeaveBlank(_('Slug'));
} else {
$slugExists = self::checkSlug($this->slug,$this->id);
if ($slugExists) {
$result['slug'] = _('Repeated').' '._('Slug');
}
}

return $result;
}

/**
* Get the SQL creation sentece of this table
*
* @param array $options
* @return string
*/
public static function _creationSchema(Array $options = array())
{
$class = self::_tableNameForClass(get_called_class());

// default options
$options = array_merge(self::_defaultCreateOptions(),$options);

return

<<<EOD
CREATE TABLE IF NOT EXISTS `{$class}` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`slug` varchar(100) NOT NULL,
`title` varchar(100) NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE={$options['engine']} DEFAULT CHARSET=ons['charset']} AUTO_INCREMENT=1;

EOD;
}
}

128
9. Vamos con el modelo

En esta ocasión hemos introducido dos métodos nuevos, uno viene dado por
el cumplimiento de la interfaz ValidableInterface: validate, y el otro por
SuggableInterface: checkSlug.

Si te acostumbras a usar este tipo de forma de trabajo a buen seguro adelanta-


rás. Se trata de unificar comportamientos en una interfaz. No te preocupes si la
interfaz solo obliga a cumplir un método pues no creo que alcances el límite de
implements que puede tener una clase. De alguna manera es un mecanismo
complementario al de la herencia. Recuerda que PHP, al menos en sus versio-
nes 5.3.x e inferiores, no permite la herencia múltiple.

Por un lado validate será llamada cuando se procese un formulario de mane-


ra automática. Es el caso del manejo automático de entidades en el backend de
My-simple-web, se comprubea si la entidad en cuestión cumple con la interfaz
ValidableInterface e invoca su método validate y en función de su resul-
tado proseguirá o no con la ejecución del resto.

Te muestro el código donde se usa esto, aunque lo veremos más adelante en


detalle:

...
if ($request->isPost()) {
$item->bind($request->post());
if ($item instanceof ValidableInterface) {
$errors = $item->validate();
}
if (!count($errors)) {
$item->save();
$app->redirect($app->urlFor('admin.list-entity',array(
'entity'=>$entity
)));
}
}
...

Ejemplo 42: fragmento de app/controller/backend/CRUDedit-entity.php

Entity_article

Una de las entidades que permite mostrar un uso más extensivo del ORM,
dentro de My-simple-web es la de artículos.

No queriendo ser un blog, es cierto que hay cuestiones en la rutina diaria de


programación de desarrollos web que se pueden resolver observando cómo
están implementados los blogs.

129
Programación PHP profesional con Slim, Paris y Twig

Junto con esta entidad hay una asociada a ella, que es la de


ArticleDescriptions, que pretende ser un simple sistema de internaciona-
lización de los los contenidos.

Fíjate en el diagrama de relaciones de las tablas como


entitiy_article se une a entity_description mediante una tabla interme-
dia: entitiy_article_description.

Veamos primero la clase que representa los artículos.


<?php

namespace Entity;

use app\models\core\BaseModel;
use app\models\core\SluggableInterface;
use app\models\core\ValidableInterface;
use lib\MyFunctions;

/**
* Class that stores articles of this web
*/
class Article
extends BaseModel
implements SluggableInterface, ValidableInterface

130
9. Vamos con el modelo

/**
* Checks that slug not exists
*
* @param string $slug
* @param int $id
* @return bool
*/
public static function checkSlug($slug, $id = 0)
{
$count = self::factory()
->where('slug',$slug)
->where_not_equal('id',$id)
->count();

return $count > 0;


}
/**
* Validates the info
*
* @return array
*/
public function validate()
{
$result = array();
if(empty($this->slug)) $this->slug = $this->title;
$this->slug = \lib\MyFunctions::slug($this->slug);
if (empty($this->slug)) {
$result['slug'] = $this->_cantLeaveBlank(_('Slug'));
} else {
$slugExists = self::checkSlug($this->slug,$this->id);
if ($slugExists) {
$result['slug'] = _('Repeated slug');
}
}
if (empty($this->title)) {
$result['title'] = $this->_cantLeaveBlank(_('Title'));
}
if (empty($this->description)) {
$result['description'] = $this->_cantLeaveBlank(_('Description'));
}

return $result;
}

/**
* Get the SQL creation sentece of this table
*
* @param array $options

131
Programación PHP profesional con Slim, Paris y Twig

* @return string
*/
public static function _creationSchema(Array $options = array())
{
$class = self::_tableNameForClass(get_called_class());
// default options
$options = array_merge(self::_defaultCreateOptions(),$options);

return

<<<EOD

CREATE TABLE IF NOT EXISTS `{$class}` (


`id` bigint(11) NOT NULL AUTO_INCREMENT,
`slug` varchar(100) NOT NULL,
`title` varchar(100) NOT NULL,
`description` text NOT NULL,
`description_id` bigint(11) NOT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE={$options['engine']} DEFAULT CHARSET={$options['charset']} AUTO_INCRE-
MENT=1 ;

EOD;

/**
* Get descriptions throught intermediary table
*
* @return \ORM
*/
public function getDescriptions() {
$sql = '`id` IN (
SELECT `description_id`
FROM `entity_article_description`
WHERE `article_id` = ?
)';
$descriptions = Description::factory()
->where_raw($sql,$this->id)
->find_many();

return $descriptions;
}

/**
* Get relations with other entities
*
* @return array
*/

132
9. Vamos con el modelo

public function _relations()


{
return array(
'one-to-many' => array('descriptions' => 'Entity\Description'),
);
}

Aquí nuevamente hemos vueltro a introducir novedades, en este caso tenemos


un getDescriptions que nos va a proporcionar las descripciones auxiliares
relacionadas con el artículo que tenemos.
La clase ArticleDescriptions queda así:
<?php

namespace Entity;

use app\models\core\BaseModel;
use app\models\core\ValidableInterface;
use lib\MyFunctions;

/**
* Class that stores articles of this web
*/
class ArticleDescription extends BaseModel
{

/**
* Get the SQL creation sentece of this table
*
* @param array $options
* @return string
*/
public static function _creationSchema(Array $options = array())
{
$class = self::_tableNameForClass(get_called_class());

// default options
$options = array_merge(self::_defaultCreateOptions(),$options);

return

<<<EOD

CREATE TABLE IF NOT EXISTS `{$class}` (


`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`article_id` bigint(11) DEFAULT NULL,
`description_id` bigint(11) DEFAULT NULL,

133
Programación PHP profesional con Slim, Paris y Twig

PRIMARY KEY (`id`)


) ENGINE={$options[‘engine’]} DEFAULT CHARSET={$options[‘charset’]} AUTO_INCRE-
MENT=1;

EOD;

Entity_contact

La entidad contacto es como una entidad falsa, pues no tiene su correspon-


diente tabla en la base de datos, está implementada con la única finalidad de
validar el formulario de contacto. Este tipo de argucias es habitual en otros
frameworks, aunque son llamados normalmente modelos. En este caso, como
lo que se pretende ilustrar es la posibilidad de hacerlo, queda de la mano del
lector, como siempre, la posibilidad de mejorarlo.

<?php

namespace Entity;

use app\models\core\BaseModel;
use app\models\core\ValidableInterface;
use lib\MyFunctions;

class Contact
extends BaseModel
implements ValidableInterface
{

public function validate()


{
$result = array();
// validar los contenidos de los campos
if (empty($this->name)) {
$result['name'] = $this->_cantLeaveBlank(_('Name'));
}

134
9. Vamos con el modelo

if (empty($this->email)) {
$result['email'] = $this->_cantLeaveBlank(_('Email'));
}
if (empty($this->phone)) {
$result['phone'] = $this->_cantLeaveBlank(_('Phone'));
}
if (empty($this->message)) {
$result['message'] = $this->_cantLeaveBlank(_('Message'));
}

return $result;
}

Como te he comentado al principio de esta entidad, su función es únicamente


validar el contenido que nos llega por parte del formulario de contacto, así que
para fijar conceptos veamos el código del controlador que atiende la ruta de
ese formulario.
$app->map('/contact/', function () use ($app) {
// ...
if ($app->request()->isPost()) {
$sum = $_SESSION['sum'];
$contact->bind($app->request()->post());
$errors = $contact->validate();
if (count($errors)==0) {
// ...
}
// ...
})->via('GET','POST')->name('contact.index');

Ejemplo 43: fragmento app/controller/frontend/contact.php

No me quiero extender más en este punto pues el formulario de contacto se ve


con más detalle en la sección “Formulario de contacto” en la página 161.

Security_user
Como no podía ser de otra manera, tenemos también implementada toda la
parte del usuario que va a acceder a nuestra aplicación, ya sea en la parte
frontal, intranet o backend. Para no interferir con el espacio de nombres más
genérico de entity que estábamos usando hasta ahora, y sobre todo para que
veas que podemos separar en carpetas/conceptos nuestras entidades relacio-
nadas con el modelo de datos.

135
Programación PHP profesional con Slim, Paris y Twig

Todo lo que tiene que ver con el usuario lo he puesto en una carpeta llamada
Security. Es importante destacar que el nombre de la carpeta debe empezar
por mayúscula, ya que cuando regeneramos la base de datos, se tiene en
cuenta este punto para «meterse» en esa carpeta y buscar las clases sus-
ceptibles de generar tablas (que son aquellas que tienen declarado el método
_creationSchema).

El nombre de security trae a colación la relación que tiene la entidad usuario


con el tema de la seguridad, ya que en función del rol que tenga asignado el
usuario tendrá derecho a entrar a unos u otros apartados. Pero se trata de una
convención, y se podría haber elegido cualquier otro perfectamente.

Otra vez más la cuestión de los nombres es al mismo tiempo un mero forma-
lismo y un compromiso por expresar en una sencilla palabra todo aquello que
conlleva la clase o clases implicadas en ello.

Veamos su código:
<?php

namespace Security;

use app\models\core\BaseModel;

class User extends BaseModel


{
private $roles = null;

/**
* Get the SQL creation sentece of this table
*
* @param array $options
* @return string
*/
public static function _creationSchema(Array $options = array())
{

136
9. Vamos con el modelo

$class = self::_tableNameForClass(get_called_class());

// default options
$options = array_merge(self::_defaultCreateOptions(),$options);

return

<<<EOD

CREATE TABLE IF NOT EXISTS `{$class}` (


`id` bigint(11) NOT NULL AUTO_INCREMENT,
`email` varchar(100) NOT NULL,
`pass` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE={$options['engine']} DEFAULT CHARSET={$options['charset']} AUTO_INCRE-
MENT=1 ;

EOD;

/**
* Get the roles have the user
*
* @return array
*/

public function getRoles()


{
if (null === $this->roles) {

$sql = sprintf("SELECT *
FROM `security_role`
WHERE `id` IN
(SELECT `id`
FROM `security_role_user`
WHERE `user_id` = '%d' )"
,$this->id);

$roles = RoleUser::factory()->raw_query($sql);

$this->roles = array();
foreach ($roles as $role) {
$this->roles[] = $role->name;
}

return $this->roles;
}

137
Programación PHP profesional con Slim, Paris y Twig

/**
* Test if user can ... $rol
*
* @param string $rol
*
* @return bool
*/
public function can($rol)
{
// first, get user's roles
$roles = $this->getRoles();
// and test if can ... $rol
return Role::can($roles,$rol);
}

/**
* Goody for find_one
*
* @param int $id
*
* @return \ORM
*/
public static function getUser($id)
{
return User::factory()->find_one($id);
}

Aquí vemos implementado el método getRoles, de la misma manera que


getDescriptions en la entidad Article.
También podemos ver el método can: un helper3 para saber de manera inme-
diata si un usuario tiene permiso para hacer una tarea en concreto y getUser:
un método atajo para buscar un usuario por su ID; este tipo de argucias son
muy útiles y condensan el código resultante y lo clarifican convenientemente.
Para permitir que las entidades expuestas tengan unos determinados re-
gistros al reiniciar la base de datos, vamos a implementar una nueva
interface: FixturableInterface.

<?php

namespace app\models\core;

use app\models\core\Registry;

3
Para mí un helper es un método que permite sistematizar una tarea repetitiva.

138
9. Vamos con el modelo

interface FixturableInterface
{

/**
* Generate fixtures
*
* @param Registry $fixturesRegistry
*
* @return void
*/
public function generateFixtures(Registry $fixturesRegistry);

/**
* Get the order of fixture generation
*
* @return int
*/
public static function getOrder();

Cada una de las entidades susceptibles de tener registros en la inicialización


debe implementar una clase que extienda de FixturableInterface.

Veamos el caso de la entidad StaticPage:

<?php

namespace Entity;

use \app\models\core\FixturableInterface;
use Entity\Staticpage;
use \app\models\core\Registry;

class StaticpageFixture implements FixturableInterface


{
/**
* Creates a new item from $assocArray and inserts into DB
*
* @param array $assocArray
*
* @return $this
*/

139
Programación PHP profesional con Slim, Paris y Twig

public function addNewItem($assocArray)


{
$item = \Entity\Staticpage::factory()->create();
foreach ($assocArray as $field=>$value) {
$item->set($field,$value);
}
$item->save();

return $this;
}

/**
* Generate fixtures
*
* @param \app\models\core\Registry $fixturesRegistry
*
* @return void
*/
public function generateFixtures(Registry $fixturesRegistry)
{
$this->addNewItem(
array(
'slug' => 'about.en',
'content' => 'About us !',
'title' => 'About us',
)
)
->addNewItem(
array(
'slug' => 'about.es',
'content' => '¡ Acerca de nosotros !',
'title' => 'Esto es lo que contamos sobre nosotros.',
)
)
->addNewItem(
array(
'slug' => 'privacy-policy.es',
'content' => 'Esta es nuestra política de privacidad',
'title' => 'Política de privacidad'
)
)
->addNewItem(
array(
'slug' => 'privacy-policy.en',
'content' => 'This is our privacy policy',
'title' => 'Privacy policy'
)
)
;
}

140
9. Vamos con el modelo

/**
* Get the order of fixture generation
*
* @return int
*/
public static function getOrder()
{
return 10;
}
}
Creo que el código se entiende perfectamente: tenemos dos métodos obliga-
torios para cumplir con la interfaz. Uno es el orden en el que hay que ejecutar
esta fixture, ya que puede que haya entidades que dependan de otras y nece-
sitaremos haber creado primero las independientes. El otro es el que se va a
invocar para crear los registros de prueba.

Para regenerar tanto la base de datos como los registros de prueba he creado
un pequeño script que tienes en la carpeta principal del proyecto.

Ten en cuenta que su utilización borra la base de datos sin preguntar, por lo
que en una instalación real de producción deberías borrarlo una vez reiniciada
la bbdd.
<?php

date_default_timezone_set('Europe/Madrid');
error_reporting(E_ALL);
print PHP_EOL.PHP_EOL.__DIR__.PHP_EOL;
$loader = require 'vendor/autoload.php';

Twig_Autoloader::register();
$em = new \app\models\EntityManager(true);

$em->dropDatabase();
$em->createDatabase();
$em->createTables();
$em->generateFixtures();

die('ok'.PHP_EOL.PHP_EOL);

Ejemplo 44: regenerateDatabaseWithCaution.php

Conclusión
Este capítulo ha sido denso en código, no podía dejar la oportunidad de en-
señarte de cerca lo que puedes implementar con el ORM. En la mayoría de
los casos has visto que nos vamos a encontrar con una clase que extiende de

141
Programación PHP profesional con Slim, Paris y Twig

BaseModel, que va a implementar una serie de interfaces en función de las


necesidades que deba cubrir nuestra entidad.

Si la entidad va a permitir un mantenimiento al uso (CRUD), necesitaremos


implementar la interfaz ValidableInterface, además de generar la clase
{Entity}FormType4, en la cual se declaran los métodos que configuran tanto
el listado de registros en forma de tabla como el que presenta los datos de un
registro único, con la intención de crearlo o editarlo. Este punto se ve con deta-
lle en el capítulo “11 Formularios” en la página 159.

4
{Entity} se substituye por el nombre de la entidad en concreto, para Article quedaría
ArticleFormType.

142
10 Añadiendo
Twig

Contenido
» Introducción
» Parte de administración de datos
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Después del capítulo anterior tan prolijo en texto y código vamos a continuar
integrando Twig en nuestra aplicación. Ya hemos hecho un uso muy básico en
algunas rutas que hemos visto en los capítulos anteriores, pero ahora vamos a
centrarnos sobre todo en la inclusión de datos procedentes de la bbdd que el
controlador ha conseguido en la parte que a él le corresponde.

Mandar datos a la plantilla


Veamos por ejemplo cómo se muestran las páginas estáticas que están alma-
cenadas en la bbdd (de esta manera el mismo admin puede alterar su conteni-
do sin necesidad del programador).

CREATE TABLE `entity_staticpage` (

`id` int(11) unsigned NOT NULL AUTO_INCREMENT,

`slug` char(50) NOT NULL DEFAULT ‘’,

`content` text,

`title` varchar(150) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

La tabla de páginas estáticas tiene esta estructura:

Vamos a utilizar el campo slug de la siguiente manera: slug de la página punto


idioma, de tal manera que tendremos tantas entradas para la página about
como idiomas tengamos declarados: about.es, about.en, etc.

$app->get('/:lang/:slug(/)', function ($lang, $slug) use ($app) {

$static = Entity\Staticpage::factory()
->where('slug',$slug.'.'.$lang)
->find_one();

144
10. Añadiendo Twig

if (!$static instanceof Entity\Staticpage) {


return $app->pass(); //@1
}
// @2
$app->render('frontend/home/staticpage.html.twig',array(
'static' => $static,
));

})->name('home.staticpage');

Ejemplo 45: primer ejemplo twig con datos de la bbdd

Este controlador que acabamos de ver atiende las peticiones de rutas estáti-
cas, como /es/about, /en/who-we-are, etc. Sería fácil declarar las rutas en el
idioma correspondiente, solo habría que insertar los registros correspondientes
a sobre-nosotros.es para /es/sobre-nosotros, no lo he hecho así porque las
demás rutas que no son estáticas no están traducidas y puede resultar un poco
chocante que las rutas cambien su estructura. En principio no te has de preo-
cupar por eso porque webs muy conocidas utilizan la técnica de anteponer el
idioma, una barra (/) y luego el slug de la ruta en inglés. En todo caso se pue-
den traducir todas las URL si es tu deseo. Te lo dejo como ejercicio.

@1:
Esta argucia permite, tras comprobar que no existe ese slug en la bbdd, pasarle
de nuevo el control a Slim para que siga procesando la ruta actual con otras
funciones que sean capaces de interpretarla (recuerda el tema de atención
duplicada al principio del libro).

Esta ruta nos viene muy bien como ejemplo porque es muy sencilla, suficiente
para ilustrar cómo le llegan al motor de plantillas las variables.

@2:
Fijémonos en la línea:
$app->render('frontend/home/staticpage.html.twig',array(
'static' => $static,
)
);

Y ahora veamos el contenido de la plantilla frontend/home/staticpage.html.twig


{% extends 'frontend/layout.html.twig' %}

{% block subcontent %}

<div class="h2 grid_12">&nbsp;</div>

145
Programación PHP profesional con Slim, Paris y Twig

<div class="clear"></div>
<div class="grid_2"><p>&nbsp;</p></div>
<div class="grid_8" id=”subcontent”>

<h2>{{ static.title }}</h2>


<p>
{{ static.content }}
</p>

</div>
<div class="grid_2"><p>&nbsp;</p></div>
<div class="h2 grid_12">&nbsp;</div>

{% endblock %}

Ejemplo 46: plantilla frontend/home/staticpage.html.twig

Nada de lo que asustarse, ¿verdad? Después del capítulo de introducción de


Twig esta plantilla está muy clara. Vamos a fijarnos en las líneas:
<h2>{{ static.title }}</h2>
<p>
{{ static.content }}
</p>

No sé si precisa más aclaración pero el segundo parámetro de la función ren-


der (ver @2) es un array asociativo con los nombres de las variables que va a
recibir Twig y como valor el contenido de dicha variable. Estas, como veremos
a continuación, pueden ser valores escalares, arrays, objetos, etc.

Aunque no es obligatorio, por convención llamaremos a la variable de la misma


manera. Por motivos obvios, no obstante, en el caso de que estés utilizando
una plantilla general para varios casos y llames a la variable que contiene los
datos sobre los que iterar items, en la llamada podrías tener algo así:
$app->render('generic.html.twig',array('items' => $customers));

Pero por lo general es mejor tener las variables con el mismo nombre en el con-
troller y en la plantilla, a la larga te evitarás quebraderos de cabeza y el código
es más fácil de seguir.

Parte de administración de datos


En principio el hecho de crear plantillas que vayan a volcar datos de la bbdd no
tiene más misterio que lo que acabas de ver, en todo caso, en un entorno real
te encontrarás cosas más dispares. Por eso vamos a tratar de ver alguna ruta

146
10. Añadiendo Twig

más compleja que mande más datos a la plantilla y que esta tenga condiciona-
les y más complicaciones que lo que hemos simplificado arriba.

La parte de administración o backend de My-simple-web la he creado tratando


de no generar una ruta para entidad, sino una para todas, por eso te pido que
ahora si puedes estés más atento si cabe que en ocasiones anteriores.

Vamos a ver cómo podemos listar todos los registros de una sola entidad, te-
niendo en cuenta, eso sí, que podemos paginar y buscar.

Para simplificar vamos a tratar con la misma entidad: páginas estáticas


(staticpage). Veamos cómo se ha llegado al listado superior:
<?php

use ...

$app->map('/admin/list/:entity/(:page)', function($entity,$page=1) use ($app) {

$entity = \app\models\core\Sanitize::string(trim(strtolower($entity)));
$ucEntity = \lib\MyFunctions::underscoredToCamelCaseEntityName($entity);
$frmLstClass = $ucEntity."FormType";
if (class_exists($frmLstClass)) {

$formList = new $frmLstClass; //var_dump($formList->getFormList());


// @1:
if ($app->request()->isPost()) {
$search = $app->request()->post('search');
$qb = new SearchQueryBuilder($formList->getSearchForm(),$search);
$qb->buildQuery();
$query = $qb->getQuery();
$params = $qb->getParams();

147
Programación PHP profesional con Slim, Paris y Twig

}else{
$search = null;
$query = null;
$params = null;
}
// @2:
if ($formList instanceof FormSearchTypeInterface) {
$search = array(
'form' => $formList->getSearchForm(),
'data' => $search,
'errors' => array(),
);
}else{
$search = array(
'form' => array(),
'data' => array(),
'errors' => array(),
);
}
// @3:
$paginator = new Paginable($ucEntity,array(
'query' => $query,
'params' => $params,
'recPerPage' => 10
));
$paginator->setBaseRouteAndParams('admin.list-entity',
array('entity'=>$entity));
if (($page > 1) && ($page > $paginator->getPages())) {
$app->notFound();
}

$paginator->setCurrentPage($page);
// @4:
$items = $paginator->getResults();
// @5:
$app->render('backend/entity/list.html.twig', array(
'form' => $formList->getFormList(),
'search' => $search,
'items' => $items,
'entity' => $entity,
'paginator' => $paginator,
'searchable'=> ($formList instanceof FormSearchTypeInterface),
'entityName'=> $ucEntity::_entityName(),
));
} else {
$app->notFound();
}

})->via('GET','POST')->name('admin.list-entity');

Ejemplo 47: app/controller/backend/CRUD/list-entity.php

148
10. Añadiendo Twig

Vamos a empezar por el final, que está más cerca. Esta ruta atiende GET y
POST porque la paginación y búsqueda se van a hacer por POST. Aclarado
este punto vamos a ver qué le vamos a mandar a la plantilla. Como ves la línea
que envía los datos a la plantilla es algo más compleja que la que hemos visto
en el punto anterior.

Fíjate en @5: le vamos a mandar form, search, items, entity, paginator,


searchable y entityName, de esta manera con la misma ruta y la misma plan-
tilla podemos ver el listado de varias entidades. ¿El truco? Las entidades que
queremos ver así deben implementar la interfaz Paginable.

@1:
Fíjate que lo primero que hacemos es comprobar si la petición es POST y si
tenemos algún valor pasado en la variable search, ya que esto indica que el
usuario ha introducido un valor en la caja de texto correspondiente.

Vamos a ver la plantilla para que tengas una idea de conjunto.


{% extends 'backend/layout.html.twig' %}

{% set iamhere = 'admin.list-' ~ entity %} {# @6 #}

{% block stylesheets %} {# @7 #}
{{ parent() }}
<style type="text/css">
.center { text-align: center; }
</style>
{% endblock stylesheets %}

{% block subcontent %} {# @8 #}
<div class="row-fl uid">
<div class="span12">
<div>
<legend class="span4">{{ entityName ~ ' list'}}</legend>
{% if paginator.needPagination %} {# @9 #}
{{ paginator_backend_render(paginator) }}
{% endif %}
</div>

{% if searchable is not null and searchable %} {# @10 #}


<div class="span11 accordion">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle btn" data-toggle="collapse"
data-parent="#accordion2" href="#collapseFilter">
<i class="icon-search"></i> Filter
</a>
</div>

149
Programación PHP profesional con Slim, Paris y Twig

<div id="collapseFilter" class="accordion-body collapse">


<div class="accordion-inner">
<form action="#" method="post"
class="form-horizontal">
{% for item in search.form %}
<div class="control-group">
{{ form_search_widget(item,
search.data,
search.errors) }}
</div>
{% endfor %}
<div class="control-group">
<div class="controls">
<button type="submit"
class="btn btn-primary">
buscar
</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endif %}

<table class="table table-striped table-bordered table-condensed">


<thead>
{{ form_table_head(form) }}
<th><span></span></th>
</thead>
<tbody>
{% for item in items %} {# @11 #}
<tr>
{{ form_table_row(form,item) }}
<td>
<a href="{{ urlFor('admin.edit-entity',
{'entity':entity,'id':item.id}) }}"
class="btn btn-primary">
<i class="icon icon-edit icon-white
icon-left">
</i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="3">{{ _('No records found.') }}</td></tr>
{% endfor %}
</tbody>
</table>

150
10. Añadiendo Twig

<div class="well well-small">


{% if paginator.nbRecords == 1 %}
{{ _('Total one record found') }}
{% else %}
{{ sprintf(_('Total %d records found'),
paginator.nbRecords) }}
{% endif %}
{{
search.data|length > 0 ?
' ' ~ _('with specified filter') :
''
}}.
</div>
</div>
</div>
{% endblock subcontent %}

Ejemplo 48: app/templates/backend/entity/list.html.twig

Quiero detenerme un instante en esta plantilla pues aunque no es muy extensa con-
densa muchas de las cosas que hemos visto o veremos. Quiero que te fijes y que
intentes hacer un esfuerzo mental intentando comprender cada una de las líneas.
Voy a hacer este esfuerzo contigo, para que no te sientas perdido.
En la primera línea ves que estamos extendiendo de una plantilla, que es la que
hace de base para todas las del backend. En principio no la abras aún, créeme,
tiene unos cuantos bloques que vamos a ver, y mucho HTML. Repasemos esta
en la que estamos y cuando te encuentres cómodo con ella, podrás mirar la
base y entender lo demás.
A continuación, en @6 estamos definiendo una variable. Esta nos va a servir
para que en el menú del sidebar se nos marque como activa la opción en la que
estamos, que si te fijas no es otra que admin.list-staticpage para este caso,
ya que entity se establece en el controlador que pasa los datos a la plantilla.

¡Muy bien! Veo que vas casando los conceptos. Este uso de variables y clases
activas para indicar el lugar del menú en donde se encuentra el usuario es algo
muy común y te acostumbrarás a verlo y utilizarlo. Ahora viene el bloque de las
hojas de estilo (@7). Como no queremos reescribirlo todo usamos la función
parent() que nos copia todo el contenido anterior del bloque, recuerda que sin
este ardid el bloque quedaría vacio a expensas de lo que incluyamos en la
plantilla hija (la que extiende).
Ahora viene el bloque principal (@8), el que contiene la información real. Me-
diante el condicional de @9 estamos agregando un paginador al listado en el

151
Programación PHP profesional con Slim, Paris y Twig

caso de que sea necesario, o sea, que haya más registros de los que caben en
una página.
Repasando te diré que paginator se le pasa a la plantilla y que es de clase Pagi-
nable, mira la creación en @3, de momento solo te diré que un objeto de clase
Paginable tiene un método needPagination que es el que hemos usado en la
plantilla. Fíjate que el paginador en realidad se pinta utilizando una función de
twig creada por nosotros.
Puedes ver su implementación en la clase PaginatorViewExtension dentro de
\app\models\core\Pagination. No te preocupes ahora por la paginación pues
hay una sección dedicada a esto en la sección “Paginación y búsqueda” en la
página 205.
El siguiente bloque (@10) pinta los campos oportunos para permitir la búsque-
da, para el caso de que la entidad proporcionada cumpla con la condición de
implementar la interfaz searchable.
Mira cómo le pasamos el dato a la plantilla en @5: 'searchable'=> ($formList
instanceof FormSearchTypeInterface). He tratado de que el método en ge-
neral sea muy compacto, por eso me he permitido esa licencia.
Ya que estamos vamos a ver uno por uno los datos que le pasamos a la plan-
tilla en @5:
$app->render('backend/entity/list.html.twig', array(
// formulario propiamente
'form' => $formList->getFormList(),
// objeto búsqueda si lo permite la entidad, sino será null
'search' => $search,
// los registros propiamente a listar
'items' => $items,
// entidad que estamos concretamente listando
'entity' => $entity,
// objeto paginador que nos va a permitir pintarlo
'paginator' => $paginator,
// booleano que indica si la entidad permite búsquedas
'searchable'=> ($formList instanceof FormSearchTypeInterface),
// el nombre de la entidad a efectos estéticos
'entityName'=> $ucEntity::_entityName(),
));

He puesto anotaciones en cada línea para facilitar su comprensión. En principio


el dato más importante es items, aunque lo podíamos haber obtenido directa-
mente en la plantilla haciendo {% set items = paginator.getResult() %}
parece que al tratarse de un dato principal queda más claro mandarlo tal cual
en el render y así no perdemos de vista que se trata de una ruta que lista los

152
10. Añadiendo Twig

registros de una determinada entidad. En el caso que nos ocupa (páginas es-
táticas) no tenemos implementado la interfaz FormSearchTypeInterface, por
eso no nos ha pintado la caja de búsqueda en la captura de la imagen.

Por el momento, vamos a obviar este punto, luego iremos a una entidad que per-
mita búsquedas y lo veremos. ¡Sigamos entonces! Lo que viene a continuación
es la tabla que lista los registros, podemos ver que la cabecera (<thead>) se
pinta con otra función propia {{ form_table_head(form) }}, recuerda que las
funciones de Twig propias se declaran en el archivo lib\TwigViewSlim.php, si lo
revisas verás que la función form_table_head de twig en realidad la implementa
el método estático form_table_head de la clase FormWidget que encontrarás
en la carpeta app\models\core\Form, que vamos a revisar brevemente.

No tienen por qué coincidir los nombres de la función twig con el método de la
clase o función global que la implementa, pero creo que coincidirás conmigo en
que así el código es más fácil de seguir.
/**
* renders the table head of a list of records
*
* @param array $form
* @return html formatted string
*/
public static function form_table_head(array $form)
{
$result = ‘’;
foreach ($form as $item) {
$label = isset($item[‘widget’][‘label’])
? $item[‘widget’][‘label’]
: $item[‘field’];
$type = $item[‘type’];
if ($type==’boolean’) {
$label = ‘<i class=”icon-check”></i>&nbsp;’.$label;
}
$result .= sprintf(‘<th><span>%s</span></th>’,$label);
}
return $result;
}

Ejemplo 49: app\models\core\Form\FormWidget::form_table_head

Veremos este tema cuando hablemos del “Formulario de contacto” en la página


161.

Resumiendo verás que se muestra la etiqueta del campo si está definida y si no


el literal del campo directamente. A destacar que si el tipo del campo es booleano

153
Programación PHP profesional con Slim, Paris y Twig

pintaremos un pequeño checkbox para facilitar la lectura. Date cuenta que a esta
función al final le estamos pasando el objeto formulario (que no deja de ser un
array).

Seguimos con el resto de la tabla. Vamos ahora a por los registros en sí. De
nuevo presta atención a la línea @11, verás que hay un bucle for que recorre
los elementos que tenemos que pintar, recuerda que items contiene los regis-
tros que cumplen con la paginación y la búsqueda (caso de haberla). Hemos
llegado a {{ form_table_row(form,item) }} que pinta la fila, dejando úni-
camente el tema de los botones de acción a expensas del resto del twig. Fíjate
que le estamos pasando el objeto formulario como a la función que pintaba la
cabecera y el item concreto que vamos a pintar.

Como ya hemos visto la anterior esta está en la misma clase, te la muestro


directamente:
/**
* renders the table row of $values record specified
*
* @param array $form
* @param array $values
* @return html formatted string
*/
public static function form_table_row(array $form, $values)
{
$result = '';
foreach ($form as $item) {
$class = isset($item['widget']['attr']['class'])
? $item['widget']['attr']['class']
: '';
$type = $item['type'];
$field = $item['field'];
$value = $values->get($field);
if ($type=="date" || $type=="date-time") {
$date = \DateTime::createFromFormat("Y-m-d h:i:s",$value);
$value = $date->format('d/m/Y'.($type=='date-time'?' h:i:s':''));
}
if ($type=='boolean' && $value) {
$class = "span12 center";
$value = '<i class="icon-check"></i>';
}
/** @var $filter FormFilterInterface */
$filter = isset($item['widget']['filter'])
? $item['widget']['filter']
: null;
if ($filter) {
$value = $filter->filter($value);
}

154
10. Añadiendo Twig

if($value==='') $value="&nbsp;";
$result .= sprintf('<td><span class="%s">%s</span></td>',
$class,$value);
}

return $result;
}

Ejemplo 50: app\models\core\Form\FormWidget::form_table_row

Date cuenta de que hay demasiado acoplamiento entre la función y el código


HTML generado, seguramente para un caso real necesitarás extraer parte del
comportamiento para poderlo configurar: demasiadas clases css. En principio
y por motivos pedagógicos no me voy a extender en este concepto, pero re-
cuerda que si existe mucho código HTML es mejor extrapolar la función a una
macro para tener todo el HTML posible contenido en las plantillas twig. De esta
manera un maquetador sin conocimientos de php puede alterar el contenido a
su gusto sin tocar archivos «sensibles». Un concepto a retener: los bucles for
en las plantillas twig tienen una cláusula condicional {% else %} que permite
mostrar algún mensaje para el caso de que no haya registros que mostrar.
Aunque tiene su propio capítulo te lo menciono aquí para que vayas fijando
ideas: las funciones _(‘texto’) que ves son invocaciones a la función de tra-
ducción de textos, revisa el capítulo 13 para ampliar información al respecto. Al
final de la plantilla nos encontramos un totalizador que vuelve a utilizar el objeto
paginator para mostrar la cantidad de registros mostrados y si son utilizando
algún filtro o no. Para que el mensaje sea más correcto utilizo un condicional
para cerciorarme de que es un único registro o no.
Vamos a ver ahora cómo podemos filtrar el contenido de la lista viendo el lista-
do de otra entidad:

155
Programación PHP profesional con Slim, Paris y Twig

Abordemos ahora la parte que nos habíamos dejado, el condicional que te-
nemos en @10. Recuerda que lo habíamos aparcado momentáneamente y
que dijimos que la entidad correspondiente tenía que implementar la interfaz
FormSearchTypeInterface para poder pintar esa zona, que si ves en la ima-
gen superior, no es más que un botón que desplega un input de texto para la
cadena de búsqueda y un botón para buscar.

Comprobarás enseguida que hace algo más que eso.

Reproduzco de nuevo ese segmento de código para podernos centrar en él:


{% if searchable is not null and searchable %}
<div class="span11 accordion">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle btn" data-toggle="collapse"
data-parent="#accordion2" href="#collapseFilter">
<i class="icon-search"></i> Filter
</a>
</div>
<div id="collapseFilter" class="accordion-body collapse">
<div class="accordion-inner">
<form action="#" method="post" class="form-horizontal">
{% for item in search.form %}
<div class="control-group">
{{ form_search_widget(item,
search.data,
search.errors)
}}
</div>
{% endfor %}
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-primary">
buscar
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endif %}

De nuevo encontramos cosas conocidas: {{ form_search_widget(item,


search.data, search.errors) }}, esta vez es un widget para buscar, en la
misma clase donde se han definido los otros dos. Dejo para tu curiosidad el ver

156
10. Añadiendo Twig

cómo está hecho, aunque este tema se trata en profundidad en el capítulo 14,
apartado «Paginación y búsqueda». A destacar el hecho de que el cuadro de
búsqueda es un formulario que se envía por POST, así no se enturbia la URL y
el arrastre de parámetros cuando paginamos es transparente. Fíjate que se ha
pintado un solo input porque la clase que define cómo ha de ser la búsqueda de
esa entidad solo tiene un campo, pero podrían haber sido varios inputs:
public function getSearchForm()
{

$formBuilder = new FormBase();


return $formBuilder
->add(array('title','description'),
'text', array(
'label' => 'Text',
'op' => 'like',
)
)
->end();
}

Ejemplo 51: app/models/Entity/ArticleFormType.php (getSearchForm)

Conclusión
Con esto hemos visto que las posibilidades de envío de datos desde el contro-
lador a la plantilla son prácticamente ilimitadas y nos dan mucho juego a la hora
de decidir qué es lo que se tiene que mostrar o no.

Como regla general recuerda que es conveniente llamar a las variables de la


misma manera en uno y otro lado. Así mismo es sensato no repetir información
que ya hayamos mandado.

En general yo prefiero objetos complejos que enviar un montón de datos es-


calares, por claridad. En el ejemplo anterior podíamos haber mandado un dato
booleano que fuera ‘needPagination’ => $paginator->needPagination(),
pero este dato ya lo teníamos en el objeto paginator que ya le mandamos a la
plantilla.

Cuestión aparte (que ya te he justificado en su momento) es el contenido princi-


pal de la ruta que son los items a mostrar, tienen categoría suficiente como para
tener su propia variable, pero hubiera sido exactamente igual hacer un {% for
item in paginator->getResult() %} dentro de la plantilla para obtenerlos.

Lo importante de todo es que tú lo tengas claro y que sigas siempre el mismo


criterio, dentro de lo posible. Un criterio único y uniforme.

157
11 Formularios
Contenido
» Introducción
» Formulario de contacto
» Formularios en el backend
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Creo que estamos de acuerdo en que la confección de los formularios es con
diferencia lo más tedioso en el desarrollo de una aplicación en general y web
en particular. Si esto lo unimos al siguiente punto en el temario: la validación,
tenemos el lote completo del tedio.

Ya vimos en el capítulo de Twig que utilizando macros podemos facilitar este


proceso sobremanera.

Ahora vamos a valernos de las interfaces y clases anexas que empezamos a ver
cuando hablábamos del modelo en el punto “Entity_contact” en la página 134.

Antes de empezar viendo los formularios de ejemplo vamos a ver el diagrama


de herencia de las clases e interfaces implicadas.

Como ves algunas de las Entidades que ya vimos implementan también esta
interfaz.
<?php

namespace app\models\core\Form;

interface FormListTypeInterface
{

public function getFormList();

public function getForm();

Ejemplo 52: app/models/core/Form/FormListTypeInteface.php

Esta interfaz obliga a implementar dos métodos que son los que permitirán al
generador de formularios hacer su trabajo. Por un lado, tenemos el método que

160
11. Formularios

configura la lista de registros; y, por otro, el que configura el formulario de un


registro completo, para ver, modificar o crear.

Formulario de contacto
Uno de los formularios que tiene la aplicación es el de contacto. Aquí se trabaja
con una entidad que se persiste en la bbdd. Necesitaremos controlar cuestio-
nes como CSRF y captchas, cosas que no nos dan de base ni Slim ni Twig.
Veamos cómo implementarlos en este caso.

La atención de la ruta de contacto queda definida así:


<?php

/**
* contact form
*/
$app->map('/contact/', function () use ($app) {

$errors = array();
$contact = \Entity\Contact::factory()->create();
// @1:
if ($app->request()->isPost()) {
$sum = $_SESSION[‘sum’];
$contact->bind($app->request()->post());

161
Programación PHP profesional con Slim, Paris y Twig

$errors = $contact->validate();
if (count($errors)==0) {
if ($sum['one']+$sum['two']==$contact->sum) {
$contact->save();
$app->redirect($app->urlFor('.contact.thanks'));
} else {
$errors['sum'] = 'Sum error';
}
}
} else {
$sum = array(
'one' => rand(1,30),
'two' => rand(1,30),
);
$sum['result'] = $sum['one'] + $sum['two'];
$_SESSION['sum'] = $sum;
}
// @2:
$app->render('frontend/contact/index.html.twig',array(
'contact' => $contact,
'sum' => $sum,
'errors' => $errors,
));
})->via('GET','POST')->name('contact.index');

Ejemplo 53: app/controller/frontend/contact.php

Como ya te adelanté en su momento, en el capítulo de Slim, la ruta de contacto


no se define como hasta ahora habíamos visto. He utilizado map y via como
se suele hacer en las rutas que muestran un formulario y luego recogen los
datos por POST para que de esa manera se pueda validar su contenido y en
función de su validez o no presentar los mensajes de error oportunos o tomar
las medidas necesarias para persistir en la bbdd y poder redireccionar a otro
punto de la aplicación.

Veamos ahora el formulario app/templates/frontend/contact/index.html.twig:


{% extends 'frontend/layout.html.twig' %}

{% import 'frontend/macros.twig' as macro %}


{% block subcontent %}

{{ macro.form_styles() }}

<div class="h2 grid_12">&nbsp;</div>


<div class="clear"></div>
<div class="grid_12"><h2>{{ _('Contact form') }}</h2></div>
<div class="clear"></div>

162
11. Formularios

<div class="grid_12" id="contact-form">


<form action="#" method="post">
<fieldset>
<ul>
<li>
<label for="">{{ _('Name') }}:</label>
<input type="text" name="name"
value="{{ contact.name }}"/>
{{ macro.form_error(errors.name) }}
</li>
<li>
<label for="">{{ _('Email') }}:</label>
<input type="text" name="email"
value="{{ contact.email }}"/>
{{ macro.form_error(errors.email) }}
</li>
<li>
<label for="">{{ _('Phone') }}:</label>
<input type="text" name="phone"
value="{{ contact.phone }}"/>
{{ macro.form_error(errors.phone) }}
</li>
<li>
<label for="">{{ _('Message') }}:</label>
<br/>
<label for=""></label>
<textarea name="message" rows=10
cols=50>{{ contact.message }}</textarea>
{{ macro.form_error(errors.message) }}
</li>
<li>
<label for="">{{ _('I\'m human') }}:</label>
{{ sum.one }} and {{ sum.two }} are
<input type="text" size="5" name="sum"
value="{{ contact.sum }}"/>
{{ macro.form_error(errors.sum) }}
</li>
<li>&nbsp;</li>
<li>{{ _('Remember to visit our') }}&nbsp;
<a href="{{ urlFor('home.staticpage',
{'slug':'privacy-policy'}) }}">
{{ _(‘privacy policy’) }}
</a>
.
</li>
<li>
<label for=""></label>
<input type="submit" value="{{ _(‘Send’) }}"/>
</li>
</ul>

163
Programación PHP profesional con Slim, Paris y Twig

</fieldset>
</form>
</div>

<div class="grid_2"><p>&nbsp;</p></div>
<div class="h2 grid_12">&nbsp;</div>

{% endblock %}

Ejemplo 54: app/templates/frontend/contact/index.html.twig

Espero que veas en el código cosas que ya hemos ido tratando a lo largo del
libro. Hay otras en cambio que trataremos en breve, como la internacionalización.
Pero no te preocupes ahora por eso, vamos a centrarnos en lo que sucede con el
formulario. La primera vez (en la petición GET) se pinta el formulario con los datos
que le llegan mediante la instrucción render (ver @2): contact, sum y errors.

Siendo contact un objeto de tipo \Entity\Contact (vacio), errors un array


vacío y sum un array asociativo de dos sumandos (un captcha rudimentario).
De momento no vamos a prestar atención a las instrucciones de validación,
que son las relacionas con el array errors, ya que inmediatamente a este
viene un capítulo sobre validación. Cuando el usuario hace clic sobre el botón
submit del formulario se produce un nuevo ciclo sobre la misma ruta, en esta
ocasión mediante POST, que hace que la condición de @1 se cumpla y pa-
semos a hidratar mediante la orden bind el objeto contact con lo que le ha
llegado de la petición y validamos su contenido. Si todo ha ido bien hacemos
un redirect sobre una ruta de agradecimiento (esta medida es conveniente
para no quedarnos sobre la misma ruta y que por error se produzca un reenvío
de la información). Si ha habido algún error de validación se pinta de nuevo el
formulario pero con los datos recibidos (contact ahora no está vacío) y con los
errores causantes del fallo.

Esta estructura es la que debes seguir para la mayor parte de los formularios
de introducción de información. En casos más concretos, antes de guardar el
registro en la bbdd sanearemos la información que nos llega por parte del usua-
rio, filtrándola de alguna manera.

Vamos a ver cómo podemos automatizar la creación de formularios. Introducien-


do una nueva clase abstracta y haciendo que cada entidad susceptible de tener
un formulario de creación o edición tenga una clase que extienda esa clase.

Por convención, si la clase Contact va a tener un formulario automatizado crea-


remos una clase ContactFormType, que extenderá de FormListTypeInterfa-
ce, y por tanto estará obligado a implementar al menos getFormList y getForm.

164
11. Formularios

Estos dos métodos nos van a permitir indicar en una entidad qué campos se
van a mostrar en el listado y cuáles en el formulario de edición.

Veamos cómo implementamos la clase ContactFormType para la entidad


Contact siguiendo este patrón.
<?php

namespace Entity;

use app\models\core\Form\FormListTypeInterface;
// ...
class ContactFormType implements FormListTypeInterface ...
{
public function getFormList()
{
$formBuilder = new FormListBase();

return $formBuilder
->add('id','text', array(
'label'=>'#',
'attr' =>array(
'class'=>'badge',
),
)
)
->add('created_at','date', array('label'=>'Created At'))
->add('email','text',array('label'=>'Email'))
->add('phone','text',array('label'=>'Phone'))
->add('pending','boolean',array(
'label'=>' ?',
'attr'=>array(
'class' => 'span1',
),
)
)
->end();
}

public function getForm()


{
$formBuilder = new FormBase();

return $formBuilder
->add('id','p', array(
'readonly'=>true,
'attr'=>array(
'class'=>'text-success',
)
)
)

165
Programación PHP profesional con Slim, Paris y Twig

->add('message','textarea')
->end();
}

// ...
}

Ejemplo 55: ContactFormType.php (1)

FormBase es un simple ayudante que permite crear el modelo de formulario al


vuelo. Vamos a ver qué estructura sigue cada uno de los métodos: getFormList
crea un objeto de tipo FormListBase y va añadiendo elementos con distintas con-
figuraciones.
Tienes que verlo como un creador de un array, que permite el paso de ciertos
parámetros vacíos que toman un valor por defecto. Es muy fácil de seguir,
getFormList al final crea una configuración para mostrar las siguientes co-
lumnas: id, created_at, email, phone, pending, con los tipos de datos indi-
cados en cada uno y con unas etiquetas para cada una, esta parte no está inter-
nacionalizada, pero te puedes hacer una idea de lo sencillo que puede ser hacerlo.
Al final, cuando decidamos mostrar este formulario el que lo use simplemente
creará un objeto de tipo ContactFormType y llamando a su getFormList()
obtendrá esa configuración que le permitirá mostrar todo eso en pantalla, pue-
des ver un ejemplo de lo que te digo en el apartado “Parte de administración de
datos” en la página 146.
Para el caso del formulario individual tenemos el método getForm que fun-
ciona exactamente igual solo que su configuración es ligeramente diferente,
pues podemos determinar el tipo de elemento que lo contiene: p, textarea,
etc., así como la clase css que se utilizará y si es de solo lectura, entre otras
cosas.
Estos métodos generan formularios muy mecánicos y normalmente no suelen
ser útiles para la parte frontal, que al ser más cuidada requiere un desarrollo a
medida, pero para la parte de backend o incluso una parte intermedia, como la
intranet de usuario pueden valer perfectamente.

Formularios en el backend
Vamos a ver ahora el caso de un listado de registros y un formulario para la
zona de backend. Esta zona está totalmente automatizada, por lo que encon-
trarás las rutas un tanto abstractas. Como ejemplo vamos a ver el caso de la
entidad Article.

166
11. Formularios

<?php

use lib\MyFunctions;
use app\models\core\BaseModel;
use app\models\core\ValidableInterface;
use app\models\core\Pagination\Paginable;
use app\models\core\Form\FormSearchTypeInterface;
use app\models\core\Search\SearchQueryBuilder;
/**
* crud - generic list for entities that have defined
* Entity\EntityFormType
*/
$app->map('/admin/list/:entity/(:page)', function($entity,$page=1) use ($app) {

$entity = \app\models\core\Sanitize::string(trim(strtolower($entity)));
$ucEntity = \lib\MyFunctions::underscoredToCamelCaseEntityName($entity);
$frmLstClass = $ucEntity."FormType";
if (class_exists($frmLstClass)) {

$formList = new $frmLstClass; //var_dump($formList->getFormList());

if ($app->request()->isPost()) {
$search = $app->request()->post('search');
$qb = new SearchQueryBuilder($formList->getSearchForm(),$search);
$qb->buildQuery();
$query = $qb->getQuery();
$params = $qb->getParams();
}else{
$search = null;
$query = null;
$params = null;
}

if ($formList instanceof FormSearchTypeInterface) {


$search = array(
'form' => $formList->getSearchForm(),
'data' => $search,
'errors' => array(),
);
}else{
$search = array(
'form' => array(),
'data' => array(),
'errors' => array(),
);
}
$paginator = new Paginable($ucEntity,array(
'query' => $query,
'params' => $params,
'recPerPage' => 10
));

167
Programación PHP profesional con Slim, Paris y Twig

$paginator->setBaseRouteAndParams(
'admin.list-entity',
array(‘entity’=>$entity)
);

if (($page > 1) && ($page > $paginator->getPages())) {


$app->notFound();
}

$paginator->setCurrentPage($page);

$items = $paginator->getResults();

$app->render('backend/entity/list.html.twig',array(
'form' => $formList->getFormList(),
'search' => $search,
'items' => $items,
'entity' => $entity,
'paginator' => $paginator,
'searchable'=> ($formList instanceof FormSearchTypeInterface),
'entityName'=> $ucEntity::_entityName(),
));

} else {
$app->notFound();
}

})->via('GET','POST')->name('admin.list-entity');

Ejemplo 56: app/controller/backend/CRUD/list-entity.php

Este es el aspecto que tendría la lista de registros de la entidad Article en el


backend.

168
11. Formularios

Para presentar este formulario internamente estamos teniendo lo mismo


que para el caso anterior, la entidad Article tiene asociada una clase
ArticleFormType .
<?php

namespace Entity;

use app\models\core\Form\FormNewInterface;
use app\models\core\Form\FormListTypeInterface;
use app\models\core\Form\FormListBase;
use app\models\core\Form\FormBase;
use app\models\core\Form\FormSearchTypeInterface;

class ArticleFormType
implements FormListTypeInterface,
FormSearchTypeInterface,
FormNewInterface
{

/**
* tipo de formulario de lista
*
* @return array
*/
public function getFormList()
{

$formBuilder = new FormListBase();

return $formBuilder
->add('id', 'text', array('label'=>'#',
'attr' =>array(
'class'=>'badge'),
))

169
Programación PHP profesional con Slim, Paris y Twig

->add('slug', 'text')
->add('title', 'text', array('label'=>'Title'))
->end();

/**
* tipo de formulario de edición de campos
*
* @return array
*/
public function getForm()
{

$subForm = new FormListBase();

$formBuilder = new FormBase();

return $formBuilder
->add('id', 'p', array('readonly'=>true,
'attr' =>array(
'class' =>'text-success'
))
)
->add('slug', 'text', array('readonly'=>true))
->add('title', 'text', array('label'=>'Title'))

/*
->addSubForm(
$subForm->add('id', 'text')
->add('lang', 'text')
->add('content','text')
->end()
)
*/
->end();

/**
* tipo de formulario de búsqueda para el backend
*
* @return array
*/
public function getSearchForm()
{

$formBuilder = new FormBase();

return $formBuilder

170
11. Formularios

->add(array('title','description'),
'text', array(
'label' => 'Text',
'op' => 'like',
)
)
->end();

Conclusión
No es que los formularios de una aplicación web no se puedan hacer de la
manera tradicional, lo que ocurre es que si uno no es muy metódico en este
sentido se suele dejar cosas como el CSRF, validación de algún campo, pre-
sentación de algún otro, etc.

Utilizar un sistema más o menos automatizado como este, además de siste-


matizar el proceso, permite reutilizar los formularios en otros lugares de la apli-
cación, personalizarlos y llevar un control más exhaustivo. Como siempre ten
en cuenta que lo que aquí se muestra es la base, y admite no pocas mejoras.

De todas maneras, como siempre, de lo que se trata es de que veas una ma-
nera de hacerlo y sobre todo de aprender.

171
12 Validación
Contenido
» Introducción
» Extender BaseModel
» ¿Cómo usar la validación?
» SluggableInterface
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Cualquier aplicación que vaya a aceptar datos de un usuario, ya sea mediante
un formulario o no (API, callbacks, etc.) debe ser capaz de soportar algún sis-
tema de validación.

Esto podemos hacerlo codificando la validación manualmente en el POST de


cada formulario. Pero ya que estamos viendo cómo hacer las cosas de manera
más «profesional» introduciremos el concepto de validación como comporta-
miento de nuestra clase BaseModel.

Recuerda que para poder extender un comportamiento de una clase de un pro-


gramador ajeno a nosotros es mejor (por no decir obligatorio) no tocar nada de
la clase original, y sí extender esta mediante el mecanismo que nos brinda PHP
de herencia. De esta manera, cada cambio que haga el propietario de la clase
no nos afectará en nuestro código cuando actualicemos la aplicación.

Cuando vimos el capítulo del modelo en detalle vimos una por una cada una de
las entidades que integran My-simple-web, pero ahora vamos a centrarnos solo
en la parte de validación de esas clases.

Extender BaseModel
Para permitir la validación vamos a preparar una interfaz y haremos que la
cumplan aquellas entidades que se van a poder introducir por parte del usuario
o admin.
<?php
namespace app\models\core;
interface ValidableInterface
{
public function validate();
}

Ejemplo 57: app/models/core/ValidableInterface.php

Sencillo, ¿verdad? Solo tenemos que implementar un método, que comproba-


remos cuando queramos validar una entidad, ella misma nos dirá si su conte-
nido es válido o no. En principio vamos a devolver un array. Si está vacío es
que la entidad es válida y, si no, contendrá una pareja clave->valor para cada
campo erróneo siendo valor el mensaje que hay que presentar al usuario. Eso
nos permite obtener el resultado de validación de toda la entidad de una sola
vez. Aunque de esta manera tan simplificada no es posible que un campo sea

174
12. Validación

erróneo por varios conceptos porque solo podremos indicar un motivo. Por tan-
to, seremos precisos con las comprobaciones que hagamos.

Vamos a ver cómo hemos implementado ese método en el caso de la entidad


Contact, este es el caso más sencillo:

class Contact extends BaseModel implements ValidableInterface


{
// ...
public function validate()
{
$result = array();
// validar los contenidos de los campos
if (empty($this->name)) {
$result['name'] = Validate::cantLeaveBlank(_('Name'));
}
if (empty($this->email)) {
$result['email'] = Validate::cantLeaveBlank(_('Email'));
}
if (empty($this->phone)) {
$result['phone'] = Validate::cantLeaveBlank(_('Phone'));
}
if (empty($this->message)) {
$result['message'] = Validate::cantLeaveBlank(_('Message'));
}

return $result;
}
// ...
}

Como te comenté: sencillo. Se comprueba que ninguno de los campos se que-


de en blanco. Para facilitar las cosas he agrupado algunos métodos que nos
ayudarán a validar algunos casos comunes dentro de la clase Validate:

<?php

class Validate
{
public static function email($email)
{
return preg_match("|^([\w\.-]{1,64}@[\w\.-]{1,252}\.\w{2,4})$|i", $email);
}

public static function int($int)


{
return ((string) intval($int) == $int);
}

175
Programación PHP profesional con Slim, Paris y Twig

/**
* Return message “$field can’t leave blank”, translated
*
* @param string $field
*
* @return string
*/
public static function cantLeaveBlank($field)
{
return sprintf(_(‘%s can\’t leave blank’),$field);
}

Es muy sencillo: solo hay que pasarle el nombre del campo. Nos devolverá un
mensaje traducido indicando que no se puede dejar el campo x en blanco.

Se podría crear un método diferente para cada tipo de validación, por ejemplo, para
comprobar un correo electrónico tienes el método Validate::mail($field).

Vamos a ver ahora un validador algo más complejo: el de la entidad Article.


public function validate()
{
$result = array();
if(empty($this->slug)) $this->slug = $this->title;
$this->slug = \lib\MyFunctions::slug($this->slug);
if (empty($this->slug)) {
$result['slug'] = $this->cantLeaveBlank(_('Slug'));
} else {
$slugExists = self::checkSlug($this->slug,$this->id);
if ($slugExists) {
$result['slug'] = _('Repeated slug');
}
}
if (empty($this->title)) {
$result['title'] = $this->cantLeaveBlank(_('Title'));
}
if (empty($this->description)) {
$result['description'] = $this->cantLeaveBlank(_('Description'));
}

return $result;
}

176
12. Validación

En este caso además de los ya vistos de no dejar en blanco hemos introducido


un comprobador de slug repetido.

En principio se puede hacer cualquier comprobación, el método validate solo


nos tiene que devolver un array vacío si todo va bien o un array asociativo con
el nombre del campo infractor y su infracción.

¿Cómo usar la validación?


El lugar adecuado para comprobar si una entidad es válida o no es antes de
persistirla en la bbdd, justo cuando nos llegan los datos a través del formulario
que el usuario tiene que rellenar. Comprobaremos que los datos son correctos
invocando a su método validate y en función del resultado persistiremos o no
ese registro en la bbdd.

Te muestro un ejemplo para el caso del formulario de contacto, este patrón ya


lo vimos anteriormente, te lo dejo de nuevo aquí como referencia.

if ($app->request()->isPost()) {
$sum = $_SESSION['sum'];
$contact->bind($app->request()->post());
$errors = $contact->validate();
if (count($errors)==0) {
if ($sum['one']+$sum['two']==$contact->sum) {
$contact->save();
$app->redirect($app->urlFor('.contact.thanks'));
} else {
$errors['sum'] = 'Sum error';
}
}
}

Este fragmento corresponde al código de la ruta que presenta y recoge el for-


mulario de contacto. Lo tienes completo en el Ejemplo 47, en la página 148. Te
he puesto en negrita lo que nos incumbe ahora. Fíjate que primero llamamos
a validate y luego determinamos que no hay errores porque $errors no tiene
elementos (array vacío). Solo entonces verificamos que el CAPTCHA rudimen-
tario de la suma de dos enteros coincide (porque los sumandos no son parte de
la entidad, y no podemos «validarlos» dentro de ella), persistiendo la entidad
mediante save o dando error de suma en caso contrario.

177
Programación PHP profesional con Slim, Paris y Twig

SluggableInterface
Aunque no propiamente una validación tal y como hemos visto en los casos
anteriores, lo cierto y verdad es que permite establecer si se va a aceptar un
slug como válido, normalmente debido al hecho de que ya exista en la bbdd en
otro registro que no es el actual.

Veamos cuál es la interface que lo declara:


<?php

namespace app\models\core;

interface SluggableInterface
{

public static function checkSlug($slug,$id=0);

Ejemplo 58: app/models/core/SluggableInterface.php

La idea como ves es tener una función homogénea que nos permita dar por
bueno ese slug, como te comentaba.

Antes de meternos en profundidad vamos a definir que es eso de slug. Lite-


ralmente traducido viene a ser babosa, pegajoso. La idea es que una URL en
lugar de ser /blog/articulos/1235 sea algo así /blog/articulos/mejorar-mi-ingles.
Creo que no hace falta discutir acerca de cuál de las dos va a ser mejor recor-
dada, y, sobre todo, cuál de las dos va a ser mejor posicionada por los bus-
cadores. ¿Te quedan dudas? Es la segunda. El slug es por tanto la palabra o
palabras que han sustituido al ID. Tradicionalmente todos los parámetros que
necesitaba un determinado php para llevar a cabo su tarea se pasaban en la
ruta con el formato ?par1=val1&par2=val2..&parn=valn, pero con el auge de los
buscadores de Internet este uso se ha dejado poco menos que para restringir
una búsqueda por fechas o por rango.

¿Cuál es el problema de la segunda ruta?


El único que tiene comparada con la primera es que no es un ID, como sabe-
mos los ID son únicos y por tanto irrepetibles, si estamos buscando el artículo
1235 no estamos buscando otro.

Tenemos que garantizar entoces que podamos acceder mediante el slug al


artículo original, es decir, necesitamos una relación biunívoca, de tal manera

178
12. Validación

que solo exista un slug igual a ese, que corresponda con el ID 1235 y que ade-
más sea URL compliant. Esto último es que no tenga caracteres no aceptados
en las rutas, como son: espacios, símbolos extraños como ?, *, #, &, y aunque
se aceptan por los buscadores, cuando tratamos con páginas con un juego de
caracteres latino, ni acentos, tildes ni cedillas.

Lo primero que necesitamos es un método que nos transforme un título en un


slug, está claro que vamos a permitir, antes o después que el usuario modifique
ese slug, porque, no te equivoques, cualquier humano hará esa conversión
mucho mejor. Si dejas que se haga de manera automática y se sustituyen los
caracteres desconocidos por un guion, podrías encontrarte con un slug como
este: espa-a-gan--el--scar, queriendo significar que España ganó el óscar.

Véamos qué función tenemos en My-simple-web, no perdiendo de vista el he-


cho de que esta es una muy primera aproximación a este tratamiento y para
que fuera una función operativa en el 100% de los casos tendría que tener
varias mejoras, te lo dejo como ejercicio.
<?php

namespace lib;

// ...

class MyFunctions
{
// ...
/**
* slugify string passed
*
* @param string $text
*
* @return string
*/
public static function slug($text)
{
$str = trim($text);
$str = str_replace(array(
'ñ','Ñ','ç','Ç',
'á','é','í','ó','ú',
'à','è','ì','ò','ù',
'ä','ë','ï','ö','ü',
'Á','É','Í','Ó','Ú',
'À','È','Ì','Ò','Ù',
'Ä','Ë','Ï','Ö','Ü',
),array(
'n','N','c','C',
'a','e','i','o','u',

179
Programación PHP profesional con Slim, Paris y Twig

'a','e','i','o','u',
'A','E','I','O','U',
'A','E','I','O','U',
'A','E','I','O','U',
),$str);

$str = preg_replace('/[^A-Za-z0-9-]/', '-', $str);


$str = preg_replace('/-+/', "-", $str);
$str = preg_replace('/-$/', '', $str);

return $str;
}

// ...

Ejemplo 59: declaración del método slug en lib/MyFunctions.php

Como te decía utilizaremos este método cuando el formulario de vuelta nos lle-
gue con el slug vacío, utilizando el título del artículo, por ejemplo, para generarlo.

Volvamos a SluggableInterface y veamos quién la utiliza en nuestra aplicación.

Como en otras ocasiones, vamos a tomar la clase Article para ver cómo está
resuelto este método.
<?php

namespace Entity;

// ..

class Article
extends BaseModel
implements SluggableInterface, ValidableInterface
{

/**
* Checks that slug not exists
*

180
12. Validación

* @param string $slug


* @param int $id
* @return bool
*/
public static function checkSlug($slug, $id = 0)
{
$count = self::factory()
->where('slug',$slug)
->where_not_equal('id',$id)
->count();

return $count > 0;


}

// ...
}

Ejemplo 60: fragmento de app/models/Entity/Article.php

Conclusión
Creo que ha quedado explicado de manera bastante clara cómo validar los
contenidos de una entidad, delegando esa comprobación en ella misma, que
es quien debe conocer cómo deben ser sus campos.

Siéntete libre, como todo en My-simple-web de ampliarlo o mejorarlo a tu anto-


jo, esta es la base.

No hemos tratado validaciones complejas como dependencias de campos, qué


pasa si el número de la seguridad social de un alumno ha de tener un valor, pero
solo si el alumno tiene 14 años o más. Un ejercicio sencillo que te dejo para ti.

181
13 i18n:
Internacionalización

Contenido
» Introducción
» Sistema de trabajo
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
Internacionalización es la preparación de una aplicación para que presente los
textos en el idioma elegido por el usuario de entre los soportados. Para ello
cada vez que mostremos en una plantilla o directamente en PHP un texto, uti-
lizaremos una función que convertirá ese texto según el idioma elegido. Hasta
aquí la teoría es sencilla.

My-symple-web está preparado para su internacionalización, hasta ahora no lo


hemos comentado porque el temario incluía este capítulo. Aunque sí que vimos
al hablar al principio de Slim, en el tema del enrutado, que arrastramos todo el
tiempo la variable :lang en las rutas.

No hay muchos sistemas que permitan obtener de una manera eficiente textos
en varios idiomas. Por su sencillez de uso y su facilidad de implementación ya
que tiene soporte nativo en PHP he elegido gettext.

Sistema de trabajo
Para que te hagas una idea, si no conoces esta herramienta, yo utilizo este
sistema:

1) Defino todos los textos en inglés conforme voy desarrollando la aplica-


ción.

2) Cuando utilizo un texto en inglés para presentar al usuario siempre le an-


tepongo el helper _ (underscore) que es un alias de gettext. Utilizo un
subrayado porque pasa desapercibido en las plantillas, ten en cuenta que
lo importante no es la función que traduce sino el texto en sí.

3) Cuando tengo toda la aplicación terminada o he llegado a un hito, lanzo el


programa poEdit y genero las traducciones que necesite, para ello tengo
la siguiente configuración de carpetas:

└── i18n
└── TRANSLATE.md
└── ca_ES
│ └── LC_MESSAGES
│ └── default.mo
│ └── default.po
└── en_GB
│ └── LC_MESSAGES
│ └── default.mo
│ └── default.po

184
13 i18n. Internacionalización

└── es_ES
│ └── LC_MESSAGES
│ └── default.mo
│ └── default.po
└── i18n.php

4) Es necesario probar que los mensajes salen en el idioma adecuado cuan-


do cambio a ese idioma.

Consejo
Cuando actualices el catálogo es posible que no veas los cambios
en la web hasta que no reinicies apache.

5) El proceso sigue porque conforme vamos añadiendo textos nuevos a la


aplicación hay que repetir los pasos.

Utilizando poEdit para traducir nuestros textos:

El archivo i18n.php es el que prepara todo el entorno de PHP para gettext.

185
Programación PHP profesional con Slim, Paris y Twig

<?php

// Bind a domain to directory


// Gettext uses domains to know what directories to
// search for translations to messages passed to gettext
@bindtextdomain('default', dirname(__FILE__).'/');
//die(dirname(__FILE__));
// Set the current domain that gettext will use
@textdomain ('default');

$langs = array (
'es' => 'ES',
'en' => 'GB',
'ca' => 'ES',
);

$code = isset($_REQUEST['lang'])?$_REQUEST['lang']:'es';

if (isset($langs[$code]))
$iso_code = $code.'_'.$langs[$code];
else{
$code = "es";
$iso_code = 'es_ES';
}

if (isset($_SESSION['lang'])) $_SESSION['lang']=$code;

// Set the LANGUAGE environment variable to the desired language


putenv ('LANGUAGE='.$iso_code);
putenv ("LC_ALL=$iso_code");
setlocale(LC_ALL, $iso_code);

Ejemplo 61: i18n.php

Como ves está documentado en las partes esenciales. Básicamente se traduce


el código corto de idioma a ISO. Se le indica a gettext dónde van a estar los
archivos de traducciones. De esta manera cada vez que llamemos a gettext
(o su alias) con una cadena como argumento, nos devolverá la equivalencia en
el idioma que se haya establecido con setlocale.

No recomiendo el uso de @ en absoluto, pero tengo que reconocer que en


algunas ocasiones aclara más el código que varios try-catch encadenados, y
más cuando en realidad no vamos a hacer nada en caso de fallo. El uso de @
en las funciones bindtextdomain y textdomain tiene sentido por el hecho de
que no en todos los sistemas están instaladas las bibliotecas necesarias para
que gettext funcione (icu e intl), y en vez de dar error sin más presentaría los
mensajes en inglés: un mal menor.

186
13 i18n. Internacionalización

En sistemas más grandes conviene tener organizadas todas las traducciones


usando claves, por ejemplo. De esta manera en lugar de utilizar el texto Save
usaríamos la clave form.contact.button.save que indica perfectamente
dónde se va a usar ese texto. Si quisieramos que todos los botones de guardar
tuvieran el mismo texto la clave sería: general.form.button.save. El sistema
de claves es muy efectivo pero como My-simple-web es una aplicación muy
pequeña me he permitido la licencia de escribir directamente los mensajes en
inglés. En todo caso para el programa poEdit es importante que las claves (o
cadenas) estén en formato ASCII pues no reconoce el UTF-8.

Para poder utilizar gettext dentro de las plantillas debemos crear una exten-
sión, esto es así para todas las funciones y fi ltros que no vienen de serie en
Twig. Este punto lo vimos en su momento (página 49). Vamos a repasar aquí
brevemente cómo hacer que gettext esté disponible en las plantillas:
<?php

class TwigViewSlim extends Twig


{
private function addFunctions(…)
{
// …
// i18n
$twigEnvironment->addFunction('_',
new Twig_Function_Function('_',array(
'is_safe' => array('html')
)));
// …

Ejemplo 62: extracto de lib/TwigViewSlim.php

Por simplicidad he quitado todas las declaraciones dejando solo la de nuestra


gettext, en este caso, el alias underscore, por los motivos que te he expuesto
antes. Tienes el código fuente completo en github. A destacar: recordando el
punto de extender Twig, le estamos diciendo a Twig que la función gettext
devuelve código HTML seguro, recuerda que por defecto Twig escapa el con-
tenido de todas las variables y el resultado de las funciones y filtros, este fl ag
le indica a Twig que el resultado no debe ser escapado. Sería como añadirle el
filtro raw al final.

187
Programación PHP profesional con Slim, Paris y Twig

¿Por qué necesitamos esta opción?


Cuando vimos cómo se traducían los mensajes con poEdit, un poco más arriba,
no se te escaparía que los símbolos ajenos al ASCII han de presentarse como
caracteres del juego HTML, como &aacute; &ntilde; etc., si no el archivo de tra-
ducciones no se comporta como se espera. Por ello como lo que nos va a de-
volver la función gettext va a ser por ejemplo Espa&ntilde;e necesitamos que
lo que se muestre al presentar la plantilla sea España y por eso necesitamos el
filtro raw o indicarle a twig que no escape el resultado de gettext.

Conclusión
Aunque no es la única manera de enfocar el tema de la internacionalización, no
he querido liarte con sistemas más complejos y menos generalistas.

Ten en cuenta que gettext puede ser usado de manera nativa por todas las
aplicaciones que se ejecutan en los servidores, de manera que, si utilizas un
sistema así poco te costará aprender variantes.

188
14 Agregando
nuevos componentes

Contenido
» Introducción
» Anotaciones para rutas
» EntityManager
» Paginación y búsqueda
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Introducción
En este capítulo vamos a abordar la parte más complicada del libro: los com-
portamientos deseados que hay que programar de cero ya que no vienen con
los componentes que he elegido para desarrollar My-simple-web, por lo que te
pido que estés muy atento al contenido de este capítulo.

Anotaciones para rutas


Recientemente, mientras escribía este libro, he intentado implementar para un
proyecto las anotaciones de descripción de rutas que utiliza Symfony2, de he-
cho utiliza tres sistemas de definición diferentes, pero el que más me gusta por
su claridad es el de anotaciones. Te explico brevemente en qué consiste para
centrarnos y acto seguido nos pondremos manos a la obra.

Extracto del archivo DemoController.php de Symfony2 al crear un nuevo pro-


yecto.
class DemoController extends Controller
{
// ...

/**
* @Route("/", name="_demo")
* @Template()
*/
public function indexAction()
{
return array();
}

// ...
}

Ejemplo 63: extracto de DemoController.php de Symfony2

Como ves la definición de la ruta viene dada por un comentario «especial» con
el símbolo arroba delante y la palabra Route. Dentro se indica como primer pa-
rámetro la ruta que va a atender ese controlador y cómo la vamos a llamar a ni-
vel lógico para dirigirnos a ella en otras partes de la aplicación, mediante name.

Para simplificar la implementación vamos a reducir el ejemplo anterior, pues


también se indica mediante anotaciones que la plantilla Twig a utilizar como
respuesta es la misma que indique el nombre del bundle, el de la clase y el del
método, algo así como DemoBundle:Demo:index.html.twig.

190
14. Agregando nuevos componentes

Para que esto no nos líe vamos a dejarlo así:


/**
* @Route("/", name="_demo")
*/
public function indexAction()
{
return $this->render('DemoBundle:Demo:index.html.twig',array());
}

Ejemplo 64: DemoController.php de Symfony2 simplificado

Esto ya se parece ligeramente a nuestro modelo de creación de rutas en Slim,


¡comparemos!

La atención de una ruta en Slim puede indicarse de esta manera (larga1).


function home_route_controller($lang='es')
{
$app = \Router\SlimExt::getInstance();
$app->render('frontend/home/index.html.twig');
}
$home_route = $app->get('/(:lang(/))', 'home_route_controller');
$home_route->name('home.index');

Vemos que hay un método que atiende la ruta, una declaración de ruta y un
nombre lógico. Se parece bastante, ¿no?

Te propongo tratar de llegar a este modelo:


<?php

class FrontendController extends Controller


{
/**
* @Route('/')
* @Method('GET')
* @Name('home')
*/
public function home()
{
return $this->render('home.html.twig');
}
// ...
}

1
Hasta ahora hemos visto la declaración compacta de la ruta mediante una clo-
sure y quedando todo en una invocación encadenada de $app->metodo(ruta,
closure)->name(nombre)

191
Programación PHP profesional con Slim, Paris y Twig

La diferencia con Symfony2 es que hemos dejado Route con solo el contenido
de la URL que tiene que atender, hemos creado una nueva anotación llamada
Name, y tenemos otra anotación Method que no habíamos visto en el otro caso
porque tiene un valor por defecto, que es GET, tenemos que reproducir ese
comportamiento, o sea, si no se indica Method será GET.

¿Cómo empezamos?
La verdad es que necesitamos leer el archivo php de manera paralela a lo que
hace el propio intérprete de PHP para analizar las anotaciones y en función de
ellas crear las correspondientes sentencias $app->get, o $app->post, etc.

Como este sistema no parece muy eficiente de nuevo volveremos la cara hacia
Symfony2 para ver cómo lo han resuelto. Utilizaremos un archivo a modo de
caché, que solo regeneraremos cuando las fechas del archivo PHP de origen
y el cacheado difieran.

En el archivo de caché generado introduciremos las órdenes $app->get para que


en realidad con hacer un include sea suficiente para incorporar las rutas a Slim.

Además necesitaremos verificar el sistema con el que Slim invoca las funciones
globales, para que nos permita hacer lo propio con los métodos estáticos de una
clase, y por último crearemos una clase Controller de la cual extenderán las
clases que van a dar soporte a rutas de Slim, una de las ventajas de la que va-
mos a dotar a la clase Controller es la instancia de Slim como una propiedad.

Veamos cómo podemos conjugar todo esto.

En primer lugar la clase Controller:


<?php

abstract class Controller


{
protected $slimInstance;

function __construct()
{
$this->slimInstance = Slim::getInstance();
}

public static function __callStatic($name, $arguments)


{
$calledClass = get_called_class();
$obj = new $calledClass;
call_user_func_array(array($obj, $name), $arguments);
}

192
14. Agregando nuevos componentes

protected function getSlim()


{
return $this->slimInstance;
}
}

Ejemplo 65: Controller.php

Esta clase es la base del resto de controladores y nos va a permitir invocar un


método normal de una clase que extienda de Controller como si fuera estáti-
co, la función __callStatic hará esa «magia», haciendo creer a Slim que está
llamando a un método estático.

Y vamos a poner como propiedad al propio Slim, aunque es fácil recuperarlo


mediante Slim::getInstance(), es muy cómodo y claro que aparezca como
propiedad.

El analizador de anotaciones
A continuación vamos a ver cómo leer el archivo PHP, extraer las cadenas median-
te coincidencia de expresiones regulares y por fin generar las reglas para las rutas.

Al principio pensé en crear un archivo yml al más puro estilo symfony, pero
luego me di cuenta de que no era tan eficiente como crear un archivo PHP
ejecutable directamente y además no es necesario alterar el archivo de caché
generado de manera manual, por lo que da igual el formato.

La misma rutina que hace el análisis crea los archivos de caché y los ejecuta, un
todo en uno que espero que no te produzca demasiado quebradero de cabeza.

El resultado queda plasmado en el siguente diagrama de fl ujo.

Al final solo vamos a mantener un archivo de caché que verificaremos cada


vez que queramos invocar la clase original, serían como los subtítulos en un
archivo de vídeo. En este caso si la fecha del archivo de caché y el de la cla-
se son diferentes, habrá que analizar de nuevo las anotaciones en previsión
de que alguna pudiera haber cambiado, y regenerar de nuevo el archivo de
caché.

Al final se han debido de cargar ambos archivos en el núcleo para que se pue-
dan invocar en el caso de que coincida alguna ruta.

Vamos a ver ahora el código completo del archivo RoutingCacheManager.


php.

193
Programación PHP profesional con Slim, Paris y Twig

Espero que con el diagrama y la explicación anterior te sea fácil de seguir.


Aunque el código es mejorable (a lo cual te invito), es perfectamente funcional.
<?php

namespace Router;

use \Slim\Slim;

class RoutingCacheManager
{
protected $cacheDir;

function __construct()
{
$this->cacheDir = dirname(dirname(dirname(dirname(__FILE__)))) .
'/cache/routing';
@mkdir($this->cacheDir, 0777);
}

/**
* This method writes the cache content into cache file

194
14. Agregando nuevos componentes

*
* @param $class
* @param $content
*
* @return string
*/
protected function writeCache($class, $content)
{
$content = '<?php' . PHP_EOL . PHP_EOL .
'// Generated with RoutingCacheManager' . PHP_EOL . PHP_EOL .
$content . PHP_EOL;
$fileName = $this->cacheFile($class);
file_put_contents($fileName, $content);

return $fileName;
}

/**
* Sets the modify time of cache file according to classfile
*
* @param $classFile
*/
protected function setDateFromClassFile($classFile)
{
$className = $this->className($classFile);
$cacheFile = $this->cacheFile($className);
$fileDate = filemtime($classFile);
touch($cacheFile, $fileDate);
}

/**
* gets the full path and the name of cache file
*
* @param $class
*
* @return string
*/
protected function cacheFile($class)
{
return $this->cacheDir . '/' . $class . '.php';
}

/**
* Extracts the className through the classfile name
*
* @param $classFile
*
* @return mixed
* @throws \Exception
*/

195
Programación PHP profesional con Slim, Paris y Twig

protected function className($classFile)


{
$parts = explode('/', $classFile);
if(count($parts)){
$className = str_replace(".php", "", $parts[count($parts)-1]);
}else{
throw new \Exception(sprintf(
'classFile “%s” passed has problems',
$classFile));
};
return $className;
}

/**
* Indicates if the classfile has a diferent modify time that cache file
*
* @param $classFile
*
* @return bool
*/
protected function hasChanged($classFile)
{
$className = $this->className($classFile);
$cacheFile = $this->cacheFile($className);
$cacheDate = file_exists($cacheFile) ? filemtime($cacheFile) : 0;
$fileDate = filemtime($classFile);

return ($fileDate != $cacheDate);


}

/**
* @param $classFile
*
* @return string
* @throws \Exception
*/
protected function processClass($classFile)
{
$className = '';
$content = file_get_contents($classFile);
$result = '';
preg_match_all('/class\s+(\w*)\s*(extends\s+)?([^{])*/s',
$content, $mclass, PREG_SET_ORDER);
$className = $mclass[0][1];
if (!$className){
throw new \Exception(sprintf('class not found in %s', $classFile));
}

preg_match_all('/(\/\*\*[^}]*})/', $content, $match, PREG_PATTERN_ORDER);

196
14. Agregando nuevos componentes

foreach ($match[0] as $k => $m) {


$function = '?';
$comments = '';
if (!substr_count($m, 'class')) {
$function = substr_count($m, ‘function’) ? 'yes' : 'no';
if ($function == 'yes') {
preg_match_all('/(\/\*\*.*\*\/)/s', $m, $mc,
PREG_PATTERN_ORDER);
$comments = nl2br($mc[0][0]);
preg_match_all('/\*\/\s+(public\s+)?' .
'(static\s+)?function\s+([^\(]*)\(/s',
$m, $mf, PREG_SET_ORDER);
$functionName = $mf[0][3];
preg_match_all("/\*\s+@Route\s*\('([^']*)'\)/s",
$comments, $params, PREG_SET_ORDER);
$route = $params[0][1];
preg_match_all("/\*\s+@Method\s*\('([^']*)'\)/s",
$comments, $params, PREG_SET_ORDER);
$method = isset($params[0][1])
? strtoupper($params[0][1])
: 'GET';
preg_match_all("/\*\s+@Name\s*\('([^']*)'\)/s",
$comments, $params, PREG_SET_ORDER);
$name = strtolower($params[0][1]);
$result .= sprintf(
'$app->map("%s", "%s::___%s")' .
'->via("%s")->name("%s");' . PHP_EOL,
$route, $className, $functionName,
str_replace(',','","',$method), $name
);
}
}
}
return $result;
}
/**
* Generates new file and return cachefile name
* @param $classFile
*
* @return string
*/
protected function updateAndGetCacheFileName($classFile)
{
$className = $this->className($classFile);
if($this->hasChanged($classFile)){
$content = $this->processClass($classFile);
$this->writeCache($className, $content);
$this->setDateFromClassFile($classFile);
}

197
Programación PHP profesional con Slim, Paris y Twig

return $this->cacheFile($className);
}

/**
* Return cachefile contents
*
* @param $classFile
*
* @return string
*/
protected function getCache($classFile)
{
return file_get_contents($this->updateAndGetCacheFileName($classFile));
}

/**
* Process the cachefile, in PHP require is enough
*
* @param $classFile
*
* @throws \Exception
*/
protected function processCache($classFile)
{
require_once($this->updateAndGetCacheFileName($classFile));
}

/**
* Main method to invoke the routing system
*
* @param $phpFile
*/
public function loadRoute($phpFile)
{
require_once $phpFile;
$this->processCache($phpFile);
}

Ejemplo 66: RoutingCacheManager.php

Verás que no he utilizado el método de creación de rutas que hemos visto con
más regularidad sino aquel que se debe usar para los formularios, que veremos
más adelante. Te hago un pequeño adelanto:
$app->map('nombre/ruta/fisica', closure)->via(methods)->name('nombre_logico');

Lo que hacemos es mapear una determina ruta via unos determinados méto-
dos. De esta manera, la caché de ruteo sin saber de antemano qué métodos

198
14. Agregando nuevos componentes

va a permitir en una ruta es más compacta. En el caso de que Slim no hubiera


implementado el método map, tendríamos que haber hecho un switch bastante
curioso. Te lo dejo como ejercicio.

La inclusión en el núcleo de Slim


No hay que hacer nada en el núcleo de Slim pues él ya se encarga de hacer la
llamada a nuestro método estático falso que al final crea una clase e invoca al
método adecuado, de manera transparente para Slim.

Vamos a ver el resultado de la caché al procesar las rutas que ya están prepa-
radas con las anotaciones:
<?php

// Generated with RoutingCacheManager


$app->map("/test/one", "TestAnnotationController::___test1Action")->
via("GET")->name("test1");
$app->map("/test/hello/:name", "TestAnnotationController::___testHelloAction")->
via("GET")->name("test_hello");
$app->map("/test/redirect", "TestAnnotationController::___testRedirectAction")->
via("GET")->name("test_redirect");

Ejemplo 67: app/cache/routing/TestAnotationController.php

EntityManager
Aunque la idea está importada de otros frameworks quizás el nombre no sea el
más adecuado, pues parte de la funcionalidad del EntityManager ya la realiza
el ORM.

Para centrar las ideas lo que vamos a implementar es un gestor que nos permi-
ta regenerar la base de datos, pretendendiendo llegar a este modelo de código
compacto:
<?php

//..
$em = new \app\models\EntityManager(true);

$em->dropDatabase();
$em->createDatabase();
$em->createTables();
$em->generateFixtures();

Ejemplo 68: regenerateDBwithoutConfirmation.php

199
Programación PHP profesional con Slim, Paris y Twig

Creo que se ve claramente la intención de este tipo de manager. Obviamente


tendremos que generar la clase EntityManager que nos provea de esos méto-
dos. Como vas a poder constatar enseguida la información de las clases Model
se obtiene del mismo archivo php donde se declara la clase. Observa los mé-
todos que hay para leer el directorio y demás.
<?php

namespace app\models;

use \ORM;
use \app\models\core\Registry;
use \app\models\core\FixturableInterface;
class EntityManager
{
private $conn;
private $dbname;
private $dump;

public function __construct($dump = false)


{
require_once dirname(dirname(__FILE__)).'/config/dbconfig.php';
$this->dump = $dump;
$this->dbname = DBNAME;
$this->conn = mysql_connect(DBHOST,DBUSER,DBPASS,$this->dbname);
ORM::configure('mysql:host='.DBHOST.';dbname='.$this->dbname);
ORM::configure('username', DBUSER);
ORM::configure('password', DBPASS);
}

/**
* Execs the sql statement passed
*
* @param $sql
*
* @return resource
*/
public function execute($sql)
{
if ($this->dump) {
print $sql.PHP_EOL;
}
$result = mysql_query($sql,$this->conn);
if (mysql_errno($this->conn)) {
die($sql.PHP_EOL.mysql_error($this->conn).PHP_EOL);
}

return $result;
}

200
14. Agregando nuevos componentes

/**
* Reads the contents of this dir and returns only dirs
* that have first letter capitalized
*
* @return array
*/
protected static function readdir()
{
$entries = array();
foreach (scandir(__DIR__) as $entry){
if ($entry!='.' && $entry!='..' && is_dir(__DIR__.'/'.$entry)) {
if ($entry == ucfirst($entry)) {
$entries[] = $entry;
}
}
}

return $entries;
}

/**
* Forces the load of classes contained in this dir
*
* @return void
*/
public static function forceRequireEntityClasses()
{
$dirs = self::readdir();
foreach ($dirs as $dir) {
$subdir = __DIR__.'/'.$dir;
$files = scandir($subdir);
foreach ($files as $file) {
// only the php files that has the first letter capitalized
if ($file!='.' && $file!='..' && preg_match('/\.php$/i',$file)) {
if ($file == ucfirst($file)) {
require_once $subdir.'/'.$file;
}
}
}
}
}

/**
* Drops database
*/
public function dropDatabase()
{
$sql = sprintf('DROP DATABASE IF EXISTS `%s`;',$this->dbname);
$this->execute($sql);
}

201
Programación PHP profesional con Slim, Paris y Twig

/**
* Create database
*/
public function createDatabase()
{
$sql = sprintf('CREATE DATABASE IF NOT EXISTS `%s`;', $this->dbname);
$this->execute($sql);
}

/**
* Select Database
*/
public function selectDb()
{
mysql_select_db($this->dbname);
}

/**
* Create tables
*/
public function createTables()
{
self::forceRequireEntityClasses();
$this->selectDb();
// get entity classes that extends from BaseModel
$classes = get_declared_classes();
foreach($classes as $class){
if (is_subclass_of($class,'app\\models\\core\\BaseModel')) {
if (method_exists($class,'_creationSchema')) {
if ($this->dump) {
print $class.PHP_EOL;
}
//$sql = $class.'::_creationSchema';
$sql = $class::_creationSchema();
$this->execute($sql);
}
}
}
}

/**
* Generate fixtures for all entities
*/
public function generateFixtures()
{
self::forceRequireEntityClasses();
$ordered = array();
// get entity classes that extends from FixturableInterface
$classes = get_declared_classes();
foreach($classes as $class){
//print $class.PHP_EOL;

202
14. Agregando nuevos componentes

if (is_subclass_of($class,'app\\models\\core\\FixturableInterface')) {
//print 'order '.$class::getOrder().’ ‘;
$ordered[sprintf("%05d-%s",$class:getOrder(),$class)] = $class;
}
}
$this->selectDb();
ksort($ordered);
print PHP_EOL;
$fixtureRegistry = new Registry();
foreach($ordered as $order=>$class){
if ($this->dump) {
print $order.PHP_EOL;
}
/** @var FixturableInterface $fixtureClass */
$fixtureClass = new $class;
$fixtureClass->generateFixtures($fixtureRegistry);
}
}
}

Ejemplo 69: app/models/EntityManager.php

El método createTables se encarga de crear la tabla correspondiente a la clase


que se está analizando. Para ello la clase debe implementar un método estático
llamado _creationSchema, que devuelve la sentencia SQL que genera la tabla.

Veamos esto para una clase en concreto, Article.php:


<?php

namespace Entity;

use ...;

/**
* Class that stores articles of this web
*/
class Article extends BaseModel implements ...
{
// ...

/**
* Get the SQL creation sentece of this table
*
* @param array $options
* @return string
*/
public static function _creationSchema(Array $options = array())
{

203
Programación PHP profesional con Slim, Paris y Twig

$class = self::_tableNameForClass(get_called_class());

// default options
$options = array_merge(self::_defaultCreateOptions(),$options);

return

<<<EOD

CREATE TABLE IF NOT EXISTS `{$class}` (


`id` bigint(11) NOT NULL AUTO_INCREMENT,
`slug` varchar(100) NOT NULL,
`title` varchar(100) NOT NULL,
`description` text NOT NULL,
`description_id` bigint(11) NOT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE={$options['engine']}
DEFAULT CHARSET={$options['charset']} AUTO_INCREMENT=1 ;

EOD;

// ...
}

Ejemplo 70: fragmento de app/models/Entity/Article.php

Como ves es sencillo: recorremos la lista de clases creada y vamos ejecutando


su correspondiente método estático para poder crear la tabla.
Te habrás dado cuenta de que también hay un generador de fixtures que per-
mite establecer la base de datos en un punto de partida conocido. Después de
recrear las tablas se llenan de los valores fijos correspondientes.
En principio podrías pensar en por qué no sencillamente hacer un dump y un
restore de la base de datos. El resultado sería similar pero el mantenimiento
resultaría más complicado.
Estaríamos hablando entonces de un archivo SQL nativo. De la manera en que
se ha explicado, cada clase conoce su propio método de creación y podemos
por ejemplo recuperar una tabla en caso de desastre. Sin necesidad de recu-
rrir a un restore manual o automatizado. Si detectamos un error en el código
relacionado con la base de datos podemos comprobar mediante este mismo
método si las tablas existen y si su sentencia de creación coincide con la que
existe en la clase. Las posibilidades de esta manera son más amplias.

204
14. Agregando nuevos componentes

Paginación y búsqueda
No es que sean lo mismo, pero te darás cuenta, por la manera en que hay que
resolver la cuestión de que van intimamente ligadas. Pues podemos paginar
pero además hay que mantener el filtrado de la búsqueda.

Veamos en primer lugar cómo empezar a obtener los registros paginados des-
de la bbdd. Evidentemente hay que hacer uso de las opciones LIMIT y OFFSET
que nos brinda SQL. Pero necesitamos algo más, lo normal es saber cuántos
registros hay en total, cuántos registros queremos por página y de ahí calcular
cuantas páginas hay en total.
Presentaremos un paginador con este estilo:

¡Bien! Vamos a ver los cálculos, cómo traducir esto a instrucciones para el
ORM y cómo presentar los datos mediante la plantilla. ¡Cógete fuerte a la silla!
<?php

define('REG_POR_PAG', 10);

$app->get('/lista-clientes', 'lista_clientes');
function lista_clientes()
{
$app = \Slim\Slim::getInstance();
$request = $app->request();
$currentPage = $request->get(‘pa’) ?: 1;

// obtengo el numero de registros


$numTotalClientes = Model::factory(‘Cliente’)->count();

$numPaginas = ceil($numTotalClientes / REG_POR_PAG);

$firstRecord = REG_POR_PAG * $currentPage;

$clientes = Model::factory(‘Cliente’)
->offset($firstRecord)
->limit(REG_POR_PAG)
->find_many();
$app->render('clientes.html.twig', array(
'clientes' => $clientes,
'pa' => $currentPage,
'numPaginas' => $numPaginas,
));
}

Ejemplo 71: Primera aproximación a la paginación

205
Programación PHP profesional con Slim, Paris y Twig

Así llamando a la ruta /lista-clientes?pa=10, obtendríamos la página 10


de los clientes. Hay muchas cosas que pulir en este ejemplo: comprobar condi-
ciones de error para el caso de que no existan registros, o la página pedida no
exista, controlar páginas en negativo.

Lo que tiene de malo esta primera aproximación es que para cada uno de los
listados que queramos tener paginados (en teoría todos), debemos copiar este
modelo, y créeme, no va a ser tan sencillo cuando añadamos la búsqueda.

Solo quiero hacerte ver cuál es la dinámica, ya que la idea es extrapolar este
modelo y llegar a conseguir que llamar a un paginador sea tan sencillo como
hacer esto:
$paginator = new Paginable("Cliente",array('recPerPage' => 10));
$paginator->setBaseRouteAndParams('lista-clientes', array(‘entity’=>cliente));
if (($page > 1) && ($page > $paginator->getPages())) {
$app->notFound();
}

Antes de ponermos manos a la obra vamos a ver cómo integraríamos en el


ejemplo anterior el tema de las búsquedas, pues como ya te he comentado,
van las dos cosas ligadas y es conveniente resolverlo de manera coordinada.
<?php

define('REG_POR_PAG', 10);

$app->get('/lista-clientes', 'lista_clientes');
function lista_clientes()
{
$app = \Slim\Slim::getInstance();
$request = $app->request();
// conviene sanear la entrada
$search = $request->get('search');
$currentPage = $request->get('pa') ?: 1;
if(!substr_count($search, '%')){
$search = '%' . $search . '%';
}

// obtengo el numero de registros


$numTotalClientes = Model::factory('Cliente')
->where_like('nombre', $search)
->count();

$numPaginas = ceil($numTotalClientes / REG_POR_PAG);

$firstRecord = REG_POR_PAG * $currentPage;

$clientes = Model::factory('Cliente')
->offset($firstRecord)

206
14. Agregando nuevos componentes

->limit(REG_POR_PAG)
->where_like('nombre', $search)
->find_many();

$app->render('clientes.html.twig', array(
'clientes' => $clientes,
'pa' => $currentPage,
'numPaginas' => $numPaginas,
));

Ejemplo 72: paginacion_busqueda.php

La solución que te he planteado es muy rudimentaria y solo vale para un cam-


po, pero se trata de que veas la idea. A destacar que hay que hacer limpieza de
todo aquello que nos llegue del usuario y vayamos a hacer algo contra la bbdd,
como el caso de $search.

Por tanto, hemos visto que el algoritmo se complica, tenemos que conservar
el where tanto en la obtención de datos, como en la cuenta de registros para
calcular el número de páginas totales.

Esto para el caso de una sola condición, podemos querer buscar por nombre,
teléfono, email, etc.

Por este motivo, vamos a crear unas interfaces que nos permitan dotar a las
clases de comportamientos especiales en los casos de paginación y búsqueda.

Empecemos por app/models/core/Pagination/PaginableInterface:


<?php

namespace app\models\core\Pagination;

interface PaginableInterface
{
/**
* Generates a paginator from the ORMWrapper specified
* with ten records per page as default
*
* @param string $entity
* @param array $_options
*/
public function __construct($entity, $_options = array());

/**
* Returns the items for the page selected

207
Programación PHP profesional con Slim, Paris y Twig

*
* @return ORM ArrayCollection
*/
public function getResults();

/**
* Set the records per page desired
*
* @param int $num
*/
public function setNumRecPerPage($num);

/**
* sets the current page
*
* @param $page
*/
public function setCurrentPage($page);

/**
* obtains total page number
*
* @return mixed
*/
public function getPages();

/**
* obtains the current page
* @return mixed
*
*/
public function getCurrentPage();

/**
* Set the base route and params to generate route
* for each page
*
* @param $route
* @param $params
*/
public function setBaseRouteAndParams($route, $params);

/**
* returns the route for specified page
*
* @param int $num
* @return string
*/
public function getRouteForPage($num);

208
14. Agregando nuevos componentes

/**
* returns true if number of records is greater
* than recPerPage
*
* @return bool
*/
public function needPagination();

/**
* returns true if currentPage is not the first
*
* @return bool
*/
public function hasPreviousPage();

/**
* returns the number of previous page
*
* @return int
*/
public function getPreviousPage();

/**
* returns true if currentPage are not the last
*
* @return int
*/
public function hasNextPage();

/**
* returns the number of the next page
*
* @return int
*/
public function getNextPage();

Ejemplo 73: app/models/core/Pagination/PaginableInterface.php

No voy a reproducir todo el código de app/models/core/Pagination/Paginable


por que lo tienes en github, pero iré poniendo aquellas partes más importantes o
complejas para comentarlas. Esta interfaz nos da una idea del contrato que tiene
que cumplir Paginable. Veamos, como te he comentado, los sitios donde está la
chicha:
public function __construct($entity, $_options = array())
{

$options = array_merge(array(

209
Programación PHP profesional con Slim, Paris y Twig

'query' => null,


'params' => null,
'recPerPage"=> 10,
),$_options);
$this->entity = $entity;
$this->query = $options[‘query’];
$this->params = $options[‘params’];

if ($this->query && $this->params) {


$this->nbRecords = BaseModel::factory($entity)
->where_raw($this->query,$this->params)
->count();
}else{
$this->nbRecords = BaseModel::factory($entity)->count();
}
$this->setNumRecPerPage($options['recPerPage']);
}

Tenemos una configuración por defecto: no hay query (búsqueda), y una can-
tidad de 10 registros por página, es importante partir de unos datos conocidos,
por eso array_merge es una instrucción muy útil para juntar las opciones junto
con parámetros básicos.

Como puedes ver en el constructor de la clase se crea directamente la query


que permite conocer el número de registros totales en función de la consulta
que queremos hacer, optimizándola con un IF para distinguir el caso de tener
búsqueda o no. Hasta ahora igual que habíamos visto en el ejemplo. ¿Por qué
podemos hacer esta query a priori en el constructor? Lo único que necesitamos
para saber el número total de registros es la condición de búsqueda si la hay,
por tanto como el constructor dispone de esa información calculamos el nú-
mero total de registros aquí y ya no tenemos que preocuparnos por ese factor.

Ahora le ha tocado el turno al método que nos devuelve los registros de la se-
lección, tanto a nivel de página actual como de búsqueda si la hay.
public function getResults()
{

if (($this->page>0) && ($this->recPerPage > 0)){


$start = ($this->page-1) * $this->recPerPage;
if ($this->query && $this->params) {
$result = BaseModel::factory($this->entity)
->where_raw($this->query,$this->params)
->offset($start)
->limit($this->recPerPage)
->find_many();

return $result;

210
14. Agregando nuevos componentes

}else{
return BaseModel::factory($this->entity)
->offset($start)
->limit($this->recPerPage)
->find_many();
}
}
}

De nuevo consideramos un condicional para el caso de que haya filtro o no.


Por lo demás es una condición como la del "Ejemplo 71: Primera aproximación
a la paginación". Lo único es que la instrucción where es en bruto porque no
conocemos de antemano qué campos vamos a querer filtrar, además de que
en la mayor parte de los casos la condición será una O para el caso de que
exista más de un campo por el que filtrar, y la única manera de hacer un where
con un OR es con where_raw o con raw_query2. En todo caso quédate con
que al final no es más que una consulta como la que podríamos hacer a mano
utilizando el ORM, acotando los resultados a una página.

Un método que nos va a ser de utilidad cuando utilicemos esta clase va a ser
el de saber cual es la URL que nos conduce a la página anterior o a la página
siguiente, para cuando tengamos que pintar un paginador en pantalla y facilitar
al usuario la navegación.

Por eso los métodos que vienen a continuación los tienes que ver en conjun-
to teniendo en mente este fin. Primero establecemos una ruta base, que va a
admitir un parámetro página obligatoriamente además de los que los paráme-
tros que admita de base. Imagínate que tenemos este tipo de ruta: /articulos/
index/:year/:month/:day/:page, como puedes deducir la función que atienda esta
ruta va a tener cuatro parámetros así: function loquesea($year,$month,
$day,$page), nos interesa tener claros los parámetros fijos y poder variar la
página en función de si estamos avanzando o retrocediendo en la navegación.

Veámos el código:
public function setBaseRouteAndParams($route, $params)
{
$this->route = $route;
$this->routeParams = $params;
}

public function getRouteForPage($num)


{
/** @var Slim $app */
$app = Slim::getInstance();

2
Como ya vimos en el capítulo dedicado al modelo.

211
Programación PHP profesional con Slim, Paris y Twig

$params = array_merge(array(‘page’=>intval($num)),$this->routeParams);

return $app->urlFor($this->route, $params);


}
Mira cómo usamos todo lo expuesto en el caso del listado:
$app->get('/:lang/articles(/:page)', function ($lang, $page=1) use ($app) {

$paginator = new Paginable('Entity\\Article', array('recPerPage' => 2));


// @1:
$paginator->setBaseRouteAndParams('articles.index');
if (($page < 1) || ($page > $paginator->getPages())) {
$app->notFound();
}
$paginator->setCurrentPage($page);
$articles = $paginator->getResults();

$app->render('frontend/articles/index.html.twig',array(
'articles' => $articles,
'paginator' => $paginator,
));

})->name('articles.index');

Ejemplo 74: articles.php

Como puedes apreciar en el código, el uso de setBaserouteAndParams ins-


truye a $paginator para que las URL que genere para la página anterior y
siguiente sean en base a la ruta articles.index, en este caso sin parámetros
adicionales, salvo el número de la página que se completará después, en fun-
ción de si avanzamos o retrocedemos.
Esta es la forma en la que pondríamos un paginador en una plantilla Twig:
<div>
<legend class="span4">{{ entityName ~ ' list'}}</legend>
{% if paginator.needPagination %}
{{ paginator_backend_render(paginator) }}
{% endif %}
</div>

Ejemplo 75: fragmento de app/templates/backend/entity/list.html.twig

Como puedes apreciar en el código existe una extensión Twig para pintar el
paginador, veámosla:
class TwigViewSlim extends Twig
{
private function addFunctions(\Twig_Environment $twigEnvironment)
{

212
14. Agregando nuevos componentes

// ...
$twigEnvironment->addFunction('paginator_backend_render',
new Twig_Function_Function(
'\app\models\core\Pagination\PaginatorViewExtension::render',
array(
'is_safe' => array('html')
)
));
// ...
}

// ...
}

Ejemplo 76: declaracion de paginator_backend_render en lib/TwigViewSlim.php

Y la clase donde se declara ese método:


<?php

namespace app\models\core\Pagination;

use app\models\core\Pagination\PaginableInterface;
use app\models\core\Pagination\PaginationRender;

class PaginatorViewExtension
{

/**
* Shows the paginator
*
* @param \app\models\core\PaginableInterface $paginator
* @return mixed
*/
public static function render(PaginableInterface $paginator)
{
$pagination = new PaginationRender($paginator);
return $pagination->render();
}

Ejemplo 77: app\models\core\Pagination\PaginatorViewExtension

Recuerda la dinámica: la funciones Twig se asocian a la clase y método que las


va a resolver en el archivo lib/TwigViewSlim.php.

Al final, PaginatorViewExtension crea un PaginationRender.

213
Programación PHP profesional con Slim, Paris y Twig

<?php

namespace app\models\core\Pagination;

use app\models\core\Pagination\PaginationRenderInterface;
use app\models\core\Pagination\Paginable;

class PaginationRender implements PaginationRenderInterface


{
private $options;
private $paginator;

public function __construct(Paginable $paginator)


{
$this->paginator = $paginator;
$this->setOptions();
}

public function setOptions(array $options = array())


{
$this->options = array_merge(
array(
'proximity' => 3,
'prev_message' => '&larr; Previous',
'prev_disabled_href' => '',
'next_message' => 'Next &rarr;',
'next_disabled_href' => '',
'dots_message' => '&hellip;',
'dots_href' => '',
'css_container_class' => 'pagination spain8 pagination-right',
'css_prev_class' => 'prev',
'css_next_class' => 'next',
'css_disabled_class' => 'disabled',
'css_dots_class' => 'disabled',
'css_active_class' => 'active',
),
$options
);
}

public function render()


{

$page = $this->paginator->getCurrentPage();
$numpages = $this->paginator->getPages();

$startPage = $page - $this->options[‘proximity’];


$endPage = $page + $this->options[‘proximity’];

if ($startPage < 1) {
$endPage = min($endPage + (1 - $startPage), $numpages);

214
14. Agregando nuevos componentes

$startPage = 1;
}
if ($endPage > $numpages) {
$startPage = max($startPage - ($endPage - $numpages), 1);
$endPage = $numpages;
}

$pages = array();

// previous
$class = $this->options['css_prev_class'];
$url = $this->options['prev_disabled_href'];
if (!$this->paginator->hasPreviousPage()) {
$class .= ' '.$this->options['css_disabled_class'];
} else {
$url = $this->paginator->getRouteForPage(
$this->paginator->getPreviousPage()
);
}

$pages[] = sprintf('<li class="%s"><a href="%s">%s</a></li>',


$class, $url, $this->options['prev_message']);

// first
if ($startPage > 1) {
$pages[] = sprintf('<li><a href="%s">%s</a></li>',
$this->paginator->getRouteForPage(1), 1);
if (3 == $startPage) {
$pages[] = sprintf('<li><a href="%s">%s</a></li>',
$this->paginator->getRouteForPage(2), 2);
} elseif (2 != $startPage) {
$pages[] = sprintf('<li class="%s"><a href="%s">%s</a></li>',
$this->options['css_dots_class'],
$this->options['dots_href'],
$this->options['dots_message']);
}
}

// pages
for ($i = $startPage; $i <= $endPage; $i++) {
$class = '';
if ($i == $page) {
$class = sprintf(' class="%s"', $this->options['css_active_class']);
}
$pages[] = sprintf('<li%s><a href="%s">%s</a></li>',
$class, $this->paginator->getRouteForPage($i), $i);
}

// last
if ($numpages > $endPage) {

215
Programación PHP profesional con Slim, Paris y Twig

if ($numpages > ($endPage + 1)) {


if ($numpages > ($endPage + 2)) {
$pages[] = sprintf('<li class="%s"><a href="%s">%s</a></li>',
$this->options['css_dots_class'],
$this->options['dots_href'],
$this->options['dots_message']);
} else {
$pages[] = sprintf('<li><a href="%s">%s</a></li>',
$this->paginator->getRouteForPage($endPage+1),
$endPage + 1);
}
}

$pages[] = sprintf('<li><a href="%s">%s</a></li>',


$this->paginator->getRouteForPage($this->pages),
$this->pages);
}

// next
$class = $this->options['css_next_class'];
$url = $this->options['next_disabled_href'];
if (!$this->paginator->hasNextPage()) {
$class .= ' '.$this->options['css_disabled_class'];
} else {
$url = $this->paginator->getRouteForPage(
$this->paginator->getNextPage());
}

$pages[] = sprintf('<li class="%s"><a href="%s">%s</a></li>',


$class, $url, $this->options['next_message']);

return sprintf('<div class="%s"><ul>%s</ul></div>',


$this->options['css_container_class'],
implode('', $pages));
}

Repasa bien el código, he puesto pequeños comentarios para que se vea qué
sección estamos tratando.

El código HTML correspondiente que se pintará para cada uno de los elemen-
tos queda parametrizado por las opciones que podemos inyectar a la clase.

Los nombres de las diferentes opciones son suficientemente explicativos de su


comportamiento.

No es muy bueno que el código HTML esté integrado dentro de las clases PHP,
en este caso se dan dos eximentes: estamos declarando un extensión de Twig,

216
14. Agregando nuevos componentes

y luego, lo hemos parametrizado todo tanto que en realidad el código HTML


queda muy difuso.

Eso sí, el tipo de paginador que mostraría sería una lista desordenada (ul) con
elementos (li) para cada página.

Esto nos es útil si queremos utilizar los css de twitter bootstrap, pero si que-
remos implementar nuestro propio paginador con elementos div por ejemplo
tendremos que cambiar parte del código.

Este tipo de paginadores también son susceptibles de ser implementados como


macros en Twig. Te lo dejo como ejercicio.

Y el resultado en la vista quedaría así:

Conclusión
Hasta aquí en lo concerniente a la incorporación de nuevos componentes a la
aplicación My-simple-web. Es posible que te hayas sentido un tanto confuso
con la cantidad de código que te he mostrado.

Si es así, antes de continuar intenta repasar de nuevo todo teniendo el código


de github a la vista en tu ordenador y entendiendo bien lo que hace cada parte.

En todo caso ten presente que lo que aquí se te ha mostrado es un ejemplo,


falta todo aquello que en un desarrollo comercial pudieras necesitar: RSS, ge-
neración de sitemap.xml dinámicamente, migración de bbdd, búsquedas avan-
zadas mediante Lucene, etc.

En la medida en que el código de My-simple-web se vaya desarrollando iré in-


corporando con mis aportes y los vuestros aquellos temas que sean de utilidad
general para tener una estructura compacta y versátil.

217
15 Aplicación
de ejemplo

Contenido
» Presentación
» ¿Cómo empezar?
» ¿Qué es un fork?
» Pull request
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Presentación
Como hemos ido viendo a lo largo de esta obra, la aplicación de ejemplo que
ilustra los conceptos de este libro la tienes a tu disposición en http://github.com/
jlaso/my-symple-web. Eres libre de usarla, modificarla y mejorarla cuanto con-
sideres oportuno, solo espero que te sea de utilidad lo expuesto, y que si tienes
alguna mejora que aportar y deseas colaborar lo hagas abiertamente, a través
de los mecanismos que proporciona github, mediante los pull-request. Por su-
puesto no he pretendido en ningún momento sentar cátedra, ten en cuenta que
solo soy un modesto programador con muchas ganas de aprender. Por lo tanto
tampoco te fies al al ciento por ciento de lo que aquí te he expuesto. Pruébalo
todo por tu cuenta, aprende del uso y de la práctica y, desde luego, si encuen-
tras algún error estaré encantado de recibirlo y corregirlo.

En todo caso, ten siempre en cuenta que My-simple-web solo es una estructura
que te permitirá crear tus propias aplicaciones, por tanto, no es una web termi-
nada. De hecho te recomiendo que me sigas en github para estar al corriente
de los cambios que en ella voy realizando regularmente.

Para ver la aplicación instalada puedes acudir a la web http://mysimpleweb.


com.es y jugar con ella.

¿Cómo empezar?
Si de verdad tienes interés en utilizar la estructura de My-simple-web para tus pro-
pios proyectos lo primero que tienes que hacer es un fork del proyecto en tu propia
cuenta de github. Cualquier mejora que creas susceptible de ser incorporada al
proyecto principal y que desees compartir tienes que solicitar un pull-request.

Lo primero que necesitas es un usuario de github. Si aún no lo tienes créate


uno y así podremos empezar. A la fecha de escribir este libro tener un usuario
en github es gratuito.
1
El logotipo de My-simple-web es cortesía de Oscar Ruiz Casanova.

220
15. Aplicación de ejemplo

Una vez hayas entrado con tu cuenta en github, acude a la dirección http://jlaso.
github.io/my-simple-web/

Verás una pantalla parecida a esta:

Esta es una pequeña pantalla resumen con unas pequeñas instrucciones para
clonar el proyecto, enlaces para descargarlo sin git y enlace al repositorio ori-
ginal en github.

Si seguimos el link:

221
Programación PHP profesional con Slim, Paris y Twig

nos llevará inmediatamente a la página de github del proyecto, donde podre-


mos hacer un fork.

¿Qué es un fork?
Es como fotocopiar un proyecto. Como te he mencionado antes, la creación
de proyectos abiertos (libre u open-source) en github es gratuita a la fecha de
publicación de este libro. Lo que hace el fork es crear un proyecto en tu cuen-
ta con el mismo nombre que el que estás «forkeando» y luego se hace un git
clone en tu cuenta.

Si descargamos el proyecto original no será con permisos de escritura, por lo


que nuestros cambios no los podríamos subir al repositorio de github.

A partir de ese instante para github es como si el proyecto fuera tuyo, por lo que
podrás hacer commits sin tener que pedir permiso a nadie.

La idea, claro, es aprovechar todo el código y por supuesto añadir cosas que ne-
cesitarás para tu propio proyecto. Por ejemplo, si queremos cambiar el aspecto
de la página de inicio o añadir una nueva funcionalidad como una tabla de au-
tores para los artículos solo es cuestión de hacerlo. Tampoco es obligatorio que
hagas commits si no quieres, ten en cuenta que el proyecto, aunque hayas hecho
un fork y esté en tu cuenta, será público, con lo cual todo el mundo verá lo que
añadas; no sé si esto puede ser un problema en tu caso porque a lo mejor estás
desarrollando para un cliente y no vas a ceder el código al público en general. Si
fuera así no hagas commits del proyecto en esa cuenta de github, a no ser que
tengas una cuenta profesional y puedas establecer como privado el proyecto.

Como te comenté en el capítulo de git, existen varias alternativas a github, en


las que sí podrías hacer el repositorio privado. En la mayor parte de los casos
los clientes nos hacen firmar cláusulas de confidencialidad y no podemos expo-
ner el código desarrollado durante el tiempo dedicado a ese cliente.

Bueno, sea como sea, el caso es que la facilidad que nos proporciona github
para ver proyectos de otros compañeros, seguirlos (watch), y hacer forks nos
proporciona una increíble ayuda para poder estudiar código de otros programa-
dores, y sobre todo utilizar lo que otros tan amablemente han puesto a dispo-
sición del mundo.

Pull request
Si al final decides subirte al carro de la programación libre, quien sabe, quizás
solo quieras aprender y de paso compartir. Una de las cosas más maravillosas

222
15. Aplicación de ejemplo

es cuando tu fork avanza por encima del proyecto principal y le pides al propie-
tario del proyecto original que incluya parte de tu código. Esta acción tiene un
nombre y se llama pull request.

Es fácil de tramitar, sigue estos pasos:

Desde la página principal de tu proyecto en github, localiza la opción Pull


request.

Pulsa el botón de New pull request.

Valida la petición comprobando lo que vas a pedir (los cambios del código apa-
recen en otro color) y pulsando sobre el botón Create pull request:

223
Programación PHP profesional con Slim, Paris y Twig

Para terminar escribe el comentario que quieras que aparezca en la petición y


pulsa el botón de Send pull request.

Como confirmación de lo que has hecho, github nos presentará esta pantalla:

Lo normal es que el propietario del proyecto al que intentas hacer el pull re-
quest entable una conversación contigo sobre las mejoras que pretendes inco-
porar o por qué son necesarias. Evidentemente no voy a reproducir aquí toda
una conversación real, pero te puedes hacer una idea de por dónde van los
tiros viendo esta captura:

224
15. Aplicación de ejemplo

Para contestar y seguir con la conversación introduce tu comentario en la caja


de texto y pulsa sobre Comment.

Una vez el propietario acepta el pull request esto es lo que veríamos por fin:

225
Programación PHP profesional con Slim, Paris y Twig

Y con esto queda visto de manera pormenorizada cómo realizar una petición a
un proyecto principal de modificaciones que hemos llevado a cabo en nuestra
copia (fork) del proyecto.

Conclusión
Aunque es fácil que no uses esta opción nunca, es bueno conocer los mecanis-
mos que se nos proporcionan a nivel colaborativo.

Espero haberte transmitido bien la filosofía de trabajo para los proyectos que
decidas compartir con el resto del mundo mediante github.

En todo caso, si necesitas ampliar más información cuenta con los foros y con
la propia documentación de github.

Siéntete libre de utilizar My-simple-web a tu antojo.

226
Parte IV: Para subir nota
16 TDD
Contenido
» Presentación
» ¿En qué consiste el TDD?
» ¿Para qué sirven los test?
» Ejemplo práctico
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Presentación
Este capítulo pretende ser solamente una introducción y un punto de partida
hacia la filosofía TDD (Test Development Driven), de tal manera que pretendo
sembrar en ti la duda razonable de si hay otra manera de hacer las cosas que
la simple programación, con los acostumbrados ciclos de prueba y error.

La aplicación de ejemplo ya cuenta con test de algunas partes, invito al lector a


que amplie estos y sea libre de hacer un pull-request que atenderé encantado.

¿En qué consiste el TDD?


El desarrollo guiado por test1 consiste básicamente en programar solo lo justo
para lo que se nos pide, conseguir que el test pase en verde (sin erores), refac-
torizar para mejorar el código, con el natural cambio de estado (habitualmente
al refactorizar se pueden introducir errores) y repetir los ciclos hasta que el
código esté haciendo su trabajo y los test pasen en verde.2

¿Para qué sirven los test?


¿Para qué se quieren los test, además de lo evidente, que es comprobar que
un método funciona de la manera esperada?

1
Test driven development.
2
Es una lástima que el libro sea en blanco y negro, porque precisamente TDD muestra
los errores en rojo y los test válidos en verdes. Usaremos no obstante esta analogía
durante todo el capítulo porque tú si verás los resultados en tu pantalla de esa manera.

230
16. TDD

El ciclo de vida del software requiere en multitud de ocasiones la mejora, sus-


titución o eliminación de funcionalidades, de tal manera que el test garantiza
que en caso de refactoring o de agregar nuevo código, este sigue teniendo el
comportamiento esperado.

En realidad no hay que esperar a ejecutar los test al llegar al final del libro, pero
al no tratar este de TDD en exclusiva no lo he abordado hasta ahora para no
hacer que el lector huya despavorido. Nuevos conceptos y todos a la vez pue-
den ocasionar este resultado.

Bien, como te decía, TDD es una filosofía de trabajo que consiste básicamen-
te en desarrollar primero el test que va a hacer que una funcionalidad sea
válida o no, este test ha de contemplar todos los casos que puede presentar
la entrada y validar la misma. Una vez se tienen todos los casos contempla-
dos, se realiza el método mínimo que hace que su salida pase el test, sin
muchas florituras. Es más, es recomedable hacerlo más bien a lo bestia, sin
patrones de diseño, sin cuidar mucho los detalles: si ponemos muchos if o
si cabe un case, el caso es que el método implementado pase todos los test
en verde. Una vez conseguido esto vamos refactorizando el código, simple-
mente empleando métodos de programación que permitan reducir el uso de
variables, el uso de if, no repitiendo código, implementando el código repe-
tido en métodos separados, etc. Pero en cada iteración, en cada cambio,
volvemos a correr los test, tienen que salir siempre en verde, si de un paso al
otro se nos cambia a rojo, tenemos que revisar lo que hemos hecho en ese
paso y arreglarlo para volver a conseguir el verde. Y esto básicamente es el
TDD, por supuesto de una manera muy somera, como te decía solo quiero
rascar y motivarte a interesarte e indagar más sobre este tema, existen libros
muy buenos acerca de este tema. El que te pongo a continuación no es que
lo recomiende explícitamente pero además de ser gratuito está escrito en-
teramente en castellano, no habla específicamente de PHP pero puede ser
un buen punto de partida. El libro se llama Diseño Ágil por TDD y lo puedes
descargar de aquí http://www.dirigidoportests.com/el-libro.

Ahora un poco de técnica, como siempre


Los tests en PHP se corren con el programa PHPUnit, que debes tener insta-
lado. No voy a explicarte cómo hacerlo porque hay miles de tutoriales buenos
acerca de esto, sin duda la página oficial http://phpunit.de es un muy buen
punto de partida. En todo caso si que voy a comentarte un par de cuestiones
relacionadas con el lanzamiento de las pruebas unitarias, ya que no es una
tarea trivial. PHPUnit revisa las clases xxxTest.php que encuentra en la car-
peta que le indicas cuando lo invocas desde la línea de comandos, pero en

231
Programación PHP profesional con Slim, Paris y Twig

ocasiones necesita ayuda sobre ciertas cuestiones, como puede ser un boots-
trap que cargue las clases, configuración, etc., en el proyecto my-simple-web
encontrarás en la raíz un pequeño archivo bootstrap.php3 y un phpunit.xml que
te reproduzco a continuación:

phpunit.xml
<phpunit backupGlobals="true"
backupStaticAttributes="false"
bootstrap="bootstrap.php"
cacheTokens="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
mapTestClassNameToCoveredClassName="false"
printerClass="PHPUnit_TextUI_ResultPrinter"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
strict="false"
verbose="false">
</phpunit>

Y bootstrap.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

Básicamente este bootstrap.php permite a PHPUnit precargar las clases para


no tener que hacer requires en todas las clases de test.

Veámos ahora un ejemplo: en la carpeta lib existen unas cuantas funciones de uti-
lidad que permiten realizar tareas como comprobar un email, generar un slug, etc.

Vamos a ver cómo sería la clase que verificaría que estas funciones producen
el resultado esperado:
<?php
namespace lib;

3
No confundas con el bootstrap de nuestra aplicación, este de ahora es un cargador
que permite inicializar lo necesario para poder correr los test. El concepto es el mis-
mo pero el archivo es diferente.

232
16. TDD

use lib\MyFunctions;
use \PHPUnit_Framework_TestCase;

class MyFunctionsTest extends PHPUnit_Framework_TestCase


{

/**
* Check_email tests
*/
public function testCheckEmail()
{
// check_mail assertions
$this->assertTrue( MyFunctions::check_email('email@email.com') );
// email that is only word
$this->assertFalse( MyFunctions::check_email('emailinvalid') );
// email that hasn’t domain termination
$this->assertFalse( MyFunctions::check_email('email@invalid') );
}

public function NotContains($str,$not)


{
return !(boolean) preg_match("/[{$not}]/i",$str);
}

public function testNotContains()


{
$this->assertTrue( $this->NotContains('abcd','m|j') );
$this->assertFalse( $this->NotContains('abcd','a|j') );
$this->assertFalse( $this->NotContains('áé','á|a') );
}

public function testSlug()


{
$text = "This a text with áéíóúàèìòùÁÉÍÓÚñÑçÇ";
$slug = MyFunctions::slug($text);
$this->assertTrue($this->NotContains(
$slug,
‘á| |é|í|ó|ú|à|è|ì|ò|ù|Á|É|Í|Ó|Ú|ñ|Ñ|ç|Ç’)
);
}

public function testGenkey()


{
$test = MyFunctions::genKey(7);
// verifies lenght
$this->assertTrue( 7 === strlen($test) );

// and that is string


$this->assertTrue( is_string($test) );

233
Programación PHP profesional con Slim, Paris y Twig

// now verifies than matches in repeated generation is less than 10%


$matches = 0;
for ($i=0;$i<1000;$i++){
if ($test == MyFunctions::genKey(7)){
$matches++;
}
}

$this->assertTrue( $matches < 10 );

public function testCamelCase()


{
$test = "ThisIsATestForKnow";
$this->assertTrue( 'this_is_a_test_for_know' ==
MyFunctions::camelCaseToUnderscored($test)
);

$test = "entity_table_name";
$this->assertTrue("Entity\\TableName" ==
MyFunctions::underscoredToCamelCaseEntityName($test)
);
}

No sé si necesitan aclaración, en todo caso muestro la salida de ejecutar el test


para esa carpeta:

> phpunit lib


PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration read from ~/php_projects/my-simple-web/phpunit.xml
.....
Time: 0 seconds, Memory: 6.00Mb
OK (5 tests, 11 assertions)

Si alteramos alguna función original y rompemos el código veremos que esta


misma ejecución obtiene otro resultado, indicándonos que el test no se cumple
y permitiéndonos revisar la función original y arreglarla.

Ejemplo práctico
Ahora vamos a hacer un ejemplo práctico desde cero, vamos a pensar en una
funcionalidad que queramos implementar y haremos el test que la cubra. E
iremos poco a poco aumentando requerimientos.

234
16. TDD

Para ver la manera de iterar, consiguiendo primero resultados negativos, arre-


glando y consiguiendo resultados positivos, de manera cíclica, vamos a imple-
mentar una función que nos devuelva un valor no nulo consistente en una cifra
de 6 dígitos en la que no se repita ninguno de los dígitos.

Vamos a llamar al método PruebaTdd y al método que lo testea testPruebaTdd,


¡qué lio!

Para que puedas seguir el código y las diferentes iteraciones voy a numerar
tanto la función como su test, lo que vamos a hacer es empezar a cumplir es-
pecificaciones una a una.

Empecemos:
<?php

class TddSample
{

public function pruebaTdd1()


{
// El método más simple no pasará el test
}
}

<?php

require __DIR__ . ‘/../TddSample.php’;

class TddSampleTest extends PHPUnit_Framework_TestCase


{

public function testPruebaTdd1()


{
$object = new TddSample();
$this->assertNotEquals(null, $object->pruebaTdd1());

Corremos el test:

> phpunit tddSample/tests


PHPUnit 3.7.13 by Sebastian Bergmann.

Configuration read from ~/mysimpleweb.com.dev/phpunit.xml

235
Programación PHP profesional con Slim, Paris y Twig

Time: 0 seconds, Memory: 6.25Mb

There was 1 failure:

1) TddSampleTest::testPruebaTdd1
Failed asserting that null is not equal to null.

~/mysimpleweb.com.dev/tddSample/tests/TddSampleTest.php:12

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Hemos podido observar que el simple hecho de implementar un método nos


permite correr el test sobre él pero que no lo pasa en verde porque no cumple
la primera premisa, que es devolver un valor no nulo.

Ahora vamos a variar el método para devolver un valor y veremos que si pasa,
para mantener todos los métodos juntos en la misma clase, los voy a numerar,
como te he dicho antes.
<?php

class TddSample
{
// ..

// Hacemos que devuelva algo fijo, y ya pasa el primer test


public function pruebaTdd2()
{
return “1”;
}

El resultado es:

> phpunit tddSample/tests


PHPUnit 3.7.13 by Sebastian Bergmann.

Configuration read from ~/mysimpleweb.com.dev/phpunit.xml

Time: 0 seconds, Memory: 6.25Mb

OK (1 test, 1 assertion)

236
16. TDD

Ahora se trata de iterar hasta conseguir el resultado final, puede que en algún
momento te parezca que hacemos trampa, pero recuerda la premisa de que
no es necesario implementar nada más que lo que se nos pide, no hay que
inventar ni hacer filigranas.

Vamos a hacer primero el test que queremos cumplir, que va a ser que el valor
devuelto sea una cadena de tamaño 6 y que esté compuesta por 6 dígitos.
<?php
//..
class TddSampleTest extends PHPUnit_Framework_TestCase
{
//..
public function testPruebaTdd3()
{
$object = new TddSample();
$result = “” . $object->pruebaTdd3();
$this->assertNotEquals(null, $result);
$this->assertEquals(6, strlen($result));
$this->assertTrue((bool)preg_match(‘/\d{6}/’,$result));
}
}

Sin duda alguna te llamarán la atención algunos typecast que he tenido que añadir,
para estar seguro de que el resultado se evalúa como una cadena no hay nada
como concatenar la cadena vacía con el resultado: "" . $object->pruebaTdd3(),
y luego el resultado de preg_match no es un booleano estricto ya que devuelve
literalmente 0 o 1, y por eso (bool) lo transforma, assertTrue hace una compro-
bación estricta y no nos la pasa sin ese «truco».

Veamos el resultado al correr de nuevo el test:

> phpunit tddSample/tests


PHPUnit 3.7.13 by Sebastian Bergmann.

Configuration read from ~/mysimpleweb.com.dev/phpunit.xml

Time: 0 seconds, Memory: 6.25Mb

OK (1 test, 3 assertions)

Como ves nuestro pequeño truco de devolver una cadena con seis unos ha fun-
cionado, ten en cuenta que para pasar este test no es necesario hacer nada más.

237
Programación PHP profesional con Slim, Paris y Twig

Compliquemos ahora el test que ha de pasar, comprobando que no se repite


ninguno, creamos una función que comprueba si hay repeticiones y la invoca-
mos desde el nuevo test(4):
<?php

require __DIR__ . '/../TddSample.php';

class TddSampleTest extends PHPUnit_Framework_TestCase


{
//,,,
public function hasRepetitions($valueToTest)
{
$stats = count_chars("" . $valueToTest, 1);
foreach ($stats as $i => $val) {
if($val > 1){
return true;
}
}

return false;
}

public function testPruebaTdd4()


{
$object = new TddSample();
$result = "" . $object->pruebaTdd3();
$this->assertNotEquals(null, $result);
$this->assertEquals(6, strlen($result));
$this->assertTrue((bool)preg_match('/\d{6}/', $result));
$this->assertFalse($this->hasRepetitions($result));
}

El resultado de ejecutar el test con este cambio es:

> phpunit tddSample/tests


PHPUnit 3.7.13 by Sebastian Bergmann.

Configuration read from ~/mysimpleweb.com.dev/phpunit.xml

Time: 70 ms, Memory: 3.75Mb


There was 1 failure:

1) TddSampleTest::testPruebaTdd4

238
16. TDD

Failed asserting that true is false.

~/mysimpleweb.com.dev/tddSample/tests/TddSampleTest.php:51

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

Como ves ahora ya no se cumple, porque «111111» es un valor no nulo, con


6 dígitos, pero contiene dígitos repetidos. Nos toca el turno de acomodar la
función para que genere por fin ese valor esperado, en principio, ten claro que
devolviendo «123456» el resultado sería correcto y pasaría el test. No te voy
a hacer pasar de nuevo por esto, al final sería un poco absurdo tener un mé-
todo que te devuelva siempre 123456, pero ten en cuenta que si lo que piden
es únicamente un valor no nulo con 6 dígitos númericos que no tenga ningún
dígito repetido con esta aproximación sobraría.

<?php

class TddSample
{
//..
public function pruebaTdd4()
{
$digits = array(0,1,2,3,4,5,6,7,8);
shuffl e($digits);

return implode("",array_slice($digits,0,6));
}

Y para que puedas comprobar que hemos conseguido el resultado perseguido


aquí tienes de nuevo el volcado de la ejecución del test.

> phpunit tddSample/tests


PHPUnit 3.7.13 by Sebastian Bergmann.

Configuration read from ~/mysimpleweb.com.dev/phpunit.xml

Time: 49 ms, Memory: 3.75Mb

OK (1 test, 4 assertions)

239
Programación PHP profesional con Slim, Paris y Twig

Una única cuestión antes de terminar este capítulo, y es que en el código verás
todas las funciones que hemos ido viendo en la misma clase y numeradas, así
como los métodos de test, están así para poder ilustrar este capítulo, evidente-
mente esto en un entorno real se hace sobre el mismo método. Ten en cuenta
que para poder ilustrar esa evolución y que la tengas a tu disposición en el
código de My-simple-web he mantenido ese imaginario hilo de tiempo.

Conclusión
No te asustes porque haya incluido este capítulo en la parte que dramáticamen-
te me he atrevido a llamar PARA SUBIR NOTA. No se trata de que aquí solo
van a llegar los de intelecto superior, ni nada parecido. La idea es que asimiles
todo lo expuesto anteriormente, y como remate o guinda de este maravilloso
pastel que hemos montado, tenemos el tema del TDD. Ten en cuenta que en la
práctica no es una cosa que se hace al final del desarrollo, sino de manera to-
talmente paralela al mismo. Es más, los tests están vivos junto con el desarrollo
del software. Permiten entre otras cosas asegurar la calidad del software que
subimos a producción. Si al final te decides a usarlos en tus desarrollos estarás
ya en un nivel Ninja ;<)

240
17 Caso práctico
Contenido
» Presentación
» E-commerce básico
» Manos a la obra
» El reto
» Conclusión
Programación PHP profesional con Slim, Paris y Twig

Presentación
Para que veas de que manera la aplicación My-simple-web te puede ser útil
en tus desarrollos vamos a extenderla para crear un e-commerce muy básico.

Como el hecho de plantearse una tienda virtual completa requiriría al menos un


libro de las dimensiones de este dedicado en exclusiva a ese fin, vamos a partir
de algo muy sencillo para que veas en primer lugar cómo utilizar My-simple-
web en tu propio beneficio y sobre todo algo más de código que con seguridad
será provechoso.

El proyecto en el que nos vamos a embarcar a continuación está relacionado


con el mundo de Internet, y no va a requerir por tu parte desembolso alguno
comprando artículos, haciendo fotos de alta calidad y páginas muy atractivas
con la intención de que esos artículos se vendan. Podrás empezar a ganar
dinero inmediatamente con la tienda que te propongo a continuación, pues los
artículos que se vendan se adquieren al proveedor en el mismo momento en
que tu cliente te los compra y te paga.

No, no estoy hablando de dropshipping1, aunque podría. Si fuera así tendría-


mos el mismo tipo de inconvenientes que los ya enumerados y, te voy a decir la
verdad, para montar una tienda virtual con todas las opciones hay unos cuan-
tos programas open-source con muy buen rendimiento.

Bien, te voy a sacar de dudas, vamos a vender dominios, sí, nombres de do-
minios.

Solo necesitamos un registrador de dominios que tenga API y una cuenta de


PayPal.

Además, para hacerlo más interesante, y sobre todo para saber si el libro te ha
resultado útil, te voy a proponer un reto: al final de este capítulo encontrarás
más información.

1
http://es.wikipedia.org/wiki/Drop_shipment

242
17. Caso práctico

E-commerce básico
Para documentar este capítulo y no montar un sistema completo de tienda
electrónica ajeno por completo al ámbito de este libro vamos a pensar en la
venta de un solo producto. Debe ser algo que podamos conseguir mediante
una API, por ejemplo algún libro de Amazon, algo de dropshipping o registro de
dominios a través de un registrador de dominios autorizado.

¿Por qué simplificar tanto?


Para una aproximación inicial a una tienda electrónica necesitamos simplificar al
máximo las opciones, ya que la concepción, desarrollo y puesta a punto de un
e-commerce completo llevaría todo el texto de este libro y un par de ellos más.

El hecho entoces es centrarnos en cómo se haría, de tal manera que luego tú


lo puedas extrapolar al caso real al que te estés enfrentando.

La estructura que vamos a seguir en este ejemplo es:

1) Montar una pequeña API que nos permita saber si un artículo está dispo-
nible,

2) Comprarlo.

3) Y, en su caso, enviarlo si corresponde.

Para centrar las ideas en este punto vamos a tomar el ejemplo de registro de
dominios.

Una de las API con las que he trabajado, he de decir que con resultado muy
satisfactorio, debido sobre todo a la madurez de la documentación y a que
admite un modo sandbox (no se realiza la compra de manera real), es la de
la multinacional OVH. No sé si en tu vida profesional vas a realizar este tipo
de ventas o integraciones en los carritos de la compra que montes, pero
además, este, por tratarse de un producto que no es físico, no necesita ser
enviado de manera material, con lo cual la última parte (y en su caso enviarlo
si corresponde) verás que consiste básicamente en enviar un correo electró-
nico al cliente.

Hay otra cuestión que en nuestro caso nos puede ayudar, de alguna manera,
a valorar este como un servicio real aplicable a otros sectores, y se trata de
que la compra efectiva no se materializa de manera inmediata. El proveedor
encola la petición, procesándola en segundo plano, y no tiene listo el dominio
hasta que no han pasado algunos minutos. Esta demora, perfectamente puede

243
Programación PHP profesional con Slim, Paris y Twig

simular la compra real de cualquier producto físico, además de permitirnos


poner en juego los cronjobs2 que tan útiles resultan en desarrollos profesio-
nales.

Sea como sea y teniendo claro que es imposible abarcar todos y cada uno de
los productos que son susceptibles de ser vendidos de manera electrónica, si
te parece oportuno tomaremos este ejemplo como base.

Otra cuestión bien diferente es que al final decidas montar una tienda online
para un cliente o para ti mismo y los productos que estés ofreciendo sean con-
trolados directamente por ti sin necesidad de una API para ello. En este caso,
bastante más complejo, hay que tener en cuenta una serie de cuestiones que
complican en exceso este ejemplo, pero te voy a enumerar algunas para que
las tengas en mente:

- Control de inventario, para no vender (y cobrar) nada de lo que no se


tenga disponibilidad (esto el dropshipping lo salva en parte3).
- Control de tallas y colores si hablamos de artículos de zapatería o moda,
teniendo en cuenta además que pueden existir diferentes tipos de talla-
jes, como son las tallas S-M-L-XL y las numéricas. Si unes este punto al
anterior ya tienes un buen lío montado.
- Zona de usuario donde el cliente pueda ver el estado de sus pedidos, des-
cargarse las facturas, hacer comentarios o ver/poner tickets de soporte.
- Zona administrativa donde el personal encargado de manejar la tienda pue-
da crear/modificar/borrar artículos, poner fotos, actualizar inventario, etc.
- Posiblemente un enlace con el programa de facturación que la empresa
esté utilizando hasta ese momento, o si la empresa no está utilizando
ninguno por ser de nueva creación facilitar esa gestión administrativa a
través de la aplicación que nosotros creemos.

Si sumas todas estas características habrás desarrollado desde cero una apli-
cación web al estilo de las más grandes, como puedan ser Prestashop o Ma-
gento, lo que no digo que no se pueda hacer, pero en la mayoría de los casos
no es conveniente reinventar la rueda.

No te recomiendo en absoluto que te metas en un embrollo así, pero a menu-


do las cosas empiezan con una tienda en la que se vende un solo producto y

2
Los cronjobs son tareas que se le indican al sistema operativo para que realice de
manera repetitiva a intervalos de tiempo establecidos.
3
El dropshipping permite conocer la disponibilidad (existencias diarías de los artícu-
los) pero no garantiza que a lo largo del día las existencias no mengüen.

244
17. Caso práctico

al final, con las distintas evoluciones, terminas gestionando una tienda com-
pleta.

Centremos conceptos
Volviendo a nuestro ejemplo quizás lo encuentres triste y pobre al lado de todos
los puntos que te he enumerado, ya que en nuestro caso:

1) El proveedor es quien controla la existencia (inventario) o no del artículo


que buscamos (el nombre de dominio), mediante una llamada a su API
nos va a decir si está libre o no.

2) Al tratarse de un solo producto no vamos a tener la complicación de las ta-


llas y colores, pero vamos a complicar el ejemplo un poquito dotándolo de
una característica muy útil desde el punto de vista de nuestro comprador.
Cuando se nos solicite en nuestra tienda la compra de un nombre de do-
minio nosotros le preguntaremos a la API no solo el dominio que ha pedido
el usuario sino todos los dominios iguales pero con diferentes terminacio-
nes. Por ejemplo, si el usuario quiere saber si puede comprar midominio.
com nosotros buscaremos midominio.com, midominio.es, midominio.info,
midominio.net y midominio.org por ejemplo. Como puedes comprender lo
haremos bien y parametrizaremos la lista de extensiones que vamos a
buscar (como si fueran los colores de una camiseta, sería como decir: lo
siento, no la tengo en rojo, pero si que me queda en verde y en amarillo).

3) Haremos un cron que verifique que el dominio está disponible una vez
registrada la compra para de esa manera hacer dos cosas: a) informar
al usuario de que ya tiene disponible el dominio; y b) hacer los ajustes
oportunos en las DNS.

4) Por eso en la zona de usuario además de listar los dominios que tiene
comprados el usuario y cuándo caducan por si los quiere renovar, centra-
lizaremos la gestión de los DNS. Ya ves que algo que parecía en principio
tan sencillo se va complicando poco a poco. Imagínate si habláramos de
más productos y con la posibilidad de descripciones, tallas, colores, dife-
rentes imágenes y/o vídeos.

Manos a la obra
Vamos a empezar
Lo primero que necesitamos, además de la API es establecer las entidades que
vamos a necesitar para realizar todo con éxito.

245
Programación PHP profesional con Slim, Paris y Twig

En principio, vamos a necesitar una tabla de usuarios y una tabla de dominios,


los registros DNS verás más adelante que el hecho de almacenarlos nosotros
localmente no nos proporciona ninguna ventaja, ya que el proveedor lo hace
igualmente, y los que él guarda son los que tienen validez.

El diagrama de fl ujo de la aplicación, siguiendo los puntos enumerados antes,


sería este:

Formulario de búsqueda: donde se le presentará al usuario una caja de texto


donde podrá introducir el dominio que quiere buscar, y un botón para realizar
la búsqueda.

Internamente, cuando recibamos la peticion POST del formulario probaremos


a buscar no solo la extensión pedida, sino también el mismo dominio con las
extensiones más comunes: .com, .net, .org, .info, etc.

246
17. Caso práctico

Formulario de presentación de resultados y selección o vuelta atrás: con los


resultados de la búsqueda se le presentará al usuario una lista con un botón en
cada fila para adquirir ese dominio, y un botón al pie para poder volver al for-
mulario anterior. Si no hay resultados presentaremos un mensaje indicándolo,
para que pruebe otra vez.

Formulario de compra: aquí se le presentará al usuario una pequeña ayuda en


lo referente al compromiso que adquiere al comprar el dominio y un formulario
para que rellene con sus datos, que serán los que se le indicarán al registrador
de dominios como propietario del dominio. Se hará una pequeña validación
sobre los datos, como que no estén en blanco o que el código postal parezca
un código postal.

Habrá además un botón de pago con PayPal en el que haremos el registro


efectivo una vez recibido el visto bueno del pago.

Internamente se dará de alta un registro con los datos del usuario, se creará
una contraseña aleatoria, se enviará un correo dándole la bienvenida e indican-
do esa contraseña para que pueda acceder al área de usuario y se creará un
registro en la tabla de dominios, que se asociará con ese usuario.

Habrá que implementar un cron cada 30 minutos4 para que verifique la validez
del registro mediante una llamada al registrador de dominios. Necesitaremos
también un cron diario para por un lado verificar la validez de los registros DNS
y por otro comprobar la caducidad de los dominios para avisar con tiempo al
usuario de que su dominio necesita ser renovado.

Area de usuario
En esta zona el usuario podrá:
» Acceder a la zona de registro de nuevos dominios.
» Pagar por adelantado un dominio que está a punto de caducar o no.
» Cambiar los DNS de su dominio de una manera muy básica (registros
A y CNAME).

Recuerda que todas las simplificaciones se hacen en aras a facilitar el enten-


dimiento del código.

Vamos a crear ahora una clase que nos encapsule los métodos del proveedor
de registro que vamos a utilizar, de tal manera que podamos tener una interfaz
4
30 minutos es un tiempo adecuado según mi experiencia, pero puede ser cualquier
otro que consideres.

247
Programación PHP profesional con Slim, Paris y Twig

uniforme para ello. Dejaremos todos los intríngulis de la conexión con la API
del tercero a esa clase.

Siguiendo los principios que hemos ido enumerando a lo largo del texto vamos
a hacer que este bloque de código sea totalmente autónomo de My-simple-
web, para lo cual crearemos un módulo separado. Creo que esto te puede ser
de ayuda así que te voy a enumerar los pasos que he seguido. No son para que
los repitas tú en este caso, sino para que los tengas de guía en el caso de que
necesites hacer algo parecido.

He creado dentro de la carpeta vendor la siguiente estructura:


vendor
├── jlaso
│ └──ovh-domain-api
│ └──JLaso
│ └──OvhDomainApi

Es habitual crear una réplica exterior de nuestro sistema de carpetas pero en


minúsculas, el nombre del vendedor o programador va separado del nombre
del paquete, tal y como lo hace github. De tal manera que en realidad nosotros
solo vamos a trabajar en la carpeta más interior, no poniendo nada en las otras.

Lo primero es crear un archivo composer.json y poner en él todo esto:

{
"name": "jlaso/ovh-domain-api",
"version": "1.0.0",
"description": "Client API for ovh.com domain functions",
"keywords": ["api", "ovh", "domains"],
"homepage": "https://github.com/jlaso/ovh-domain-api.git",
"type": "module",
"license": "mit",
"authors": [
{
"name": "Joseluis Laso",
"email": "jlaso@joseluislaso.es",
"homepage": "http://www.joseluislaso.es"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": { "JLaso\\OvhDomainApi": "" }
},
"target-dir": "JLaso/OvhDomainApi"
}

248
17. Caso práctico

Los puntos más importantes, sin duda, son los que indican cómo se va a llamar
el paquete (name), dónde está ubicado (homepage), y dónde se va a ubicar al
desplegarlo (target-dir).

Para crear ese composer.json necesitamos saber qué poner en homepage, por lo
que de manera paralela habremos creado el repositorio correspondiente en github.

Recuerda que en su momento vimos que no es obligatorio utilizar github, hay


otras alternativas, inclusive podemos utilizar nuestro propio servidor privado,
pero como lo que vamos a implementar es un paquete público, la fusión pac-
kagist + github es la mejor opción en cuanto a rápidez, visibilidad y facilidad.

Al crear el repositorio ya disponemos, por un lado, de la dirección final del


paquete en el repositorio (homepage) y de los datos para crear y vincular un
repositorio local asociado a ese. De tal manera que nuestros cambios en local
puedan ser validados y subidos al repositorio central.

Por tanto desde un terminal y estando situados en la carpeta más interna (re-
cuerda que hemos comentado que solo vamos a trabajar en esa), ejecutare-
mos las siguientes instrucciones:

git init
git remote add origin jlaso@github.com/ovh-domain-api.git

Esto inicializa el repositorio vacío y lo vincula con el de github asociándole el


nombre origin.

A partir de aquí todos los avances que se produzcan en el código local se pue-
den ir subiendo al repositorio central.

Nos queda ahora crear ese paquete en packagist.org para que la vinculación
sea total, y de esa manera, en el proyecto principal, sea suficiente con incluir la
siguiente línea en el composer.json
"require": {
...
"jlaso/ovh-domain-api": "*",
...
},

Quiero recordarte aquí, tal y como vimos en el capítulo de Composer que este
último paso no es obligatorio y que podemos utilizar los repositorios VCS. Pero
como te he comentado al principio de esta sección quiero que veas cómo ha-
cerlo todo de arriba abajo y luego, en función de tus necesidades reales, pue-
das discernir qué camino es el mejor para tu caso concreto.

249
Programación PHP profesional con Slim, Paris y Twig

Nos queda ahora escribir el código de la API que nos va a permitir crear esa
capa de abstracción y utilizar instrucciones sencillas para realizar las tareas
básicas que vamos a llevar a cabo:
» Comprobar si un dominio está libre.
» Registrar un dominio.
Así, el contenido de esa API será el siguiente:
<?php

namespace JLaso\OvhDomainApi\Service;

class OvhApi
{
const UNKNOWN = 0; // Can not retrieve availability of domain
const AVAILABLE = 1; // Domain is available
const NOT_AVAILABLE = - 1; // Domain is not available

const WSDL_URL = "https://www.ovh.com/soapi/soapi-re-1.56.wsdl";

/** @var string [User registered in OVH] */


protected $username;
/** @var string [Password to access OVH] */
protected $password;
/** @var array */
protected $accessData;
/** @var bool */
protected $sandBoxMode;
/** @var string */
protected $language;
protected $session = null;
/** @var \SoapClient */
protected $soapClient = null;
/** @var \SoapFault */
protected $lastException = null;

/**
* @param $user
* @param $password
* @param bool $sandBoxMode
* @param string $language
*/
function __construct($user, $password, $sandBoxMode = true, $language = 'es')
{
$this->sandBoxMode = $sandBoxMode;
$this->language = $language;
$this->password = $password;
$this->username = $user;

250
17. Caso práctico

$this->accessData = array(
'hosting' => 'none',
'offer' => 'gold',
'profile' => 'whiteLabel',
'owo' => 'no',
'owner' => $user,
'admin' => $user,
'tech' => $user,
'billing' => $user,

'dns1' => '',


'dns2' => '',
'dns3' => '',
'dns4' => '',
'dns5' => '',

// only mandatory for .fr domains


'method' => '',
'legalName' => '',
'legalNumber' => '',
'afnicIdent' => '',
'birthDate' => '',
'birthCity' => '',
'birthDepartement' => '',
'birthCountry' => 'ES', // Country must be in ISO3166

'dryRun' => $sandBoxMode,


);
}

/**
* @param string $domain
*
* @return int
*/
public function isAvailable($domain)
{
$domain = strtolower(trim($domain));
$this->login();
$results = $this->request(‘domainCheck’, array($domain));

foreach($results as $result){
if($result->predicate == 'is_available'){
if($result->value){
return self::AVAILABLE;
}else{
return self::NOT_AVAILABLE;
}
}
}

251
Programación PHP profesional con Slim, Paris y Twig

return self::UNKNOWN;
}

/**
* @param OwnerDomain $ownerData
*/
public function createOwnerId(OwnerDomain $ownerData)
{
$result = $this->request('nicCreate', $ownerData->asArray());
var_dump($result); die;
}

/**
* @param string $domain
* @param string $ownerId
*
* @return bool
*/
public function registerDomain($domain, $ownerId)
{
if ($this->isAvailable($domain)) {
$this->request(
'resellerDomainCreate',
array($domain, 'owner' => $ownerId)
);

return true;
}

return false;
}

/**
* PROTECTED METHODS
*/

/**
* logout in ovh
*/
protected function login()
{
$this->soapClient = new \SoapClient(self::WSDL_URL);
$this->session = $this->soapClient->login(
$this->username,
$this->password,
$this->language,
false
);
}

252
17. Caso práctico

/**
* logout in ovh
*/
protected function logout()
{
if($this->soapClient && $this->session){
$this->soapClient->logout($this->session);
$this->session = null;
}
}

/**
* @param $method
* @param array $param
* @param bool $catchException
*
* @return bool|mixed
* @throws \Exception
* @throws \SoapFault
*/
protected function request($method, $param = array(), $catchException = true)
{
if(!$this->session){
$this->login();
}
$this->lastException = null;
try{

// add as first parameter the session


array_unshift($param, $this->session);
$response = call_user_func_array(
array($this->soapClient, $method),
$param
);

}catch (\SoapFault $e){

if($catchException){
$this->lastException = $e;

return false;
}else{
throw $e;
}
}

return $response;
}

Ejemplo 78: ontenido de Service/OvhApi.php

253
Programación PHP profesional con Slim, Paris y Twig

Ya que tenemos el capítulo de TDD tan reciente, no he podido evitar la tenta-


ción y he creado los test correspondientes a las funciones de disponibilidad de
dominio y registro. Dejo a tu disposición el código fuente para que lo evalúes y
refresques toda la información que vimos en el capítulo anterior.
<?php

require __DIR__ . '/config.php';


use JLaso\OvhDomainApi\Service\OvhApi;
use JLaso\OvhDomainApi\Service\OwnerDomain;

class OvhDomainTest extends PHPUnit_Framework_TestCase


{

/** @var \JLaso\OvhDomainApi\Service\OvhApi */


private $api;

const SANDBOX_MODE = true;

function __construct()
{
$this->api = new OvhApi(OVH_USER, OVH_PASS, self::SANDBOX_MODE, ‘es’);
}

public function testDomainExists()


{
$this->assertEquals(
$this->api->isAvailable('google.com'),
OvhApi::NOT_AVAILABLE
);
}

public function testDomainNotExists()


{
// normally a domain invented don’t exists, doesn’t ?
$domain = "d".uniqid().".com";
$this->assertEquals(
$this->api->isAvailable($domain),
OvhApi::AVAILABLE
);
}

public function testBuyDomain()


{
$domain = "php.ki";
$owner = new OwnerDomain(
'email@example.com', 'Owner Name', 'FirstName', 'p4$Sw0rd',
'Address line', 'Area', 'Madrid', ‘es’, '28001', 'phone', 'fax'
);

254
17. Caso práctico

$this->assertTrue($this->api->registerDomain($domain, $owner));
}

Ejemplo 79: ontenido de Tests/OvhDomainTest.php

Como has podido comprobar, lo primero que hacemos es requerir un archivo en la


misma carpeta que se llama config.php, pero este archivo no existe, mira el código
en github o en tu código, si te has descargado el proyecto. En su lugar existe un
archivo config.php.dis que contiene una estructura vacía o con datos inventados.

Esta práctica es habitual: incluir un archivo de distribución con datos ficticios


y excluir el archivo correcto de git poniendo su patrón en el archivo .gitignore.

Es importante que adquieras este hábito, sobre todo en repositorios públicos,


pues no es adecuado publicar información sensible.

# personal configuration
Tests/config.php

Ejemplo 80: .gitignore

<?php

define("OVH_USER", "xxxxx-ovh");
define("OVH_PASS", "123456");

Ejemplo 81: config.php.dis

Te preguntarás que de dónde obtener entonces los datos que hay que poner en
lugar de los que hay de muestra.

Primero: copia el archivo config.php.dis como config.php, a continuación ve a


ovh.com y créate una cuenta. Obtendrás un código de cliente (OVH_USER) y
el password será el que tú hayas elegido (OVH_PASS).

Para que la API funcione no es necesario que recargues crédito, pero sí que
aceptes los contratos oportunos.

También vas a necesitar una cuenta en PayPal para poder hacer las transac-
ciones con tus futuros clientes.

Recuerda que en PayPal una vez realizados todos los pasos debes activar tu
cuenta como vendedor, esto no es un proceso inmediato, tenlo en cuenta de
cara a saber cuándo vas a poder tener tu tienda en producción al 100%.

255
Programación PHP profesional con Slim, Paris y Twig

Paypal ya pone a nuestra disposición el formulario de compra5, con copiar y pe-


gar el mismo en nuestra web puede ser suficiente, en todo caso ten en cuenta
cosas como por ejemplo que no pregunte la dirección de envío ya que nuestro
producto es virtual.
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">

<!-- Saved buttons use the "secure click" command -->


<input type="hidden" name="cmd" value="_s-xclick">

<!-- Saved buttons are identified by their button IDs -->


<input type="hidden" name="hosted_button_id" value="221">

<!-- Saved buttons display an appropriate button image. -->


<input type="image" name="submit" border="0"
src="https://www.paypalobjects.com/en_US/i/btn/btn_buynow_LG.gif"
alt="PayPal - The safer, easier way to pay online">

<img alt="" border="0" width="1" height="1"


src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif">

<!-- Avoid to ask for a shipping address -->


<input type="hidden" name="no_shipping" value="1">

<input type="hidden" name="return"


value="http://www.example.com/pagado.php">
<input type="hidden" name="cancel_return"
value="http://www.example.com/cancelado.php">

</form>

Ejemplo 82: ejemplo de formulario de paypal

El reto
Te propongo terminar lo que hemos empezado a describir en este capítulo, con
las siguientes condiciones:

1) Utilizar My-simple-web como punto de partida.

2) Usar la API que te he recomendado o cualquiera que tú consideres opor-


tuna, ya sea con este proveedor de nombres de dominio o con cualquier
otro, inclusive con cualquier otro tipo de producto.

3) Publicar tus resultados en tu área de github, si has hecho un fork de My-


simple-web esto te resultará fácil.

5
https://developer.paypal.com

256
17. Caso práctico

En todo caso ten precaución con lo siguiente:

Cuando pongas datos personales, como datos de acceso a cualquier API uti-
liza un archivo que no esté seguido por git (su patrón estará incluido en el
archivo .gitignore) y en su lugar (además de) pon el mismo archivo indicando
que es una muestra o para distribución, en el caso de My-simple-web habrás
visto que existe un archivo app/config/dbconfig_sample.php. Este archivo es la
base para que solo copiando y renombrando a dbconfig.php, y evidentemente
cambiando el contenido por datos reales en cada caso, sea suficiente para que
la aplicación funcione.

Como el nombre deja intuir, dbconfig.php es para los datos de configuración


de la base de datos aunque puedes meter toda la información relativa a la con-
figuración ahí. Si al final optas por crear un archivo propio recuerda no subir a
github información personal, agregando ese archivo a .gitignore.

Una vez publicado el proyecto y probado envíame un correo a jlaso@joseluislaso.


es poniendo en el asunto «Reto SPT», en el cuerpo del mensaje puedes ponerme
lo que tú quieras, pero es imprescindible que al menos me indiques la dirección
URL de tu proyecto en github.

Todos los proyectos que estén basados en la estructura de My-simple-web


(esto es, habiendo hecho un fork) serán incluidos en mi página web como desa-
rrollos hechos utilizando My-simple-web y si su autor me autoriza con mención
expresa de su nombre.

Todos los proyectos que me lleguen en los que se haya resuelto correctamente
el ejercicio propuesto de compra de dominios, gestión del área del usuario con
posibilidad de renovaciones, cambio de DNS, etc., y todo lo que se les ocurra
añadir o incluso cualquier solución de tienda electrónica basada en la estructu-
ra de My-simple-web, serán mencionados en próximas revisiones de este libro
si el autor me permite expresamente mencionar su nombre.

257
Programación PHP profesional con Slim, Paris y Twig

Para poder llevar a cabo de manera material el proyecto vas a necesitar tener
una cuenta en PayPal y otra en OVH, en ambos la creación de una cuenta es
gratuita. Para el caso de Paypal tendrás que asociar una tarjeta de crédito y
luego tendrás que habilitar tu cuenta para poder vender con ella. En el caso de
OVH las compras son siempre contra un saldo prepago6, con lo cual y para em-
pezar a probar puedes hacer las compras en modo sandbox7 y posteriormente
pasar a producción, si no tuvieras saldo en la cuenta el proveedor te va avisar
de ello mediante un correo electrónico permitiendo finalizar el pedido del domi-
nio en cuestión y no perder la venta.

Conclusión
Como colofón quiero aprovechar para motivarte y hacer que muevas un poco
los dedos tecleando, así como de paso recibir feedback por tu parte. Todo es-
critor espera que le lean, y qué mejor manera de comprobarlo viendo que sus
lectores ponen en práctica las explicaciones que se han dado en el texto.

Para no dejar a nadie de lado, aquellas personas que tengan interés no hace
falta que terminen el proyecto para poder avanzar: si es tu caso y te quedas
atascado en algún punto puedes contactarme en la misma dirección de e-mail
(jlaso@joseluislaso.es). Estaré encantado de orientarte dentro de mis posibili-
dades. Ten en cuenta que el proceso de aprendizaje es eso: un proceso.

6
Esto es así a la fecha de cierre de edición, en todo caso verifica esta información en
http://www.ovh.com
7
OVH lo llama DryRun.

258
Índices
Ejemplos de código
Ejemplo 1: extracto de inicialización del bootstrap (index.php) .......................... 12

Ejemplo 2: app controller frontend home.php.......................................................... 12

Ejemplo 3: archivo web .htaccess ...................................................................................... 18

Ejemplo 4: sample plain.php, acceso en plano a la bbdd ......................................... 26

Ejemplo 5: sample orm.php, acceso mediante ORM a la bbdd .............................. 28

Ejemplo 6: bucle recorriendo registros obtenidos mediante el ORM ................. 30

Ejemplo 7: bucle recorriendo registros obtenidos con sentencias Mysql ......... 30

Ejemplo 8: sample relations.php ........................................................................................ 35

Ejemplo 9: bloques separados: header, body y footer ............................................... 38

Ejemplo 10: pintado de bloques por método ................................................................ 38

Ejemplo 11: pintar bloque eader con función ............................................................ 39

Ejemplo 12: función para generar bloque con sustitución ...................................... 40

Ejemplo 13: contenido de demo.template ...................................................................... 40

Ejemplo 14: base.html.twig básico ..................................................................................... 43

Ejemplo 15: base.html.twig completo............................................................................... 44

Ejemplo 16: frontend.html.twig .......................................................................................... 44

Ejemplo 17: home.html.twig ................................................................................................. 45

Ejemplo 18: acceso objeto desde twig .............................................................................. 45

Ejemplo 19: declaración de array asociativo dentro de twig .................................. 46

Ejemplo 20: condicional en twig ......................................................................................... 46

Ejemplo 21: condición ternaria en twig ........................................................................... 46

Ejemplo 22: ejemplo de un bucle for en twig ................................................................ 47

Ejemplo 23: mensaje else en un bucle for en twig....................................................... 47

Ejemplo 24: filtro raw en twig.............................................................................................. 48


Ejemplo 25: filtros encadenados en twig ........................................................................ 48

Ejemplo 26: formulario en twig sin usar macros ......................................................... 51

Ejemplo 27: macro drawField en twig .............................................................................. 51

Ejemplo 28: formulario en twig empleando una macro ........................................... 51

Ejemplo 29: web index.php o bootstrap ......................................................................... 98

Ejemplo 30: app controller autoload.php ..................................................................... 99

Ejemplo 31: app controller frontend home.php ....................................................... 99

Ejemplo 32: home route controller ................................................................................100

Ejemplo 33: demos controller error404demo.php ................................................102

Ejemplo 34: demos controller error500demo.php ...............................................105

Ejemplo 35: ejemplo de función middleware ..............................................................109

Ejemplo 36: sample join.php ..............................................................................................114

Ejemplo 37: sample filter.php ............................................................................................115

Ejemplo 38: BaseModel.php incipiente ..........................................................................116

Ejemplo 39: app models core BindableInterface.php ..........................................117

Ejemplo 40: BaseModel.php con bind.............................................................................117

Ejemplo 41: app models core SluggableInterface.php.........................................125

Ejemplo 42: fragmento de app controller backend CRUDedit entity.php ...129

Ejemplo 43: fragmento app controller frontend contact.php...........................135

Ejemplo 44: regenerateDatabase ithCaution.php ..................................................141

Ejemplo 45: Primer ejemplo twig con datos de la bbdd .........................................145

Ejemplo 46: plantilla frontend home staticpage.html.twig.................................146

Ejemplo 47: app controller backend CRUD list entity.php ...............................148

Ejemplo 48: app templates backend entity list.html.twig.................................151

Ejemplo 49: app models core Form Form idget::form table head ............153
Ejemplo 50: app models core Form Form idget::form table row ..............155

Ejemplo 51: app models Entity ArticleFormType.php (getSearchForm) ....157

Ejemplo 52: app models core Form Form istTypeInteface.php ....................160

Ejemplo 53: app controller frontend contact.php .................................................162

Ejemplo 54: app templates frontend contact index.html.twig ........................164

Ejemplo 55: ContactFormType.php (1)..........................................................................166

Ejemplo 56: app controller backend CRUD list entity.php ...............................168

Ejemplo 57: app models core ValidableInterface.php .........................................174

Ejemplo 58: app models core SluggableInterface.php.........................................178

Ejemplo 59: declaración del método slug en lib MyFunctions.php ..................180

Ejemplo 60: fragmento de app models Entity Article.php .................................181

Ejemplo 61: i18n.php .............................................................................................................186

Ejemplo 62: extracto de lib TwigViewSlim.php.........................................................187

Ejemplo 63: extracto de DemoController.php de Symfony2 .................................190

Ejemplo 64: DemoController.php de Symfony2 simplificado ...............................191

Ejemplo 65: Controller.php..................................................................................................193

Ejemplo 66: RoutingCacheManager.php ........................................................................198

Ejemplo 67: app cache routing TestAnotationController.php ..........................199

Ejemplo 68: regenerateDBwithoutConfirmation.php ..............................................199

Ejemplo 69: app models EntityManager.php ............................................................203

Ejemplo 70: fragmento de app models Entity Article.php .................................204

Ejemplo 71: Primera aproximación a la paginación .................................................205

Ejemplo 72: paginacion busqueda.php..........................................................................207

Ejemplo 73: app models core Pagination PaginableInterface.php ................209

Ejemplo 74: articles.php .......................................................................................................212


Ejemplo 75: fragmento de app templates backend entity list.html.twig ....212

Ejemplo 76: declaracion de paginator backend render en lib


TwigViewSlim.php...................................................................................................................213

Ejemplo 77: app models core Pagination PaginatorViewExtension.............213

Ejemplo 78: contenido de Service OvhApi.php .........................................................253

Ejemplo 79: contenido de Tests OvhDomainTest.php ............................................255

Ejemplo 80: .gitignore ............................................................................................................255

Ejemplo 81: config.php.dis...................................................................................................255

Ejemplo 82: ejemplo de formulario de paypal ............................................................256


Conclusiones finales
Aunque te parezca un tópico espero que hayas disfutado con la lectura y pues-
ta en práctica de los ejemplos tanto como lo he hecho yo preparándolos.
Quien me conoce personalmente sabe que vivo por y para la programación. Mi
formación en PHP es muy limitada y constantemente me sorprendo a mí mismo
con cosas nuevas que aprendo sobre este lenguage de programación.
Confieso, que al igual que muchos detractores de PHP, lo considero un lengua-
je poco robusto debido, sobre todo, al hecho de no ser un lenguaje compilado.
En cambio, reconozco que la comunidad de programadores que hay detrás de
él y, sobre todo, la de Symfony, suplen con creces las deficiencias que pudiera
tener, eso sí, hay que aplicar con mano dura a la hora de programar, no solo
con su formato (PSR-0 a PSR-2), sino con la especialización de las clases y
su aplicación en niveles de interfaz, clase abstracta y clase final; empleo de
patrones de diseño, refactorización del código escrito, etc.
Cosas que cuando adquieras la práctica verás necesarias en la medida que te
aportan facilidad en el mantenimiento del código y facilidad general en la lectura del
mismo y el de otros colegas que escriban siguiendo los mismos principios que tú.
Uses o no uses la aplicación My-simple-web, que ha servido como hilo conduc-
tor para el desarrollo de este libro, o la tengas como base en los desarrollos
que tú mismo escribas, espero haber encaminado tus pasos hacia una progra-
mación más profesional.
Si así ha sido, creéme que me harás el hombre más feliz del mundo. En todo
caso, como no es oro todo lo que reluce y al final no soy nada más que un pro-
gramador ilusionado, es posible que no haya explicado todo lo bien que hubiera
querido determinados conceptos. Por ello, estaré encantado de recibir tus críti-
cas, sugerencias, mejoras, dudas, o alabanzas, quién sabe, acerca de este libro
o de programación PHP en general. Eso sí, ten en cuenta mis limitaciones, como
te digo, este que te habla, no es más que un sencillo programador, muy ilusio-
nado, pero sencillo, por tanto no esperes recibir respuestas tajantes rozando lo
«divino», serán comentarios de un compañero a otro compañero.
Sea como sea, he preparado un apartado en mi web personal con el fin de re-
coger todas esas cuestiones:
http://www.joseluislaso.es/books/programacion-php-profesional-con-slim-paris-y-twig
Quedo a la espera de tus noticias.
Atentamente,
Joseluis Laso
9 788426 721600

S-ar putea să vă placă și