Funcionamiento interno

Parece que quieres entender cómo funciona y cómo extender Symfony2. ¡Eso me hace muy feliz! Esta sección es una explicación en profundidad de Symfony2 desde dentro.

Nota

Necesitas leer esta sección sólo si quieres entender cómo funciona Symfony2 detrás de la escena, o si deseas extender Symfony2.

Descripción

El código Symfony2 está hecho de varias capas independientes. Cada capa está construida en lo alto de la anterior.

Truco

La carga automática no la gestiona la plataforma directamente; Esto se hace utilizando el cargador automático de Composer (vendor/autoload.php), el cual está incluido en el archivo src/autoload.php.

Componente HttpFoundation

Al nivel más profundo está el componente HttpFoundation. HttpFoundation proporciona los principales objetos necesarios para hacer frente a HTTP. Es una abstracción orientada a objetos de algunas funciones y variables nativas de PHP:

  • La clase Symfony\Component\HttpFoundation\Request resume las principales variables globales de PHP, tales como $_GET, $_POST, $_COOKIE, $_FILES y $_SERVER;
  • La clase Symfony\Component\HttpFoundation\Response abstrae algunas funciones PHP como header(), setcookie() y echo;
  • La clase Symfony\Component\HttpFoundation\Session y la interfaz Symfony\Component\HttpFoundation\SessionStorage\SessionStorageInterface, abstraen la gestión de sesiones y las funciones session_*().

Componente HttpKernel

En lo alto de HttpFoundation está el componente HttpKernel. HttpKernel se encarga de la parte dinámica de HTTP; es una fina capa en la parte superior de las clases Petición y Respuesta para estandarizar la forma en que se manejan las peticiones. Este, también proporciona puntos de extensión y herramientas que lo convierten en el punto de partida ideal para crear una plataforma Web sin demasiado trabajo.

Además, opcionalmente añade configurabilidad y extensibilidad, gracias al componente de inyección de dependencias y un potente sistema de complementos (paquetes).

Ver también

Lee más sobre la Inyección de dependencias y los Paquetes.

Paquete FrameworkBundle

El paquete FrameworkBundle es el paquete que une los principales componentes y bibliotecas para hacer una plataforma MVC ligera y rápida. Este viene con una sensible configuración predeterminada y convenios para suavizar la curva de aprendizaje.

El núcleo

La clase Symfony\Component\HttpKernel\HttpKernel es la clase central de Symfony2 y es responsable de procesar las peticiones del cliente. Su objetivo principal es «convertir» un objeto Symfony\Component\HttpFoundation\Request a un objeto Symfony\Component\HttpFoundation\Response.

Cada núcleo de Symfony2 implementa Symfony\Component\HttpKernel\HttpKernelInterface:

function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)

Controladores

Para convertir una Petición a una Respuesta, el núcleo cuenta con un «Controlador». Un controlador puede ser cualquier PHP ejecutable válido.

El núcleo delega la selección de cual controlador se debe ejecutar a una implementación de Symfony\Component\HttpKernel\Controller\ControllerResolverInterface:

public function getController(Request $request);

public function getArguments(Request $request, $controller);

El método getController() devuelve el controlador (un PHP ejecutable) asociado a la petición dada. La implementación predeterminada de (Symfony\Component\HttpKernel\Controller\ControllerResolver) busca un atributo _controller en la petición que representa el nombre del controlador (una cadena «class::method», cómo Bundle\BlogBundle\PostController:indexAction).

Truco

La implementación predeterminada utiliza la clase Symfony\Bundle\FrameworkBundle\EventListener\RouterListener para definir el atributo _controller de la petición (consulta el Evento kernel.request).

El método getArguments() devuelve un arreglo de argumentos para pasarlo al Controlador ejecutable. La implementación predeterminada automáticamente resuelve los argumentos del método, basándose en los atributos de la Petición.

Procesando peticiones

El método handle() toma una Petición y siempre devuelve una Respuesta. Para convertir la Petición, handle() confía en el mecanismo de resolución y una cadena ordenada de notificaciones de evento (consulta la siguiente sección para más información acerca de cada evento):

  1. Antes de hacer cualquier otra cosa, difunde el evento kernel.request —si alguno de los escuchas devuelve una Respuesta, salta directamente al paso 8;
  2. El mecanismo de resolución es llamado para determinar el controlador a ejecutar;
  3. Los escuchas del evento kernel.controller ahora pueden manipular el controlador ejecutable como quieras (cambiarlo, envolverlo, ...);
  4. El núcleo verifica que el controlador en realidad es un PHP ejecutable válido;
  5. Se llama al mecanismo de resolución para determinar los argumentos a pasar al controlador;
  6. El núcleo llama al controlador;
  7. Si el controlador no devuelve una Respuesta, los escuchas del evento kernel.view pueden convertir en Respuesta el valor devuelto por el Controlador;
  8. Los escuchas del evento kernel.response pueden manipular la Respuesta (contenido y cabeceras);
  9. Devuelve la respuesta.

Si se produce una Excepción durante el procesamiento, difunde la kernel.exception y se da la oportunidad a los escuchas de convertir la excepción en una Respuesta. Si esto funciona, se difunde el evento kernel.response; si no, se vuelve a lanzar la excepción.

Si no deseas que se capturen las Excepciones (para peticiones incrustadas, por ejemplo), desactiva el evento kernel.exception pasando false como tercer argumento del método handle().

Funcionamiento interno de las peticiones

En cualquier momento durante el manejo de una petición (la ‘maestra’ uno), puede manejar una subpetición. Puedes pasar el tipo de petición al método handle() (su segundo argumento):

  • HttpKernelInterface::MASTER_REQUEST;
  • HttpKernelInterface::SUB_REQUEST.

El tipo se pasa a todos los eventos y los escuchas pueden actuar en consecuencia (algún procesamiento sólo debe ocurrir en la petición maestra).

Eventos

Cada evento lanzado por el núcleo es una subclase de Symfony\Component\HttpKernel\Event\KernelEvent. Esto significa que cada evento tiene acceso a la misma información básica:

  • getRequestType() — devuelve el tipo de la petición (HttpKernelInterface::MASTER_REQUEST o HttpKernelInterface::SUB_REQUEST);
  • getKernel() — devuelve el núcleo que está procesando la petición;
  • getRequest() — devuelve la Petición procesada actualmente.

getRequestType()

El método getRequestType() permite a los escuchas conocer el tipo de la petición. Por ejemplo, si un escucha sólo debe estar atento a las peticiones maestras, agrega el siguiente código al principio de tu método escucha:

use Symfony\Component\HttpKernel\HttpKernelInterface;

if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
    // regresa inmediatamente
    return;
}

Truco

Si todavía no estás familiarizado con el Despachador de eventos de Symfony2, primero lee la sección del Componente despachador de eventos.

Evento kernel.request

Clase del evento: Symfony\Component\HttpKernel\Event\GetResponseEvent

El objetivo de este evento es devolver inmediatamente un objeto Respuesta o variables de configuración para poder invocar un controlador después del evento. Cualquier escucha puede devolver un objeto Respuesta a través del método setResponse() en el evento. En este caso, todos los otros escuchas no serán llamados.

Este evento lo utiliza el FrameworkBundle para llenar el atributo _controller de la Petición, a través de Symfony\Bundle\FrameworkBundle\EventListener\RouterListener. RequestListener usa un objeto Symfony\Component\Routing\RouterInterface para emparejar la Petición y determinar el nombre del controlador (guardado en el atributo _controller de la Petición).

Evento kernel.controller

Clase del evento: Symfony\Component\HttpKernel\Event\FilterControllerEvent

Este evento no lo utiliza el FrameworkBundle, pero puede ser un punto de entrada para modificar el controlador que se debe ejecutar:

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

public function onKernelController(FilterControllerEvent $event)
{
    $controller = $event->getController();
    // ...

    // el controlador se puede cambiar a cualquier PHP ejecutable
    $event->setController($controller);
}

Evento kernel.view

Clase del evento: Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent

Este evento no lo utiliza el FrameworkBundle, pero lo puedes usar para implementar un subsistema de vistas. Este evento se llama sólo si el controlador no devuelve un objeto Respuesta. El propósito del evento es permitir que algún otro valor de retorno se convierta en una Respuesta.

El valor devuelto por el controlador es accesible a través del método getControllerResult:

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;

public function onKernelView(GetResponseForControllerResultEvent $event)
{
    $val = $event->getControllerResult();
    $response = new Response();

    // ... de alguna manera modifica la Respuesta desde el valor de retorno

    $event->setResponse($response);
}

Evento kernel.response

Clase del evento: Symfony\Component\HttpKernel\Event\FilterResponseEvent

El propósito de este evento es permitir que otros sistemas modifiquen o sustituyan el objeto Respuesta después de su creación:

public function onKernelResponse(FilterResponseEvent $event)
{
    $response = $event->getResponse();

    // ... modifica el objeto Respuesta
}

El FrameworkBundle registra varios escuchas:

  • Symfony\Component\HttpKernel\EventListener\ProfilerListener: recoge los datos de la petición actual;
  • Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener: inyecta la barra de herramientas de depuración web;
  • Symfony\Component\HttpKernel\EventListener\ResponseListener: fija el Content-Type de la respuesta basándose en el formato de la petición;
  • Symfony\Component\HttpKernel\EventListener\EsiListener: agrega una cabecera HTTP Surrogate-Control cuando es necesario analizar etiquetas ESI en la respuesta.

Evento kernel.exception

Clase del evento: Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent

FrameworkBundle registra un Symfony\Component\HttpKernel\EventListener\ExceptionListener el cual remite la Petición a un determinado controlador (el valor del parámetro exception_listener.controller —debe estar en notación clase::método—).

Un escucha de este evento puede crear y configurar un objeto Respuesta, crear y establecer un nuevo objeto Excepción, o simplemente no hacer nada:

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;

public function onKernelException(GetResponseForExceptionEvent $event)
{
    $exception = $event->getException();
    $response = new Response();
    // configura el objeto respuesta basándose en la excepción capturada
    $event->setResponse($response);

    // alternativamente puedes establecer una nueva excepción
    // $exception = new \Exception('Some special exception');
    // $event->setException($exception);
}

Nota

Debido a que Symfony se asegura de que el código de estado de la Respuesta se ajusta al más adecuado dependiendo de la excepción, establecer el estado de la respuesta no funcionará. Si quieres reescribir el código de estado (no deberías, sin una buena razón), configurar la cabecera X-Status-Code:

return new Response('Error', 404 /* ignored */, array('X-Status-Code' => 200));

El despachador de eventos

El despachador de eventos es un componente independiente y es el responsable de mucha de la lógica y flujo subyacente detrás de una petición Symfony. Para más información consulta la documentación del Componente despachador de eventos.

Generador de perfiles

Cuando se activa, el generador de perfiles de Symfony2 recoge información útil sobre cada petición presentada a tu aplicación y la almacena para su posterior análisis. Utiliza el generador de perfiles en el entorno de desarrollo para que te ayude a depurar tu código y mejorar el rendimiento; úsalo en el entorno de producción para explorar problemas después del hecho.

Rara vez tienes que lidiar con el generador de perfiles directamente puesto que Symfony2 proporciona herramientas de visualización como la barra de herramientas de depuración web y el generador de perfiles web. Si utilizas la edición estándar de Symfony2, el generador de perfiles, la barra de herramientas de depuración web, y el generador de perfiles web, ya están configurados con ajustes razonables.

Nota

El generador de perfiles recopila información para todas las peticiones (peticiones simples, redirecciones, excepciones, peticiones Ajax, peticiones ESI; y para todos los métodos HTTP y todos los formatos). Esto significa que para una única URL, puedes tener varios perfiles de datos asociados (un par petición/respuesta externa).

Visualizando perfiles de datos

Usando la barra de depuración web

En el entorno de desarrollo, la barra de depuración web está disponible en la parte inferior de todas las páginas. Esta muestra un buen resumen de los datos perfilados que te da acceso instantáneo a una gran cantidad de información útil cuando algo no funciona como esperabas.

Si el resumen presentado por las herramientas de la barra de depuración web no es suficiente, haz clic en el enlace simbólico (una cadena compuesta de 13 caracteres aleatorios) para acceder al generador de perfiles web.

Nota

Si no se puede hacer clic en el enlace, significa que las rutas del generador de perfiles no están registradas (más adelante hay información de configuración).

Analizando datos del perfil con el generador de perfiles web

El generador de perfiles web es una herramienta de visualización para perfilar datos que puedes utilizar en desarrollo para depurar tu código y mejorar el rendimiento; pero también lo puedes utilizar para explorar problemas que ocurren en producción. Este expone toda la información recogida por el generador de perfiles en una interfaz web.

Accediendo a información del generador de perfiles

No es necesario utilizar el visualizador predeterminado para acceder a la información de perfiles. Pero ¿cómo se puede recuperar información del perfil de una petición específica después del hecho? Cuando el generador de perfiles almacena datos sobre una Petición, también le asocia una ficha; esta ficha está disponible en la cabecera HTTP X-Debug-Token de la Respuesta:

$profile = $container->get('profiler')->loadProfileFromResponse($response);

$profile = $container->get('profiler')->loadProfile($token);

Truco

Cuando el generador de perfiles está habilitado pero no la barra de herramientas de depuración web, o cuando desees obtener la ficha de una petición Ajax, utiliza una herramienta como Firebug para obtener el valor de la cabecera HTTP X-Debug-Token.

Usa el método find() para acceder a elementos basándose en algún criterio:

// consigue las 10 últimas fichas
$tokens = $container->get('profiler')->find('', '', 10);

// consigue las 10 últimas fichas de todas las URL que contienen /admin/
$tokens = $container->get('profiler')->find('', '/admin/', 10);

// consigue las 10 últimas fichas de peticiones locales
$tokens = $container->get('profiler')->find('127.0.0.1', '', 10);

Si deseas manipular los datos del perfil en una máquina diferente a la que generó la información, utiliza los métodos export() e import():

// en la máquina en producción
$profile = $container->get('profiler')->loadProfile($token);
$data = $profiler->export($profile);

// en la máquina de desarrollo
$profiler->import($data);

Configurando

La configuración predeterminada de Symfony2 viene con ajustes razonables para el generador de perfiles, la barra de herramientas de depuración web, y el generador de perfiles web. Aquí está por ejemplo la configuración para el entorno de desarrollo:

  • YAML
    # carga el generador de perfiles
    framework:
        profiler: { only_exceptions: false }
    
    # activa el generador de perfiles web
    web_profiler:
        toolbar: true
        intercept_redirects: true
    
  • XML
    <!-- xmlns:webprofiler="http://symfony.com/schema/dic/webprofiler" -->
    <!-- xsi:schemaLocation="http://symfony.com/schema/dic/webprofiler http://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd"> -->
    
    <!-- carga el generador de perfiles -->
    <framework:config>
        <framework:profiler only-exceptions="false" />
    </framework:config>
    
    <!-- activa el generador de perfiles web -->
    <webprofiler:config
        toolbar="true"
        intercept-redirects="true"
        verbose="true"
    />
    
  • PHP
    // carga el generador de perfiles
    $container->loadFromExtension('framework', array(
        'profiler' => array('only-exceptions' => false),
    ));
    
    // activa el generador de perfiles web
    $container->loadFromExtension('web_profiler', array(
        'toolbar'             => true,
        'intercept-redirects' => true,
        'verbose'             => true,
    ));
    

Cuando only-exceptions se establece a true, el generador de perfiles sólo recoge datos cuando tu aplicación lanza una excepción.

Cuando intercept-redirects está establecido en true, el generador de perfiles web intercepta las redirecciones y te da la oportunidad de analizar los datos recogidos antes de seguir la redirección.

Si activas el generador de perfiles web, también es necesario montar las rutas de los perfiles:

  • YAML
    _profiler:
        resource: @WebProfilerBundle/Resources/config/routing/profiler.xml
        prefix:   /_profiler
  • XML
    <import resource="@WebProfilerBundle/Resources/config/routing/profiler.xml" prefix="/_profiler" />
    
  • PHP
    $collection->addCollection($loader->import("@WebProfilerBundle/Resources/config/routing/profiler.xml"), '/_profiler');
    

Dado que el generador de perfiles añade algo de sobrecarga, posiblemente desees activarlo sólo bajo ciertas circunstancias en el entorno de producción. La configuración only-exceptions limita al generador de perfiles a 500 páginas, ¿pero si quieres obtener información cuando la IP cliente proviene de una dirección específica, o para una porción limitada del sitio web? Puedes utilizar una emparejadora de petición:

  • YAML
    # activa el generador de perfiles sólo para peticiones entrantes de la red 192.168.0.0
    framework:
        profiler:
            matcher: { ip: 192.168.0.0/24 }
    
    # activa el generador de perfiles sólo para las URL /admin
    framework:
        profiler:
            matcher: { path: "^/admin/" }
    
    # combina reglas
    framework:
        profiler:
            matcher: { ip: 192.168.0.0/24, path: "^/admin/" }
    
    # usa una instancia emparejadora personalizada definida en el servicio 'custom_matcher'
    framework:
        profiler:
            matcher: { service: custom_matcher }
    
  • XML
    <!-- activa el generador de perfiles sólo para peticiones entrantes de la red 192.168.0.0 -->
    <framework:config>
        <framework:profiler>
            <framework:matcher ip="192.168.0.0/24" />
        </framework:profiler>
    </framework:config>
    
    <!-- activa el generador de perfiles sólo para las URL /admin -->
    <framework:config>
        <framework:profiler>
            <framework:matcher path="^/admin/" />
        </framework:profiler>
    </framework:config>
    
    <!-- combina reglas -->
    <framework:config>
        <framework:profiler>
            <framework:matcher ip="192.168.0.0/24" path="^/admin/" />
        </framework:profiler>
    </framework:config>
    
    <!-- usa una instancia emparejadora personalizada definida en el servicio "custom_matcher" -->
    <framework:config>
        <framework:profiler>
            <framework:matcher service="custom_matcher" />
        </framework:profiler>
    </framework:config>
    
  • PHP
    // activa el generador de perfiles sólo para peticiones entrantes de la red 192.168.0.0
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'matcher' => array('ip' => '192.168.0.0/24'),
        ),
    ));
    
    // activa el generador de perfiles sólo para las URL /admin
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'matcher' => array('path' => '^/admin/'),
        ),
    ));
    
    // combina reglas
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'matcher' => array('ip' => '192.168.0.0/24', 'path' => '^/admin/'),
        ),
    ));
    
    # usa una instancia emparejadora personalizada definida en el servicio 'custom_matcher'
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'matcher' => array('service' => 'custom_matcher'),
        ),
    ));
    
Bifúrcame en GitHub