Adaptando un sitio en Symfony2 para Facebook

Nuestro cliente  solicitó que partes del sitio que estábamos manteniendo pudiera accederse desde una tab de facebook. Más que como un trabajo lo vimos como un desafío: ¿Como agregar un sitio a un tab de facebook haciendo la menor cantidad de cambios posibles?

Luego de un poco de análisis, advertimos que lo único que debía cambiar en el sitio eran las vistas. Los datos a mostrar eran los mismos, pero las vistas tenían que ser obviamente adaptadas para Facebook.

El primer problema fue cómo detectar cuando entrabamos por Facebook o por medio del sitio a las mismas secciones y poder navegar de ahi en más ya sea con el skin de facebook o el normal.

Cuando Facebook muestra un tab, lo abre por medio de un post con el parametro signed_request pero si hago click sobre un link de la página, al resto de las páginas no llegaba ningun parámetro.

Entonces pensamos primero en guardar en sesión la existencia de ese parámetro o no, pero no pudo ser posible ya que la sessión de una persona que navega el sitio por Facebook y por la web son la misma. Entonces si entraba por Facebook al sitio, pero luego iba con el explorador al mismo sitio pero fuera de Facebook, vería el sitio como si estuviera dentro de facebook, o sea mal. Tampoco podiamos guardarlo en una cookie porque sucedía el mismo problema.

Como no se nos ocurrió de que forma persistir el estado cuando se llamaba a una pagina, comenzamos a pensar en pasar el estado en todos los links. Entonces decidimos que si la página era invocada con un parámetro Facebook (en su querystring) significa que debia mostrarse la versión Facebook.

Como habia muchos links en las páginas y no estaban siendo procesados por Symfony (el sitio era un hibrido entre symfony y un sitio legado. Ver post Nuestra experiencia extrangulando un sitio) no teníamos control sobre cómo se generaban, por ello no podíamos desde PHP fácilmente agregarle el parámetro ?facebook a todos los links.

Se nos ocurrió entonces utilizar Javascript. Hicimos que el layout de Facebook tenga un pequeño bloque de Javascript que busca todos los tags A y les agrega a su href el parámtro de facebook.

De esta manera cualquier link de la página pasó a tener el parámetro agregado dinamicamente y cuando se le hacía click a la siguiente página que se accedía traía también el parámetro deseado y se sabía que había que mostrarla en modo facebook.

Desde el punto de vista del controller era fácil lo que se debía hacer, había que agregar un condición-if en todos los actions, que comprobara ese parámetro y si estaba presente, mostrar un template para Facebook, en caso contrario mostrar el template default.

El código era siempre muy similar al siguiente:

	public function peopleAction() {

		... codigo del action ...

		$datos_vista = array( .... datos_vista .... );
		if($this->getRequest()->query->has("facebook")) {
			return $this->render("MyBundle:Default:people-facebook.html.twig");
		}
		return $datos_vista;
	}

Siguiendo la filosofía DRY tener que hacer eso en cada uno de los action era muy poco agradable, asi que comenzamos a pensar como simplificarlo.

La solución se presentó con los event listeners de Symfony. Especificamente el kernel.view que se ejecuta cuando un action retorna cualquier cosa que no sea un objeto del tipo Response (en nuestro caso un array de datos).

Simplemente definimos un event_listener nuevo (algo así como un filter para quienes vienen de otras tecnologías como en Java) este verificará la existencia del parámetro y si lo encontraba modificará cual es el template a usar.

El filter quedó así:

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;

class FacebookListener {
	private $logger;

	public function __construct($logger) {
		$this->logger = $logger;
	}

	/**
	 * Verifica si el request fue hecho por medio de facebook y en ese caso
	 * cambia la vita de destino por una adaptada a facebook
	 *
	 * @param FilterResponseEvent $event
	 */
	public function onKernelView(GetResponseForControllerResultEvent $event) {
		$request = $event->getRequest();
		if($request->query->has("facebook")) {
			$template = $request->attributes->get('_template');
			$template = preg_replace("/\./", "-facebook.", $template, 1);

			$request->attributes->set('_template', $template);
		}
	}
}

Luego de esto había que agregarlo al service container y taggearlo de forma que Symfony lo utilizara correctamente. Para eso modificamos el archivo services.xml agregando la siguiente seccion

	    <service id="facebook.listener" class="MyBusiness\MyBundle\Listener\FacebookListener">
	    	<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="10" />
	    	<argument type="service" id="logger" />
	    </service>

¿Que hay para destacar en este fragmento de xml? Principalmente que el logger lo agregamos porque en algun momento estábamos haciendo muchas pruebas y necesitabamos un log, pero al final quitamos todos los mensajes y ya no se usa, pero igual no hace mal tenerlo por cualquier cosa.

Luego y lo más importante, es el atributo priority=10 cuando definimos el tag.

Sucede que Symfony ya tiene un kernel.view listener configurado, que se encarga de procesar el template cuando se retorna de una vista un array. Este listener de Symfony está implementado en la clase TemplateListener y tiene una prioridad de cero.

Como queríamos modificar el template a mostrar, necesitabamos que nuestro listener tuviera mayor prioridad que el de Symfony, por eso le pusimos priority=10. Realmente no generamos un Response en nuestro listener, y solo modificamos un dato, symfony le pasa el control al siguiente listener en prioridad (en este caso TemplateListener) que se encarga de hacer su tarea habitual.

Espero que les haya interesado este post, fue en verdad muy interesante y divertido para nosotros investigarlo y con esto confirmar que Symfony 2 es uno de los mejores frameworks para PHP según nuestra humilde opinión. 🙂


Discussion Area - Leave a Comment