Last active
April 22, 2023 19:16
-
-
Save AlexSayHello/57a9a14bb84fe5ed5da1e47843bb642d to your computer and use it in GitHub Desktop.
Crea tu propio framework PHP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Crea tu propio framework PHP</title> | |
<link rel="stylesheet" href="https://stackedit.io/res-min/themes/base.css" /> | |
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script> | |
</head> | |
<body><div class="container"><h1 id="crea-tu-propio-framework-php">Crea tu propio framework PHP</h1> | |
<p>Este documento es la traducción al español de una serie de artículos escritos por Fabien Potencier llamados <strong>“Create your own PHP Framework”</strong>, disponibles en la documentación de Symfony en este <a href="http://symfony.com/doc/master/create_framework/index.html">enlace</a>. En dichos artículos se expone una forma sencilla de construir un pequeño framework en PHP utilizando <strong>componentes</strong> de Symfony. He realizado esta traducción debido a que el framework <strong>Artisan</strong> de <strong>Softcontrol</strong> esta basado en los componentes del framework Symfony, con lo cual, leyendo esta documentación, se pueden obtener una idea del funcionamiento a bajo nivel de <strong>Artisan</strong> para aquella persona que desee desarrollar utilizando el framework de Softcontrol.</p> | |
<h2 id="tabla-de-contenidos">Tabla de contenidos</h2> | |
<p><div class="toc"> | |
<ul> | |
<li><a href="#crea-tu-propio-framework-php">Crea tu propio framework PHP</a><ul> | |
<li><a href="#tabla-de-contenidos">Tabla de contenidos</a></li> | |
<li><a href="#introducción">Introducción</a><ul> | |
<li><a href="#por-qué-te-gustaría-crear-tu-propio-framework">¿Por qué te gustaría crear tu propio framework?</a></li> | |
<li><a href="#antes-de-empezar">Antes de empezar</a></li> | |
<li><a href="#bootstraping-arranque">Bootstraping (Arranque)</a><ul> | |
<li><a href="#gestión-de-dependencias">Gestión de dependencias</a></li> | |
</ul> | |
</li> | |
<li><a href="#nuestro-proyecto">Nuestro proyecto</a></li> | |
</ul> | |
</li> | |
<li><a href="#el-componente-httpfoundation">El componente HttpFoundation</a><ul> | |
<li><a href="#programamando-orientado-a-objectos-con-el-componente-httpfoundation">Programamando orientado a objectos con el componente HttpFoundation</a><ul> | |
<li><a href="#autocarga-de-clases">Autocarga de clases</a></li> | |
</ul> | |
</li> | |
</ul> | |
</li> | |
<li><a href="#el-controlador-frontal-front-controller">El Controlador Frontal (Front Controller)</a></li> | |
<li><a href="#el-componente-de-enrutamiento-rounting-component">El Componente de Enrutamiento (Rounting Component)</a></li> | |
<li><a href="#plantillas-templating">Plantillas (Templating)</a></li> | |
<li><a href="#el-componente-httpkernel-el-resolver-controller">El Componente HttpKernel: el Resolver Controller</a></li> | |
<li><a href="#separación-de-preocupaciones">Separación de preocupaciones</a></li> | |
<li><a href="#testeo-unitario-unit-testing">Testeo unitario (Unit Testing)</a></li> | |
<li><a href="#el-componente-eventdispatcher">El componente EventDispatcher</a></li> | |
<li><a href="#el-componente-httpkernel-httpkernelinterface">El componente HttpKernel: HttpKernelInterface</a></li> | |
<li><a href="#el-componente-httpkernel-la-clase-httpkernel">El componente HttpKernel: La clase HttpKernel</a></li> | |
<li><a href="#el-componente-dependencyinjection">El componente DependencyInjection</a></li> | |
</ul> | |
</li> | |
</ul> | |
</div> | |
</p> | |
<h2 id="introducción">Introducción</h2> | |
<p>Symfony es un conjunto reusable de componentes autonomons, desacoplados y cohesivos escritos en PHP que resuelve problemas comunes del desarrollo web.</p> | |
<p>En lugar de usar estos componentes de bajo nivel, tu puedes usar el framework web full-stack que esta listo para ser usado, el cual se basa en estos componentes… o tu puedes crear tu propio framework. Este tutorial es sobre lo último.</p> | |
<h3 id="por-qué-te-gustaría-crear-tu-propio-framework">¿Por qué te gustaría crear tu propio framework?</h3> | |
<p>En primer lugar, ¿Por qué te gustaría crear tu propio framework? Si miras alrededor, todo el mundo te dira que es una mala reinventar la rueda y que es mejor elegir un framework existente y olvidarse de crear un conjunto propio. La mayoria de las veces estan en lo cierto pero hay algunas buenas razones para empezar a crear tu propio framework:</p> | |
<ul> | |
<li>Para aprender mas sobre la arquitectura de bajo nivel de los frameworks web en general y sobre el framework Symfony en particular.</li> | |
<li>Para crear un framework adaptado a tu necesidades especificas (asegurate primero que tus necesidades son muy especificas).</li> | |
<li>Para experimentar creando un framework por diversion (con el concepto de aprender y tirar).</li> | |
<li>Para refactorizar una web antigua o existente que necesita una buena dossis de las mejores y mas recientes practicas de desarrollo web.</li> | |
<li>Para demostrar al mundo que se puede crear un framework por tu cuenta pero con poco esfuerzo.</li> | |
</ul> | |
<p>Este tutorial te guiara suavemente a través de la creacion de un framework de desarrollo web, paso a paso. En cada paso, vas a tener un framework completamente funcional que tu puedes usar tal cual o como punto de partida. Empezara con un framework simple y se añadiran nuevas características en cada paso. Con el tiempo, tendrás un framework completo de características.</p> | |
<p>Y por supuesto, cada paso será la ocasión de aprender mas sobre los componentes de Symfony.</p> | |
<blockquote> | |
<p>Si no tienes tiempo para leer el libreo completo, o tu quieres empezar mas rapido, tambien puedes echarle un vistazo a <a href="http://silex.sensiolabs.org/">Silex</a> un micro-framework basado en los Componentes de Symfony. El código es bastante ligero y aprovecha muchos aspectos de los <strong>Componentes de Symfony</strong>.</p> | |
</blockquote> | |
<p>Muchos frameworks web modernos se anuncian a si mismos con frameworks <strong>MVC</strong>. Este tutorial no va a hablar del patrón <strong>MVC</strong>, ya que los componentes de Symfony son capaces de crear cualquier tipo de frameworks, no solo los que siguen la arquitectura <strong>MVC</strong>. de todas formas, si conoces las semantica de <strong>MVC</strong>, este libro trara sobre como crear la parte del <strong>Controlador</strong> del framework. Para el <strong>Modelo</strong> y la <strong>Vista</strong>, realmente depende de tus gustos personales y puedes usar cualquier libreria de terceros (Doctrine, Propel o PDO plano; PHP o Twig para la vista).</p> | |
<p>Al crear un framework, seguir el patron <strong>MVC</strong> no es el objetivo correcto. El objetivo principal deberia ser la <strong>Separación de preocupaciones</strong> o <strong>Separación de intereses</strong>; este es probablemente el único patrón de diseño del que debería preocuparse. Los principios fundamentales de los <strong>Componentes de Symfony</strong> estan enfocados en la especificación HTTP. Como tal, el framework que vas a crear debe ser etiquetado mas precisamente como un framwork HTTP or un Framework de <strong>Petición/Respuesta</strong> (<strong>Request/Response</strong>),</p> | |
<h3 id="antes-de-empezar">Antes de empezar</h3> | |
<p>Leer acerca de como crear un framework no es suficiente. Tendras que seguir el tutorial a lo largo de todos sus capitulos y escribir todos los ejemplo incluidos.</p> | |
<p>Para ello, necesita una versión reciente de PHP (5.5.9 o superior es suficiente), un servidor web (como Apache, NGinx o el servidor incorporado de PHP), tambien se necesita un buen conocimiento de PHP y entender la <strong>Programación Orientada a Objectos</strong>.</p> | |
<p>¿Listo para seguir? ¡Sigue leyendo!</p> | |
<h3 id="bootstraping-arranque">Bootstraping (Arranque)</h3> | |
<p>Antes de siquiera pensar en crear el primer framework, necesitas pensar sobre algunas convenciones: donde vas a almacenar el codigo, como vas a llamar a las clases, como vas a hacer referencia a las dependecias externas, etc…</p> | |
<p>Para almacenar tu nuevo framework, crea un directorio en cualquier lugar de tu maquina.</p> | |
<pre><code>$ mkdir framework | |
$ cd framework | |
</code></pre> | |
<h4 id="gestión-de-dependencias">Gestión de dependencias</h4> | |
<p>Para instalar los <strong>Componentes de Symfony</strong> que necesitas para tu Framework, vas a usar <a href="http://packagist.org/about-composer">Composer</a>, administrador de dependencias de proyecto para PHP. Si no lo tienes todavía, <a href="http://symfony.com/doc/master/setup/composer.html">descarga e instalar <strong>Composer</strong> ahora</a>.</p> | |
<h3 id="nuestro-proyecto">Nuestro proyecto</h3> | |
<pre><code>// framework/index.php | |
$input = $_GET['nombre']; | |
printf('Hola %s', $input); | |
</code></pre> | |
<p>Puedes usar el servidor incorporado para probar esta gran aplicación en el navegador.</p> | |
<pre><code>$ php -S 127.0.0.1:4321 | |
</code></pre> | |
<p>De lo contrario, tambien puedes usar tu proprio servidor (Apache, Nginx, etc…)</p> | |
<p>En el siguiente capitulo, vamos a introducir el <strong>Componente HttpFoundation</strong> y ver que es lo que nos ofrece.</p> | |
<h2 id="el-componente-httpfoundation">El componente HttpFoundation</h2> | |
<p>Antes de adentrarnos en el proceso de creaciñon del Framework, vamos a dar un paso hacia atrás y vamos a observar porque te gustaría utilizar un framework en lugar de mantener tus apliaciones de PHP planas (plain-old PHP) como como son. ¿Porque usar un framework es en realidad una buena idea, incluso para la pieza de codigo mas simple y por qué crear tu framework sobre los componentes de Symfony es mejor que crear un framework desde cero.</p> | |
<blockquote> | |
<p>No vamos a hablar de los beneficios obvios y tradicionales cuando trabajamos en apliaciones grande con mas de unos cuantos desarrolladores; internet tiene ya una gran cantidad de recursos sobre este tema. <br> | |
Incluso si la “aplicaciones” que escribimos en el capitulo anterior era bastante simple, adolece de algunos problemas,</p> | |
</blockquote> | |
<pre><code>// framework/index.php | |
$input = $_GET['name']; | |
printf('Hello %s', $input); | |
</code></pre> | |
<p>En primer lugar, si el parametro <code>name</code> no es definido en la cadena de consulta de la URL, obtendras un <strong>Warning de PHP</strong>, asi que vamos a arreglar esto.</p> | |
<pre><code>// framework/index.php | |
$input = isset($_GET['name']) ? $_GET['name']: 'World'; | |
printf('Hello %s', $input); | |
</code></pre> | |
<p>A continuación, esta aplicación no es segura. ¿Te lo puedes creer? Incluso esta simple pieza de código en PHP es vulnerable a una de los problemas de seguridad mas extendidos de la red, XSS (Cross-Site Scripting). Aquí esta una versión mas segura.</p> | |
<pre><code>$input = isset($_GET['name']) ? $_GET['name'] : 'World'; | |
header('Content-Type: text/html; charset=utf-8'); | |
printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-9')) | |
</code></pre> | |
<blockquote> | |
<p>Como habrás notado, asegurar tu código con <code>htmlspecialchars</code> es tedioso y propenso a errores. Esta es una de las razones por la que usaar un motor de plantillas como <a href="http://twig.sensiolabs.org/">Twig</a>, donde el auto-escape esta habilitado por defecto, podría ser una buena idea (y el escape explicito es menos doloroso con el uso del simple filtro <code>e</code>)</p> | |
</blockquote> | |
<p>Como tu mismo has podido ver, el código simple que habíamos escrito no es tan simple si queremos evitar los <strong>warning y notices</strong> de PHP y hacerlo más seguro.</p> | |
<p>Mas allá de la seguridad, este código no es facilmente comprobable (testable). Incluso si no hay mucho que ponder a prueba. Se me ocurre que pruebas unitarias para el trozo de codigo mas simple de PHP no es natural y se siente mal</p> | |
<p>Aquí esta un prueba unitaria provisional para el código anterior.</p> | |
<pre><code>// framework/test.php | |
class IndexTest extends \PHPUnit_Framework_TestCase | |
{ | |
public function testHello() | |
{ | |
$_GET['name'] = 'Fabien'; | |
ob_start(); | |
include 'index.php'; | |
$content = ob_get_clean(); | |
$this->assertEquals('Hello Fabien', $content); | |
} | |
} | |
</code></pre> | |
<blockquote> | |
<p>Si nuestra aplicación fuera solo un poco mas grande habríamos encontrado aun mas problemas, si tienes curiosidad sobre este tema, lee el capitulo “<a href="http://symfony.com/doc/master/introduction/from_flat_php_to_symfony2.html">Symfony versus Flat PHP”</a> de este libro.</p> | |
</blockquote> | |
<p>En este punto, si no estas convecido que la seguridad y el testeo en efecto dos buenas razones para dejar de escribir código viejo y adoptar un framework en su lugar, tu puedes dejar de leer este libro y volver a cualquier código en el que estuviese trabajando antes.</p> | |
<blockquote> | |
<p>Por supuesto, usando un framework deberia darte mucho mas que seguridad y testeabilidad, pero lo mas importante a mantener en mente es que el framework que elijas debe permitirte escribir mejor código rápidamente.</p> | |
</blockquote> | |
<h3 id="programamando-orientado-a-objectos-con-el-componente-httpfoundation">Programamando orientado a objectos con el componente HttpFoundation</h3> | |
<p>Escribir codigo web esta relacionado con interactuar con HTTP. Asi que, los principios fundamentales de nuestro framework debe estar alrededor de la <a href="http://tools.ietf.org/wg/httpbis/">Especificación HTTP</a>.</p> | |
<p>La <strong>Especificación HTTP</strong> describe como el cliente (un navegador por ejemplo) interactua con un servidor (nuestra aplicación a traves de un web server). El dialogo entre el cliente y el servidor es especificado por un mensajes <strong>bien definidos</strong>, peticiones y respuestas: <em>el cliente envia una petición al servidor y basado en esta petición, el servidor retonar una respuesta</em>.</p> | |
<p>En PHP, la petición es representada por las variables globales (<code>$_GET</code>, <code>$_POST</code>, <code>$_FILE</code>, <code>$_COOKIE</code>, <code>$_SESSION</code>…) y la respuesta es generada por las funciones (<code>echo</code>, <code>header</code>, <code>setcookie...</code>)</p> | |
<p>El primer paso hacia un mejor codigo es, probablemente, usar un enfoque orientado a objectos: ese es el objetivo principal del <strong>Componente HttpFoundation</strong>: reemplazando las variables globales y funciones por defecto por una capa <strong>orientada a objectos</strong>.</p> | |
<p>Para usar este componente, añade lo como una dependencia del proyecto.</p> | |
<pre><code>$ composer require symfony/http-foundation | |
</code></pre> | |
<p>Ejecutando este comando tambien descargará el componente de Symfony HttpFoundation y lo instalara en el directorio <code>vendor/</code>. Un archivo <code>composer.json</code> y un <code>composer.lock</code> van a ser creados, conteniento los nuevos requisitos.</p> | |
<pre><code>{ | |
"require": { | |
"symfony/http-foundation": "^3.0" | |
} | |
} | |
</code></pre> | |
<p>El bloque de código muestra el contenido del archivo <code>composer.json</code>(la versión actual puede variar).</p> | |
<blockquote> | |
<h4 id="autocarga-de-clases">Autocarga de clases</h4> | |
<p>Al instalar una nueva dependecia, Composer tambien genera un archivo <code>vendor/autoload.php</code> que permite a cualquier clase ser cargada con facilidad. Sin la autocarga, tu tendrías que requerir el archivo donde la clases se encuentra definida antes de ser capaz de usarla. Pero gracias a <a href="http://www.php-fig.org/psr/psr-4/">PSR-4</a>, podemos dejar que Composer y PHP hagan el trabajo duro por nosotros.</p> | |
</blockquote> | |
<p>Ahora , vamos a reescribir nuestra aplicación para usar las clases <code>Request</code> y <code>Response</code>.</p> | |
<pre><code>// framework/index.php | |
require_once __DIR__ . 'vendor/autoload.php'; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
$request = Request::createFromGlobals(); | |
$input = $request->get('name', 'World'); | |
$response = new Response(sprintf('Hello %s'), htmlspecialchars($input, ENT_QUOTES, 'UTF-8')); | |
</code></pre> | |
<p>$response->send();</p> | |
<p>El método estático <code>createFromGlobals()</code> crea un objecto <code>Request</code> basado en el estado actual de las variables globales de PHP.</p> | |
<p>El método <code>send()</code> envia el objecto <code>Response</code> de vuelva al cliente (Que primero da salida a las cabeceras HTTP seguido del contenido)</p> | |
<p>La principal diferencia con el código anterior es que tienes el control total de los mensajes HTTP. Puedes crear cualquier petición que desees y tu te encargas de enviar la respuesta cuando lo necesites.</p> | |
<blockquote> | |
<p>No tenemos que definir explicitamente la cabecera <code>Content-Type</code> en el código reescrito ya que el <code>charset</code> del objecto <code>Response</code> es por defecto <code>UTF-8</code>.</p> | |
</blockquote> | |
<p>Con la clase <code>Request</code>, posees toda la información de la <strong>petición</strong> en tus manos gracias a una API sencilla y agradable.</p> | |
<pre><code>//la URI que es solicitada (ejemplo. /about) quitando los parametros de la consulta | |
$request->getPathInfo(); | |
//devuelve las variables GET y POST respectivamente | |
$request->query->get('foo'); | |
$request->request->get('bar', 'Valor por defecto si la variable bar no existe'); | |
//devuelve las variables del servidor | |
$request->server->get('HTTP_HOST'); | |
//devuelve una instancia de UploadedFile identificada por foo | |
$request->files->get('foo'); | |
//devuelve un valor de una COOKIE | |
$request->cookies->get('PHPSESSID'); | |
//devuelve una cabecera HTTP, con las letras en minuscula y normalizada. | |
$request->headers->get('host'); | |
$request->headers->get('content_type'); | |
$request->getMethod(); //GET, POST, PUT, DELETE, HEAD | |
$request->getLanguages(); //una array de los lenguajes acceptados por el cliente. | |
</code></pre> | |
<p>Tambien puedes simular una petición</p> | |
<pre><code>$request = Request::create('/index.php?name=Fabien'); | |
</code></pre> | |
<p>Con la clase <code>Response</code>, puedes ajustar facilmente la respuesta.</p> | |
<pre><code>$response = new Response; | |
$response->setContent('Hello World'); | |
$response->setStatusCode(200); | |
$response->headers->set('Content-Type', 'text/html'); | |
//configura la cache de las cabeceras HTTP | |
$response->setMaxAge(10); | |
</code></pre> | |
<blockquote> | |
<p>Para depurar una respuesta, convierte la a una <strong>string</strong>; esta devolverá una representación HTTP de la respuesta (cabeceras y contenido).</p> | |
</blockquote> | |
<p>Por último pero no menos importante, estas clases, al igual que todas las clases del código de <strong>Symfony</strong>, ha sido <a href="http://symfony.com/blog/symfony2-security-audit">auditadas</a> por razones de seguridad por una compañia independiente. Y al ser un proyecto de código abierto tambien significa que muchos desarrolladores al rededor del mundo han leido y han arreglado fallos de seguridad potenciales. ¿Cuando fue la última vez que ordeno una auditoria de seguridad para su framework casero?</p> | |
<p>Incluso algo tan simple como obtener la dirección IP del cliente puede ser inseguro.</p> | |
<pre><code>if ($myIp === $_SERVER['REMOTE_ADDR']) { | |
//el cliente es conocido por lo que le damos mas privilegios | |
} | |
</code></pre> | |
<p>Esto funciona perfectamente bien hasta que añades un proxy inverso delante de los servidores de producción, en este punto, tu vas a tener que cambiar tu codigo para que funcione tanto en tus equipo de desarrollo (donde no tienes un proxy) y en tus servidores.</p> | |
<pre><code>if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] ||| $myIp === $_SERVER['REMOTE_ADDR']) { | |
//el cliente es conocido por lo tanto le damos mas privilegios. | |
} | |
</code></pre> | |
<p>Usando el metodo <code>Request::getClientIp()</code> te habría proporcinado el comportamiento correcto desde el primer día (y tambien habría cubierto el caso de que tuvieres proxies encadenados).</p> | |
<pre><code>$request = Request::createFromGlobals(); | |
if ($myIp === $request->getClientIp()) { | |
//el cliente es conocido por lo tanto le damos mas privilegios | |
} | |
</code></pre> | |
<p>Y hay un beneficio añadido, esto es <strong>seguro</strong> por defecto. ¿Que significa esto?. El valor <code>$_SERVER['HTTP_X_FORWARDED_FOR'] no es de confianza</code>ya que puede ser manipulado por el usuario final cuando no hay un proxy. Por lo tanto, si usas este código en producción sin un proxy, se hace facil abusar de tu sistema. Ese no es el caso con el método <code>getClientUp()</code> ya que debe confiar solo en los proxies inversor añadidos mediante el metodo <code>setTrustedProxies()</code>:</p> | |
<pre><code>Request::setTrustedProxies(array('10.0.1')); | |
if ($myIp === $request->getClientIp(true)) { | |
//el cliente es conocido, por lo tanto le damos mas privilegios. | |
} | |
Por lo tanto, el método `getClientIp()` funciona de forma segura en todas las circunstancias. Puedes usarlo en todos tus proyectos, sea cual sea la configuración, este se comportara de forma correcta y segura. Este es una de las metas al usar un framework. Si tuvieras que escribir un framework desde cero, tendrias qye pesar en todos estos casos por ti mismo. ¿Por qué no usar una tecnología que ya funciona. | |
</code></pre> | |
<blockquote> | |
<p>Si quieres aprender mas sobre el <strong>Componete HttpFoundation</strong>, puedes echarle un vistazo a la API de <a href="http://api.symfony.com/3.1/Symfony/Component/HttpFoundation.html">HttpFoundation</a> o leer su <a href="http://symfony.com/doc/current/components/http_foundation.html">documentación</a> dedicada.</p> | |
</blockquote> | |
<p>Lo creas o no tenemos nuestro primer framework. Puedes para aquí si quieres. Usando solo el <strong>Componente HttpFoundation de Symfony</strong> ya permite escribir mejor código y mas testeable. Esto tambien te permite escribir código más ya que muchos problemas del día a día ya se ha resuelto para tí.</p> | |
<p>Como cuestión de hecho, los proyectos como drupal han adoptado el <strong>componente HttpFoundation;</strong> si funciona para ellos, es probable que también funcione para tí. No reinventes la rueda.</p> | |
<p>Casi olvidamos hablar de un beneficio adicional: el uso del <strong>Componente HttpFoundation</strong> es el comienzo de una mejor interoperatibilidad ente todas las aplicaciones y frameworks que lo usen (como Symfony, Drupal 8, phpBB 4, ezPublish, Laravel, Silex y muchos mas).</p> | |
<h2 id="el-controlador-frontal-front-controller">El Controlador Frontal (Front Controller)</h2> | |
<p>Hasta ahora, nuestra aplicación es simplista ya que solo tiene una página. Para condimentar las cosas un poco, vamos a ir a lo loco y añadir otra página que se despida del usuario (que diga “adios”).</p> | |
<pre><code>// framework/bye.php | |
require_once __DIR__ . '/vendor/autoload.php'; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
$request = Request::createFromGlobals(); | |
$response = new Response('Goodbay!'); | |
$response->send(); | |
</code></pre> | |
<p>Como puedes ver por ti mismo, gran parte del código es exactamente el mismo que el que escribimos en la primera página. Vamos a extraer el código común que podemos compartir entre todas nuestras páginas. Código compartido suena una como un buen plan para crear nuestro primer framework “real”.</p> | |
<p>La forma en PHP de realizar la refactorización seria probablemente la creación de un archivo para incluir.</p> | |
<pre><code>//framework/init.php | |
require_once __DIR__ . '/vendor/autoload.php'; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Componene\HttpFoundation\Response; | |
$request = Request::createFromGlobals(); | |
$response = new Response(); | |
</code></pre> | |
<p>Veamos esto en acción</p> | |
<pre><code>//framework/index.php | |
require_once __DIR__ . 'init.php'; | |
$input = $request->get('name', 'World'); | |
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); | |
$response->send(); | |
</code></pre> | |
<p>Y para la página de <strong>despedida:</strong></p> | |
<pre><code>require_once __DIR__ . '/init.php'; | |
$response->setContent('Goodbye!'); | |
$response->send(); | |
</code></pre> | |
<p>Hemos movido de hecho la mayor parte del código a un lugar central, pero esto no se siente como una buena <strong>abstracción</strong>, ¿cierto?. Todavía tenemos el metodo <code>send()</code> para todas las páginas, nuestras paginas no lucen como plantillas y seguimos sin ser capaces de testear este código correctamente.</p> | |
<p>Ademas, añadir una nueva página quiere decir que tenemos que crear un nuevo script PHP cuyo nombre es expuesto al usuario final a través de la URL (<code>http://127.0.0.1:4321.php</code>). Esto es porque el envío de la petición es realizado por directamente por el servidor web. Podría ser una buena idea mover este envío a nuestro código para mayor flexibilidad. Esto puede lograrse facilmente mediante el enrutamiento de todas las solicitudes del cliente hacía un único script PHP.</p> | |
<blockquote> | |
<p>La exposición de un único script PHP al usuario final es un patrón de diseño llamado el <a href="http://symfony.com/doc/master/introduction/from_flat_php_to_symfony2.html#from-flat-php-front-controller"><strong>Controlador Frontal (Front Controller)</strong></a></p> | |
</blockquote> | |
<p>Dicho script podría ser como el siguiente.</p> | |
<pre><code>//framework/front.php | |
require_once __DIR__ . '/vendor/autoload.php'; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
$request = Request::createFromGlobals(); | |
$response = new Response(); | |
$map = array( | |
'/hello' => __DIR__ . '/hello.php', | |
'bye' => __DIR__ . '/bye.php'; | |
); | |
$path = $request->getPathInfo(); | |
if (isset($map[$map])) { | |
require $map[$map]; | |
} | |
else { | |
$response->setStatusCode(404); | |
$response->setContent('Not Found'); | |
} | |
$response->send(); | |
</code></pre> | |
<p>Y aquí esta, por ejemplo, el nuevo script <code>hello.php</code>:</p> | |
<pre><code>//framework/hello.php | |
$input = $request->get('name', 'World'); | |
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); | |
</code></pre> | |
<p>En el script <code>front.php</code> , la variable array <code>$map</code> asocia las rutas URL con su correspondiente ruta del script PHP.</p> | |
<p>Como bonus adicional, si el cliente pregunta por una ruta que no esta definida en el mapa de URL, devolvemos una pagina 404 personalizada, ahora tienes el control de tu web.</p> | |
<p>Para acceder a una página, ahora debes usar el script <code>front.php</code>.</p> | |
<ul> | |
<li><code>http://127.0.0.1:4321/front.php/hello?name=Fabien</code></li> | |
<li><p><code>http://127.0.0.1:4321/front.php/bye</code></p> | |
<p><code>/hello</code> y <code>/bye</code> son las rutas de las páginas.</p></li> | |
</ul> | |
<blockquote> | |
<p>Muchos servidores como Apache o Nginx son capaces de reescribir la url entrante y eliminar el script del controlador fontral asi que tus usuarios pueden escribir <code>http://127.0.0.1:4321/hello?name=Fabien</code>, el cual luce mucho mejor.</p> | |
</blockquote> | |
<p>El truco es el uso del método <code>Request::getPathInfo()</code> el cual devuelve la ruta de la petición eliminado el nombre del script del controlador frontal incluyendo sus subdirectorios.</p> | |
<blockquote> | |
<p>Ni siquiera necesitas configurar un servidor web para probar el codigo. En su lugar, reemplaza <code>$request = Request::createFromGlobals()</code> y llama a algo como <code>$request = Request::create('/hello?name=Fabien')</code> donde el argumento es la ruta de URL que quieres simular.</p> | |
</blockquote> | |
<p>Ahora que el servidor siempre accede al mismo script (<code>front.php</code>) para todas las páginas, podemos asegurar nuestro código aun más moviendo todos los demás archivos fuera del directorio raiz de la web:</p> | |
<p><br></p> | |
<pre><code>example.com | |
├── composer.json | |
├── composer.lock | |
├── src | |
│ └── pages | |
│ ├── hello.php | |
│ └── bye.php | |
├── vendor | |
│ └── autoload.php | |
└── web | |
└── front.php | |
</code></pre> | |
<p>Ahora, configura el directorio raíz de tu servidor web para que apunte a <code>web/</code> y todos los demás archivo no serán accesibles desde el cliente.</p> | |
<p>Para comprobar los cambios en un navegador (<code>http://localhost:4321/hello/?name=Fabien</code>), ejecuta el servidor incorporado de PHP.</p> | |
<pre><code>$ php -S 127.0.0.1:4321 -t web/ web/front.php | |
</code></pre> | |
<blockquote> | |
<p>Para esta nueva estructura de trabajo, tendrás que ajustar algunas rutas y varios archivos PHP, los cambios se dejan como un ejercicio para el lector.</p> | |
</blockquote> | |
<p>La última cosa que es repetida en cada página es la llamada a <code>setContent()</code>. Podemos convertir todas las páginas en “plantillas” reproduciendo el contenido y llamando a <code>setContent()</code> directamente desde el script del controlador frontal:</p> | |
<pre><code>//example.web/web/front.php | |
//... | |
$path = $request->getPathInfo(); | |
if (isset($map[$map])) { | |
ob_start(); | |
include $map[$path]; | |
$response->setContent(ob_get_clean()); | |
} | |
else { | |
$response->setStatusCode(404); | |
$response->setContent('Not Found'); | |
} | |
//... | |
</code></pre> | |
<p>Y el script <code>hello.php</code> puede ser convertido a una plantilla.</p> | |
<pre><code><!-- example.com/src/pages/hello.php --> | |
<?php $name = $request->get('name', 'World'); | |
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> | |
</code></pre> | |
<p>Tenemos la primera versión de nuestro framework</p> | |
<pre><code>// example.com/web/front.php | |
require_once __DIR__.'/../vendor/autoload.php'; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
$request = Request::createFromGlobals(); | |
$response = new Response(); | |
$map = array( | |
'/hello' => __DIR__.'/../src/pages/hello.php', | |
'/bye' => __DIR__.'/../src/pages/bye.php', | |
); | |
$path = $request->getPathInfo(); | |
if (isset($map[$path])) { | |
ob_start(); | |
include $map[$path]; | |
$response->setContent(ob_get_clean()); | |
} else { | |
$response->setStatusCode(404); | |
$response->setContent('Not Found'); | |
} | |
$response->send(); | |
</code></pre> | |
<p>La adición de una nueva página consta de dos pasos: añadir una entrada en el mapa de URL y crear una plantilla PHP en <code>src/pages/</code>. Desde una plantilla, obtener los datos de la <strong>Petición</strong> a través de la variable <code>$request</code> y definir las cabeceras de la <strong>Respuesta</strong> mediante la variable <code>$respose</code>.</p> | |
<blockquote> | |
<p>Si decides parate aquí, probablemente puedas mejorar tu framework extrayendo el mapa de URL a un archivo de configuración.</p> | |
</blockquote> | |
<h2 id="el-componente-de-enrutamiento-rounting-component">El Componente de Enrutamiento (Rounting Component)</h2> | |
<p>Antes de que empecemos a bucear en el <strong>Componente de Enrutamiento (Routing Component)</strong>, vamos a refactorizar nuestro framework actual solo un poco para hacer las plantillas aún mas legibles.</p> | |
<pre><code>// example.com/web/front.php | |
require_once __DIR__ . '/../vendor/autoload.php'; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
$request = Request::createFromGlobals(); | |
$map = array( | |
'/hello' => 'hello', | |
'bye' => 'bye' | |
); | |
$path = $request->getPathInfo(); | |
if (isset($map[$path])){ | |
ob_start(); | |
extract($request->query->all, EXTR_SKIP); | |
include sprintf(__DIR__ . '/../src/pages/%s.php', $map[$path]); | |
$response = new Response(ob_get_clean()); | |
} | |
else { | |
$response = new Response('Not Found', 404); | |
} | |
$response->send(); | |
</code></pre> | |
<p>Ya que ahora extraemos los parámetros de la consulta de la petición, simplifica la plantilla de la siguiente forma:</p> | |
<pre><code><!-- example.com/src/pages/hello.php --> | |
Hello <?php echo htmlspecialchars(isset($name) ? $name : 'World', ENT_QUOTES, 'UTF-8')?> | |
</code></pre> | |
<blockquote> | |
<p>Nota del traductor: <br> | |
La función <code>extract()</code> importa una serie de variables a la tabla de símbolos actual desde un array, quiere decir que, dado un determinado array asociativo, esta función creara una variable en el ámbito correspondiente donde la clave del array será el valor de la variable y el valor del elemento se corresponderá con el valor de la variable. Por ese motivo, en la plantilla <code>hello.php</code> se puede observar como se utilizan los nombres de los parámetros de la petición como variables. Este método es muy flexible aunque poco recomendado debido a los problemas de seguridad y flexibilidad que puede acarrear la creación de variables dinámicas.</p> | |
<p>Por último, quería destacar la expresión utilizada en la función <code>htmlspecialchars()</code>, se trata de una expresión lógica utilizando el <strong>operador ternario</strong>, que tiene la siguiente estructura: <code>foo ? bar : baz</code>. El cual evalúa el valor de <code>bar</code> y si este es verdadero evaluara la expresión <code>bar</code>, de lo contrario evaluara el valor <code>baz</code>. Se podría decir que es una forma contraída de una estructura condicional <code>if-else</code>.</p> | |
</blockquote> | |
<p>Ahora, estamos en buena forma para añadir nuevas características.</p> | |
<p>Un aspecto muy importante de cualquier web es la forma de sus URLs. Gracias al mapa de URL, hemos desacoplado la URL del código que genera la respuesta, pero esto no es todavía lo suficiente flexible. Por ejemplo, podríamos querer soportar rutas dinámicas que permitan incrustar datos directamente dentro de la URL (ejemplo: <code>hello/Fabien</code>) es vez de apoyarse en una cadena de consulta (ejemplo: <code>/hello?name=Fabien</code>).</p> | |
<p>Para soportar esta carácteristicas, añade el <strong>Componente de Enrutamiento de Symfony (Symfony Routing component)</strong> como una dependencia.</p> | |
<pre><code>$ composer require symfony/routing | |
</code></pre> | |
<p>En lugar de una array para el mapa URL, el <strong>componente de Enrutamiento</strong> se basa en una instancia de <code>RouteCollection</code>:</p> | |
<pre><code>use Symfony\Component\Routing\RouteCollection; | |
$routes = new RouteCollection(); | |
</code></pre> | |
<p>Vamos a añadir una ruta que describa la URL <code>/hello/SOMETHING</code> y a añadir otra para un simple <code>/bye</code>:</p> | |
<pre><code>use Symfony\Component\Routing\Route; | |
$routes->add('hello', new Route('/hello/{name}', array('name' => 'World'))); | |
$routes->add('bye', new Route('/bye')); | |
</code></pre> | |
<p>Cada entrada en la colección es definida por un nombre (<code>hello</code>) y una instancia de <code>Route</code>, la cual es definida por un patrón de ruta (<code>/hello/{name}</code>) y un array con valores por defectos para los atributos de la ruta (<code>array('name' => 'World')</code>).</p> | |
<blockquote> | |
<p>Lee la documentación del componente de Enrutamiento para aprender mas sobre su muchas características como la generación de URL, atributos requeridos, forzado de método HTTP, cargadores para archivos YAML y XML, dumpers para mejorar el rendimiento de las reglas de reescritura y mucho mas.</p> | |
</blockquote> | |
<p>Basado en la información en la información almacenada en la instancia <code>RouteCollection</code>, una instancia de <code>UrlMatcher</code> puede emparejar URL paths.</p> | |
<pre><code>use Symfony\Component\Routing\RequestContext; | |
use Symfony\Component\Routing\UrlMatcher; | |
$context = new Request(); | |
$context->fromRequest(); | |
$matcher = new UrlMatcher($routes, $context); | |
$attributes = $matcher->match($request->getPathInfo()); | |
</code></pre> | |
<p>El método <code>match()</code> toma la ruta de una petición y devuelve un array de atributos (nota que la ruta emparejada es automáticamente almacenada bajo el atributo especial <code>_route</code>):</p> | |
<h2 id="plantillas-templating">Plantillas (Templating)</h2> | |
<h2 id="el-componente-httpkernel-el-resolver-controller">El Componente HttpKernel: el Resolver Controller</h2> | |
<h2 id="separación-de-preocupaciones">Separación de preocupaciones</h2> | |
<h2 id="testeo-unitario-unit-testing">Testeo unitario (Unit Testing)</h2> | |
<h2 id="el-componente-eventdispatcher">El componente EventDispatcher</h2> | |
<h2 id="el-componente-httpkernel-httpkernelinterface">El componente HttpKernel: HttpKernelInterface</h2> | |
<h2 id="el-componente-httpkernel-la-clase-httpkernel">El componente HttpKernel: La clase HttpKernel</h2> | |
<h2 id="el-componente-dependencyinjection">El componente DependencyInjection</h2></div></body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment