Un liviano y básico router php para proyectos rápidos y pequeños.
Tabla de contenidos
- Configure
- Routing
- Configure router
- Routes
- Routes Group
- Controllers
- Using attributes to define routes
- Engine
- Dependencies Container
- Services Provider
- Container vs Services
- Request
- Response
- Client Request
- Emitter
- Data Collection
- Views
- DB Connection
- CORS
- Handler
- Users
- Functions
En Apache edita el archivo .htaccess
en la raíz del proyecto:
<IfModule mod_rewrite.c>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
AddDefaultCharset utf-8
Para Nginx agrega lo siguiente en las configuraciones del servidor:
server {
location / {
try_files $uri $uri/ /index.php;
}
}
Por último genera el autoload 1
composer dump-autoload
El proceso de enrutamiento consiste en registrar un conjunto de rutas que serán comparadas con el REQUEST_URI
del lado del cliente (web browser) representado por un objeto Request
(Ver Request). Si una ruta coincide se ejecuta la acción asociada con esta, es decir un método de una clase (controlador) que devolverá una respuesta representada por un objeto de Response
(Ver Response). Si la URI solicitada no corresponde con ninguna de las rutas registradas, el router lanzará un RouteNotFoundException
, en cambio si el método de petición no es soportado por el router lanzará un UnsupportedRequestMethodException
.
El método Router::handleRequest
procesa el Request, hace el enrutamiento y devuelve un objeto Response que es enviado al cliente (Ver Emitter).
use Application\Http\FooController;
use rguezque\Forge\Exceptions\RouteNotFoundException;
use rguezque\Forge\Router\{
Emitter,
Request,
Response,
Route,
Router
};
require __DIR__.'/vendor/autoload.php';
$router = new Router;
$router->addRoute(new Route('GET', '/', FooController::class, 'indexAction'));
$router->addRoute(new Route('GET', '/hola/{nombre}', FooController::class, 'holaAction'));
$router->addRouteGroup('/foo', function(RouteGroup $group) {
$group->addRoute(new Route('GET', '/', FooController::class, 'fooAction'));
$group->addRoute(new Route('GET', '/bar', FooController::class, 'barAction'));
});
try {
$response = $router->handleRequest(Request::fromGlobals());
} catch(RouteNotFoundException $e) {
$response = new Response(
sprintf('<h1>Not Found</h1>%s', $e->getMessage()),
404
);
} catch(UnsupportedRequestMethodException $e) {
$response = new Response(
sprintf('<h1>Method Not Allowed</h1>%s', $e->getMessage()),
405
);
}
Emitter::emit($response);
El router puede recibir como parámetros un array asociativo con las opciones disponibles de configuración. Las opciones disponibles son:
-
'router.basepath'
: Especifica un directorio base en caso de que el router este alojado en un subdirectorio de la raíz del servidor. -
'router.supported.request.methods'
: Define un array que sobrescribe los métodos de petición http permitidos por el router. Por default el router aceptaGET
yPOST
. -
'router.views.path'
: (Opcional) Especifica un directorio global donde se buscarán por default los archivos de templates para las vistas, ya sea para rutas que renderizan vistas (RouteView
) o bien para la claseView
.Nota: Si se define un nuevo directorio de templates al crear un objeto de
View
este tendrá preferencia sobre el directorio global.
use rguezque\Forge\Router\Router;
$router = new Router([
'router.basepath' => '/myapp',
'router.supported.request.methods' => ['GET', 'POST', 'PUT'],
'router.viewspath' => __DIR__.'/templates'
]);
//$router->addRoute(...)
//...
El método Router::addRoute
permite agregar una ruta. Una ruta se representa por una instancia de Route
. Esta clase recibe cuatro parámetros obligatorios; el método de petición, el string path, el nombre del controlador y el nombre del método a ejecutar para dicha ruta. Por default los únicos métodos aceptados por el router son GET
y POST
(Ver Configure router si requieres definir o agregar tus propios métodos http de petición aceptados.).
Por ejemplo si una ruta recibirá una petición POST
:
$router->addRoute(new Route('POST', '/article/save', BlogController::class, 'saveNewAction'));
El router también acepta dos rutas con el mismo string path pero diferente método de petición, nombre y acción a ejecutar.
$router->addRoute(new Route('GET', '/articles', BlogController::class, 'saveNewAction'));
$router->addRoute(new Route('POST', '/articles', BlogController::class, 'updateAction'));
Para definir rutas que solo devuelven una vista, sin tener que definir un controlador, se envía una instancia de RouteView
. Recibe tres parámetros; la definición de la ruta, la ruta a la plantilla y opcionalmente un array asociativo con argumentos a pasar a dicha plantilla. Todos los RouteView
son de tipo GET
por default.
$router->addRoute(new RouteView('/', __DIR__.'/views/homepage.php'));
$router->addRoute(new RouteView('/hello/{name}', __DIR__.'/views/hello.php'));
$router->addRouteGroup('/foo', function(RouteGroup $group) {
$group->addRoute(new RouteView('/', __DIR__.'/views/login_form.php', ['action'=>'/foo/login']));
$group->addRoute(new Route('/foo/login', FooController::class, 'helloAction'));
});
Nota: Si previamente se ha definido el directorio de las vistas en la configuración no es necesario especificar la ruta completa, simplemente el nombre del template (Ver Configure router). Si hay un parámetro nombrado en la definición de ruta de RouteView
se manda como parámetros al template.
Si una ruta tiene wildcards2 nombrados, se recuperan en un objeto Bag
(Ver The Bag Class) a través del método Request::getParameters
y pueden ser tomados a través de Bag::get
usando como clave el parámetro nombrado con que fueron definidos en la ruta. Si la ruta tiene wildcards en forma de expresiones regulares (RegEx) se recuperan con Bag::all
que devuelve un array lineal con los valores enumerados en orden de match (emparejamiento, coincidencia).
//index.php
//...
$router->addRoute(new Route(
'GET',
'/hola/(\w+)/(\w+)',
FooController::class,
'holaAction'
));
$router->addRoute(new Route(
'GET',
'/hello/{name}/{lastname}',
FooController::class,
'helloAction'
));
//...
// FooController.php
//...
public function holaAction(Request $request, Response $response): Response {
list($nombre, $apellido) = $request->getParameters()->all();
return $response->withContent(sprintf('Hola %s %s', $nombre, $apellido));
}
public function helloAction(Request $request, Response $response): Response {
$args = $request->getParameters();
$nombre = $args->get('name');
$apellido = $args->get('lastname');
return $response->withContent(sprintf('Hola %s %s', $nombre, $apellido));
}
//...
Important
No se recomienda definir parámetros nombrados y en forma de expresiones regulares en la misma ruta. De ser así, aunque se devuelven todas las coincidencias no será posible acceder por nombre a las que hayan sido definidas como regex y tendrás que acceder a cada una a través de su posición numérica en el array devuelto por Bag::all
.
Para las rutas que solo devuelven una vista (Ver Routes) es obligatorio que si dicha ruta contiene parámetros deben ser parámetros nombrados, pues se agregan como argumentos al template de la vista.
El método Router::addRouteGroup
permite crear un grupo de rutas bajo un mismo prefijo de ruta. Especifica el prefijo y seguidamente un closure con un parámetro RouteGroup
que permite definir las rutas. Cada ruta heredará el prefijo definido.
$router->addRouteGroup('/foo', function(RouteGrup $group) {
$group->addRoute(new Route('GET', '/', FooController::class, 'holaAction'));
$group->addRoute(new Route('GET', '/hello/{nombre}', FooController::class, 'helloAction'));
$group->addRoute(new Route('GET', '/bar/entry/{id}', FooController::class, 'entryAction'));
});
Lo anterior genera las rutas:
/foo/
/foo/hello/{nombre}
/foo/bar/entry/{id}
Los controladores deben ser solo clases instanciables (no closures3, funciones, objetos o métodos estáticos). Para una mejor lectura del código los nombres de controladores deben tener el sufijo Controller
y los métodos el sufijo Action
, de lo contrario lanzará un BadNameException
.
Los controladores reciben un parámetro Request
y un Response
dependiendo del motor de funcionamiento del router (Ver Engine) y devolverán un Response
o un array
según sea el caso.
// index.php
//...
$router = new Router;
$router->addRoute(new Route('GET', '/hola/{nombre}/{apellido}', FooController::class, 'holaAction'));
//...
// app/Http/FooController.php
namespace App\Http;
use rguezque\Forge\Router\Request;
use rguezque\Forge\Router\Response;
class FooController {
public function holaAction(Request $request, Response $response): Response {
$args = $request->getParameters();
$message = sprintf('Hola %s %s', $args->get('nombre'), $args->get('apellido'));
$response->clear()->withContent($message);
return $response;
}
}
Para recuperar los argumentos como un array asociativo usa el método Bag::all
, donde cada clave de dicho array corresponde al nombre de cada wildcard de la ruta:
//...
$args = $request->getParameters();
$params = $args->all() // Devuelve los parámetros en un array asociativo nombre-valor
Si se hace una petición GET
en la URI solicitada (ejem. /path/?foo=bar&lorem=ipsum
), serán accesibles en $_GET
con el método Request::getQueryParams
.
// Los valores "foo" y "lorem" del ejemplo anterior se recuperan en un objeto Bag con:
$params = $request->getQueryParams()
Esta forma utiliza los atributos de las clases y los métodos para generar y agregar rutas y grupos de rutas. El método Router::addControllersWithAttributes
recibe como argumento un array de nombres de controladores previamente con atributos definidos, en caso contrario no registrará ninguna ruta.
use App\MainController;
use rguezque\Forge\Exceptions\RouteNotFoundException;
use rguezque\Forge\Exceptions\UnsupportedRequestMethodException;
use rguezque\Forge\Router\Emitter;
use rguezque\Forge\Router\Injector;
use rguezque\Forge\Router\Request;
use rguezque\Forge\Router\Router;
use rguezque\Forge\Router\Response;
require __DIR__.'/vendor/autoload.php';
$app = new Router;
// Aqui se cargan los controladores
$app->addControllersWithAttributes([
MainController::class
]);
$container = new Injector;
$container->add(MainController::class);
$engine->setContainer($container);
$app->setEngine($engine);
try {
$response = $app->handleRequest(Request::fromGlobals());
} catch (RouteNotFoundException $e) {
$response = new Response($e->getMessage(), 404);
} catch (UnsupportedRequestMethodException $e) {
$response = new Response($e->getMessage(), 405);
}
Emitter::emit($response);
Estos atributos deben tener una definición específica. Para el caso de las rutas, el controlador debería verse así:
namespace App;
use rguezque\Forge\Router\Request;
use rguezque\Forge\Router\Response;
// Se debe incluir la clase RouteAttribute pues de aqui se recuperan
// los parámetros para crear la ruta
use rguezque\Forge\Router\Attributes\RouteAttribute;
class MainController {
#[RouteAttribute('GET', '/')]
public function indexAction(Request $request, Response $response) {
return $response->withContent('Hola mundo!');
}
#[RouteAttribute('GET', '/about')]
public function indexAction(Request $request, Response $response) {
return $response->withContent('Hola mundo!');
}
}
Por cada método se define uno o más atributos RouteAttribute
que debe contener los valores: método de petición de la ruta, y el string path de la ruta. Para el caso de los grupos de rutas, se debe definir un atributo GroupAttribute
para la clase, el cual recibe como valor el prefijo del grupo de rutas:
namespace App;
use rguezque\Forge\Router\Request;
use rguezque\Forge\Router\Response;
// Se debe incluir la clase GroupAttribute y RouteAttribute pues de
// aqui se recuperan los parámetros para crear el grupo de rutas
use rguezque\Forge\Router\Attributes\GroupAttribute;
use rguezque\Forge\Router\Attributes\RouteAttribute;
#[GroupAttribute('/foo')]
class MainController {
#[RouteAttribute('GET', '/')]
public function indexAction(Request $request, Response $response) {
return $response->withContent('Hola mundo!');
}
#[RouteAttribute('GET', '/about')]
public function indexAction(Request $request, Response $response) {
return $response->withContent('Hola mundo!');
}
}
De esta forma tenemos un grupo con un prefijo que heredarán cada una de las rutas que se generen a partir de cada método que tenga atributos de ruta definidos, en caso contrario serán ignorados estos métodos; de la misma forma las clases que no tengan definido atributos de grupo solo se crearán las rutas de forma normal por cada método.
El router tiene dos motores de funcionamiento, ApplicationEngine
(usado por default) y JsonEngine
, este último permite usar el router como una API. En el primer caso los métodos de cada controlador reciben dos parámetros, Request
y Response
y cada método debe devolver un Response
de lo contrario lanzará un UnexpectedValueException
. Si una ruta contiene wildcards estos son enviados en el Request
y recuperados con Request::getParameters
, mientras que el parámetroResponse
proporciona los métodos para generar una respuesta.
En el segundo caso, JsonEngine
exige que se retorne un array asociativo en cada método de los controladores. El router se encarga de convertirlo a formato json
y generar el respectivo JsonResponse
.
El motor de funcionamiento se asigna al router con el método Router::setEngine
que recibe un objeto EngineInterface
. No es obligatorio definir un motor de funcionamiento, a menos que se use un Contenedor (Ver Dependencies Container) o un Proveedor de Servicios (Ver Services Provider), o bien, se requiera devolver datos en JSON.
// index.php
//...
$router->addRoute(new Route('GET', 'hola_page', '/hola/{nombre}', FooController::class, 'holaAction'));
$app = new ApplicationEngine();
$router->setEngine($app);
try {
$response = $router->handleRequest(Request::fromGlobals());
} catch(RouteNotFoundException $e) {
$response = Response::create(404)
->withContent(
sprintf('<h1>Not Found</h1>%s', $e->getMessage())
);
}
Emitter::emit($response);
// app/Http/FooController.php
//...
public function holaAction(Request $request, Response $response): Response {
$nombre = $request->getParameter('nombre');
$response->clear()->withContent(sprintf('Hola %s', $nombre));
return $response;
}
// index.php
//...
$router->addRoute(new Route('GET', '/show/{id}', FooController::class, 'showAction'));
$app = new JsonEngine();
$router->setEngine($app);
try {
$response = $router->handleRequest(Request::fromGlobals());
} catch(RouteNotFoundException $e) {
$response = Response::create(404)
->withContent(
sprintf('<h1>Not Found</h1>%s', $e->getMessage())
);
}
Emitter::emit($response);
// app/Http/FooController.php
//...
public function showAction(Request $request): array {
$id = $request->getParameter('id');
//...
$data = [
'id' => $id,
'title' => 'Lorem ipsum',
'author' => 'John Doe'
];
return $data;
}
La clase Injector
permite crear un contenedor de dependencias. A cada motor de funcionamiento del router (ApplicationEngine
o JsonEngine
) se le puede asignar un contenedor con el método EngineInterface::setContainer
desde el cual se buscarán los controladores (class controller) y demás dependencias que serán inyectados al constructor de cada clase. Si no se asigna un contenedor el router generara una instancia de cada controlador con ReflectionClass::newInstance
asumiendo que no deben inyectarse dependencias.
//...
$container = new Injector;
$container->add(FooController::class);
$app = new ApplicationEngine();
$app->setContainer($container);
$router->setEngine($app);
try {
$response = $router->handleRequest(Request::fromGlobals());
} catch(RouteNotFoundException $e) {
$response = Response::create(404)
->withContent(
sprintf('<h1>Not Found</h1>%s', $e->getMessage())
);
}
Emitter::emit($response);
Esta clase permite crear contenedores e inyectar dependencias. Cuenta con cinco métodos:
-
Injector::add
: Recibe el nombre de la dependencia y el nombre de la clase a instanciar así como los parámetros a inyectar. Si solo se envía la clase a instanciar, se toma el nombre de la clase como el nombre de dicha dependencia. También se permite agregar unClosure
o un método estático como dependencia, en cuyo caso es obligatorio asignar un nombre. -
Injector::get
: Recupera una dependencia por su nombre. Opcionalmente puede recibir como segundo argumento un array con argumentos utilizados por la dependencia solicitada, esto es útil cuando la dependencia es una función o método estático cuyo resultado dependerá de parámetros enviados al momento de llamarla.En el caso de que la dependencia sea una clase, se devolverá una instancia de esta, y si se mandan argumentos adicionales se inyectarán también al constructor, de igual forma es útil cuando la clase recibirá algunos argumentos que podrían ser opcionales o cuyo valor dependerá de la programación al momento de solicitarla.
-
Injector::has
: Verifica si existe una dependencia por su nombre.
Si se agrega como dependencia una clase Injector::add
devolverá una instancia de Dependency
que permite encadenamiento para ejecutar alguno de los siguientes métodos para definir parámetros de dicha clase que luego serán inyectados como argumentos al recuperarla con Injector::get
:
Dependency::addParameter
: Permite agregar un parámetro a una clase agregada al contenedor.Dependency::addParameters
: Permite agregar varios parámetros a una clase agregada al contenedor a través de un array.
Los métodos de Injector
también se pueden llamar como métodos estáticos a través del facade Container
.
Container::add(FooController::class);
Container::add(PDO::class)->addParameters(/*...*/);
$engine = new ApplicationEngine;
$engine->setContainer(Container::app());
La clase Services
permite crear un proveedor de servicios. A cada motor de funcionamiento del router (ApplicationEngine
o JsonEngine
) se le puede asignar un proveedor con el método EngineInterface::setServices
desde el cual se podrá tener acceso en toda la aplicación. La diferencia con un contenedor es que el proveedor de servicios inyecta los servicios registrados a cada método de un controller class y todos los servicios son accesibles en toda la aplicación, mientras que el contenedor solo inyecta las dependencias agregadas al constructor de cada clase especificada y solo están disponibles estas dependencias en dicha clase que se inyectan.
Esta clase permite registrar servicios y solo dispone de dos métodos, Services::register
que recibe dos parámetros, un alias para el servicio y un closure con el servicio a devolver, y el método Services::has
que devuelve true
si un servicio especificado existe. Para acceso a un servicio simplemente se invoca como un método del objeto Services
(método mágicoServices::__call
) o bien como una propiedad en contexto de objeto (método mágico Services::__get
). Ejemplo:
$services = new Services;
$services->register('pi_const', function() {
return 3.141592654;
});
$engine = new ApplicationEngine();
$engine->setServices($services)
$router = new Router;
$router->addRoute(new Route('GET', '/', FooController::class, 'indexAction'));
$router->setEngine($engine);
$router->handleRequest(Request::fromGlobals());
// FooController::indexAction
public function indexAction(Request $request, Response $response, Services $service): Response {
// Se verifica que exista y se recupera el servicio como un método
$pi = $services->has('pi_const') ? $services->pi_const() : 3.14;
// O se recupera el servicio como propiedad en contexto de objeto
//$pi = $services->pi_const;
return $response->withContent($pi);
}
Solo se puede implementar uno a la vez, o se elige usar un contenedor de dependencias (EngineInterface::setContainer
) o bien el proveedor de servicios (EngineInterface::setServices
). Si se intenta utilizar ambos el que sea asignado en última instancia sobrescribirá al primero.
La clase Request
representa una petición HTTP del lado del servidor. Los métodos disponibles son los siguientes:
fromGlobals()
: Método estático que crea unRequest
a partir de los globales$_GET
,$_POST
,$_SERVER
,$_COOKIE
,$_FILES
, y un array vacío para los parámetros nombrados de las rutas. Los getters devuelven un objetoBag
(Ver The Bag Class).getQueryParams()
: Devuelve los parámetros de$_GET
.getBodyParams()
: Devuelve los parámetros de$_POST
.getPhpInputStream()
: Devuelve el contenido del stream de solo lecturaphp://input
a través de un objetoPhpInputStream
. Dispone de tres métodos:PhpInputStream::getParsedStr
que devuelve los parámetros de la petición en un array asociativo asbtraido en un objetoBag
; yPhpInputStream::getDecodedJson
que sirve para recuperar parámetros enviados en formato json igualmete encapsulados en un objetoBag
. Y por último el métodoPhpInputStream::getRawData
que devuelve los datos tal cual hayan sido recibidos.getServerParams()
: Devuelve los parámetros de$_SERVER
.getCookieParams()
: Devuelve los parámetros de$_COOKIE
.getUploadedFiles()
: Devuelve los parámetros de$_FILES
.getParameters()
: Devuelve los parámetros nombrados de una ruta.getParameter(string $parameter, $default = null)
: Devuelve un parámetro nombrado de una ruta, y si no existe devolverá el valor default especificado.getAllHeaders()
: Devuelve todos los encabezados HTTP de la petición actual.withQueryParams(array $query)
: Agrega aRequest
parámetros$_GET
especificados.withBodyParams(array $body)
: Agrega aRequest
parámetros$_POST
especificados.withServerParams(array $server)
: Agrega aRequest
parámetros$_SERVER
especificados.withCookieParams(array $cookies)
: Agrega aRequest
parámetros$_COOKIE
especificados.withUploadedFiles(array $files)
: Agrega aRequest
parámetros$_FILES
especificados.withParameters(array $parameters)
: Agrega aRequest
parámetros nombrados de una ruta.withParameter(string $name, $value)
: Agrega aRequest
un parámetro nombrado.withoutParameter(string $name)
: Elimina deRequest
un parámetro de ruta especifico.buildQueryString(string $url, array $params)
: Genera una cadena de petición GET (similar abuild_query_string
, ver functions).
La clase ClientRequest
representa peticiones HTTP desde el lado del cliente. desde el constructor se define la URL y un array opcional con datos de la petición como lo son el método de petición, los encabezados HTTP y posibles datos de petición.
use rguezque\Forge\Router\ClientRequest;
// Si se omite el segundo parámetro se asume que será una petición GET
$request = new ClientRequest('https://jsonplaceholder.typicode.com/posts', [
'method' => 'POST',
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Bearer 928348ur3489jr837rry'
],
'body' => [
'id' => 423
]
]);
// Se envía la petición y se recupera la respuesta
$response = $request->send();
Métodos disponibles:
withRequestMethod(string $method)
: Especifica el tipo de petición que se hará (GET
,POST
,PUT
,DELETE
).withHeader(string $key, string $value)
: Agrega un encabezado a la petición.withHeaders(array $headers)
: Agrega múltiples encabezados a la petición, recibe un array asociativo como parámetro, donde cada clave es un encabezado seguido de su contenido.withPostFields($data, bool $encode = true)
: Agrega parámetros a la petición mediante un array asociativo de datos que es convertido a formato JSON.withBasicAuth(string $username, string $password)
: Agrega un encabezadoAuthorization
basado en un nombre de usuario y contraseña simples.withTokenAuth(string $token)
: Agrega un encabezadoAuthorization
basado en JWT.send()
: Envía la petición y devuelve el resultado en un array con los índicesstatus
yresponse
.
Representa una respuesta HTTP del servidor.
create(int $code = 200, string $phrase = '')
: Método estático que crea un simple response con un código y texto de estatus (opcional).getStatus()
: Devuelve el código de estatus HTTP actual.getContent()
: Devuelve el cuerpo del response.getstatusText()
: Devuelve el texto de estatus HTTP actual.getProtocolVersion()
: Devuelve la versión de protocolo HTTP actual del servidor.withContent(string $content)
: Especifica el contenido a mostrar.withHeader(string $key, string $value)
: Especifica un encabezado HTTP y su valor.withHeaders(array $headers)
: Especifica varios encabezado HTTP a la vez y sus valores.withStatus(int $code)
: Especifica un código numérico de estatus HTTP. Ver HTTP Status Codes.withStatusPhrase(string $phrase)
: Especifica un texto a asignar al actual código de estatus HTTP.withProtocolVersion(string $version)
: Especifica que versión de protocolo HTTP usar. Por lo regular es '1.1'.clear()
: Limpia el response actual, reiniciando a los valores default.
Extiende a la clase Response
, por default recibe un array asociativo y devuelve una respuesta en formato de datos JSON. Si los datos que recibe JsonResponse
ya están en formato JSON previamente, se debe especificar un segundo parámetro false
, para evitar el intento de convertir los datos.
$data = [
'id' => $id,
'name' => 'John Doe',
'age' => 30
];
return new JsonResponse($data);
Extiende a la clase Response
y devuelve una respuesta de redirección a la URI especificada. Si se usa UriGenerator
se puede crear la URI de las rutas incluyendo las que tienen wildcards y enviarla como argumento (Ver Uri Generator).
return new RedirectResponse('/hola/John/Doe');
Esta clase solo contiene el método estático Emitter::emit
, y recibe como parámetro un objeto Response
. Se encarga de "emitir" el response.
use rguezque\Forge\Router\{Emitter, Router, Reques};
//...
$response = $router->handleRequest(Request::fromGlobals());
Emitter::emit($response);
Ambas clases, Bag
y Arguments
, sirven para manipular una colección de datos (array asociativo). Sin embargo tienen diferencias, la clase Bag
solo contiene métodos de lectura, es decir, no permite modificar los datos, mientras que Arguments
extiende a la clase Bag
y es de lecto escritura, es decir, permite leer y modificar los datos.
Esta clase contiene métodos de solo lectura, es decir, solo puede evaluar y consultar los parámetros que almacena.
get(string $key, mixed $default = null)
: Recupera un parámetro por nombre, si no existe devuelve el valor default especificado. También se puede recuperar un parámetro en contexto de objeto gracias al método mágicoBag::__get
(De esta forma devolveránull
si no existe).all()
: Devuelve el array de parámetros.has(string $key)
: Devuelvetrue
si un parámetro existe.valid(string $key)
: Devuelvetrue
si un parámetro no esta vació y no tiene valornull
.count()
: Devuelve la cantidad de parámetros almacenados.keys()
: Devuelve un array lineal con todos los nombres de los parámetros, es decir, las claves del array asociativo de parámetros.gettype(string $key)
: Devuelve el tipo de dato de un parámetro.
Esta clase extiende a la clase padre Bag
heredando sus métodos y además contiene métodos de escritura, es decir, puede crear, modificar y eliminar los parámetros que almacena.
set(string $key, mixed $value)
: Permite crear o sobrescribir un parámetro de la colección de datos. También se puede crear un parámetro en contexto de objeto gracias al método mágicoArguments::__set
.remove(string $key)
: Elimina un parámetro por su nombre.clear()
: Elimina todos los parámetros.
Almacena y proporciona acceso a las variables de $GLOBALS
mediante métodos estáticos. Tiene los mismos métodos de Bag
y Arguments
, excepto Bag::keys
y Bag::gettype
La clase Session
permite manipular las variabes de $_SESSION
a través de una instancia singleton. Los métodos disponibles son:
create()
: Inicializa o selecciona el namespace donde se almacenan las variables de sesión. Devuelve una instancia singleton deSession
.start()
: Inicia o retoma una sesión activa. Siempre debe invocarse antes de los demás métodos (Permite encadenamiento$session->start()->get('foo')
).started()
: Devuelvetrue
si una sesión está activa,false
en caso contrario.set(string $key, mixed $value)
: Crea o sobrescribe una variable de sesión.get(string $key, mixed $default = null)
: Devuelve una variable por nombre; si no existe devuelve el valor default especificado (null
asignado por default).has(string $key)
: Devuelvetrue
si una variable existe;false
en caso contrario.valid(string $key)
: Devuelvetrue
si una variable existe y además no es nula y no está vacía;false
en caso contrario.count()
: Devuelve la cantidad de variables existentes.remove(string $key)
: Elimina una variable por nombre.clear()
: Elimina todas las variables.destroy()
: Destruye la sesión activa.
$session = Session::create();
$session->set('foo', 'bar'); // Crea una variable
$session->get('foo'); // Recupera una variable
Note
Aunque Session::start
se invoca automáticmente al llamar cualquiera de los demás métodos, se deja en acceso público.
Las vistas son el medio por el cual el router devuelve y renderiza un objeto Response
con contenido HTML en el navegador. La única configuración que se necesita es definir el directorio en donde estarán alojados los archivos templates.
Note
Si previamente se ha definido el directorio de templates en la configuración no es necesario especificarlo en el constructor de la clase View
(Ver Configure router), aunque si se define un directorio aquí, este tendrá prioridad sobre la configuración inicial.
use rguezque\Forge\Router\View;
$view = new View(
__DIR__.'/mis_plantillas', // Directorio donde se alojan los templates
);
La configuración inicial de View
puede ser sobrescrita con el método View::setPath
.
$view->setPath(__DIR__.'/templates');
El método que permite definir un template principal es View::template
, este puede recibir uno o dos parámetros; el primer parámetro es el nombre del archivo template y el segundo es un array asociativo con argumentos que se envían al template.
// app/Http/FooController.php
function __construct(View $view) {
$this->view = $view;
}
public function homeAction(Request $request, Response $response): Response {
$result = $this->view->template('home.php', ['message' => 'Hola mundo!'])->render();
return $response->withContent($result);
}
Una forma alternativa de enviar argumentos a una vista es a través de los métodos View::addArgument
y View::addArguments
. El primero recibe dos parámetros (nombre y valor) y el segundo un array asociativo. Estos parámetros serán automáticamente incluidos al invocar el método View::render
, por lo cual deben ser declarados antes de renderizar (Ver Render).
$view->addArgument('message', 'Hello weeerld!');
$view->addArguments([
'id' => 1,
'name' => 'Banana',
'color' => 'yellow'
]);
Para extender un template se utiliza el método View::extendWith
, este método recibe tres parámetros; el nombre del template que extenderá al template principal, un alias único con el que se incluirá en el template principal y opcionalmente un array de argumentos que se envian al template que está extendiendo al principal.
$data = [
'home': '/',
'about': '/about-us',
'contact': '/contact-us'
];
// Se guarda el template menu.php con el alias 'menu_lateral' y se le envian parámetros en la variable $data
$view->template('index.php', ['title' => 'Ejemplo de vistas']);
$view->extendWith('menu', 'menu_lateral', $data);
$view->render();
//menu.php
// Recibe los parámetros enviados en $data
<nav>
<ul>
<li><a href="<?= $home ?>">Home</a></li>
<li><a href="<?= $about ?>">About</a></li>
<li><a href="<?= $contact ?>">Contact</a></li>
</ul>
</nav>
// index.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?></title>
</head>
<body>
<?php
// Imprime en pantalla el contenido de menu.php guardado previamente con el alias 'menu_lateral'
echo $menu_lateral
?>
</body>
</html>
El método View::render
se invoca siempre al final y devuelve lo contenido en el actual buffer para ser recuperado en una variable y enviado en un Response
.
La clase DbConnection
proporciona el medio para crear una conexión singleton con MySQL a través del driver PDO
o la clase mysqli
. El método estático DbConnection::getConnection
recibe los parámetros de conexión y devuelve un objeto con la conexión creada dependiendo del parámetro driver
donde se define si se utilizara por default MySQL con PDO
o con mysqli
.
use rguezque\Forge\Router\DbConnection;
$db = DbConnection::getConnection([
// 'driver' => 'mysqli',
'driver' => 'mysql', // Se usa PDO
'host' => 'localhost',
'port' => 3306,
'user' => 'root',
'pass' => 'mypassword',
'dbname' => 'mydatabase'
'charset' => 'utf8'
]);
Otra alternativa es usar una database URL como parámetro de conexión, a través del método estático DbConnection::dsnParser
; este recibe una URL y la procesa para ser enviada a DbConnection::getConnection
de la siguiente forma:
use rguezque\Forge\Router\DbConnection;
// Con mysqli
// 'mysqli://root:[email protected]/mydatabase?charset=utf8'
// Con PDO
$connection_params = DbConnection::dsnParser('mysql://root:[email protected]/mydatabase?charset=utf8');
$db = DbConnection::getConnection($connection_params);
El método estático DbConnection::autoConnect
realiza una conexión a MySQL tomando automáticamente los parámetros definidos en un archivo .env
.
use rguezque\Forge\Router\DbConnection;
$db = DbConnection::autoConnect();
El archivo .env
debería verse mas o menos así:
DB_DRIVER="mysqli"
DB_NAME="mydatabase"
DB_HOST="127.0.0.1"
DB_PORT=3306
DB_USER="root"
DB_PASS="mypassword"
DB_CHARSET="utf8"
Note
Se debe usar alguna librería que permita procesar la variables almacenadas en .env
y cargarlas en las variables $_ENV
.
El método Router::setCors
permite definir una configuración de dominios externos (origenes) a los que se les permite hacer peticiones de recursos restringidos, mejor conocido como CORS (Cross-Origin Resource Sharing).
Esta configuración se define a través de un objeto CorsConfig
en el cual se agregan los origenes, los métodos de petición permitidos para cada origen y encabezados http aceptados.
require __DIR__.'/vendor/autoload.php';
use rguezque\Forge\Router\Router;
use rguezque\Forge\Router\CorsConfig;
$router = new Router;
// Agregar origenes desde el constructor
$cors = new CorsConfig([
'http://localhost:3000' => [
'methods' => ['GET', 'PATCH'],
'headers' => ['Authorization']
],
'http://foo.net' => [
'methods' => ['GET', 'POST'. 'DELETE'],
'headers' => ['Authorization', 'Accept']
]
]);
// Utilizando el método CorsConfig::addOrigin para agregar más origenes
$cors->addOrigin(
'(http(s)://)?(www\.)?localhost:3000', // origen
['GET', 'POST'], // métodos de petición aceptados
['X-Requested-With'] // headers aceptados
);
$router->setCors($cors);
Los métodos y encabezados http son opcionales, por default los métodos aceptados son GET
y POST
, y los encabezados permitidos son Content-Type
. Accept
, y Authorization
.
Esta clase se encarga de configurar el manejador de errores tanto en modo production como development, así como la zona horaria para el manejo correcto de fechas en PHP. Recibe un array asociativo con tres parámetros: log_path
, environment
y timezone
; no importa el orden en que se declaren. Debe declararse al inicio, antes que todo en el controlador frontal. Las configuraciones se aplican con solo crear una instancia de Handler
o con el método estático Handler::configure
que de igual forma recibe los parámetros ya mencionados.
use rguezque\Forge\Router\Handler;
new Handler([
'log_path' => __DIR__.'/var/logs',
'timezone' => 'America/Mexico_City',
'environment' => 'development' // Cambiar a 'production' para puesta en marcha (deploy)
]);
La clase Users
recibe 2 parámetros, una conexión PDO y un array opcional que define los nombres de la tabla y campos donde se buscara a los usuarios. Por default se buscara por usuarios en la tabla users
donde el campo de usuario deberá llamarse username
y el campo de la contraseña deberá llamarse password
. Sin embargo puede definir su propio nombre de la tabla de usuarios así como los campos de usuario y contraseña, ya sea al momento de crear la instancia de Users
o con los métodos: setUsersTable
, setIdentityField
, setCredentialField
después de creada la instancia.
Usando el contenedor:
$container = new Injector;
$container->add(PDO::class)->addParameters([
'mysql:dbname=ejemplo;host=localhost;port=3306;charset=utf8',
'usuario',
'contrasena'
]);
// Los parámetros se inyectan en el orden en que son declarados
$container->add(Users::class)->addParameter(PDO::class)->addParameter([
'tablename' => 'usuarios',
'identity_field' => 'nombre_usuario',
'credential_field' => 'contrasena'
]);
Usando servicios también se puede definir desde el constructor pero se muestra de esta forma a modo de ejemplo de como llamar a los métodos setUsersTable
, setIdentityField
y setCredentialField
:
$services = new Services();
$services->register('pdo', function() {
return new PDO('mysql:host=localhost;port=3306;dbname=test;charset=utf8', 'usuario', 'contrasena');
});
$services->register('users', function() use($services) {
$pdo = $services->pdo();
$users = new Users($pdo);
$users->setUsersTable('mis_usuarios');
$users->setIdentityField('email');
$users->setCredentialField('contrasena');
return $users;
});
El router dispone de ciertas funciones que se invocan bajo el namespace Forge\functions
. Ejemplo:
use function rguezque\Forge\functions\str_ends_with;
str_ends_with('FooBar', 'Bar') // Devuelve true
Se incluyen las siguientes:
add_trailing_slash(string $str)
: Añade una barra diagonal al final de una cadena de texto.remove_trailing_slash(string $str)
: Remueve las barras diagonales al final de una cadena de texto.add_leading_slash(string $str)
: Añade una barra diagonal al inicio de una cadena de texto.remove_leading_slash(string $str)
: Elimina las barras diagonales al inicio de una cadena de texto.str_starts_with(string $haystack, string $needle)
: Devuelvetrue
si una cadena de texto tiene un prefijo específico.str_ends_with(string $haystack, string $needle)
: Devuelvetrue
si una cadena de texto tiene un sufijo específico.str_prepend(string $subject, string ...$prepend)
: Concatena una o varias cadenas de texto al inicio de otra cadena de texto principal. La primera declarada es la primera en ser concatenada y así sucesivamente.str_append(string $subject, string ...$append)
: Concatena una o varias cadenas de texto al final de otra cadena de texto principal.str_path(string $path)
: Utilizada por el router, aplica un formato válido de ruta para ser procesado en el routing. Elimina slashes al final y agrega uno al inicio.is_assoc_array(mixed $value)
: Devuelvetrue
si un argumento es un array asociativo y no lineal.json_file_get_contents(string $file)
: Lee el contenido de un archivo.json
y lo devuelve como un array asociativo en php.unsetcookie(string $name)
: Elimina una cookie.equals(string $strone, string $strtwo)
: Devuelvetrue
si dos cadenas de texto son equivalente o iguales.str_to_pascalcase(string $str)
: Convierte una cadena de texto a formato PascalCase.url_exists(string $url)
: Devuelvetrue
si una URL existe.str_random($length = 20, $special_chars = true, bool $more_entropy = false)
: Genera una cadena de texto aleatoriamente, con una longitud definida (Por default es de 20).dd($var)
: Vuelca información de una variable en texto preformateado para una mejor lectura de su cóntenido y termina el script actual.build_query_string(string $url, array $params)
: Genera una cadena de petición GET.generate_uuidv4
: Genera un identificador UUID v4.generate_ulid
: Genera un identificador ULID.
Footnotes
-
Al final del proyecto utiliza el autoloader optimizado
composer dump-autoload -o
↩ -
Un wildcard es uno o varios parámetros que se definen en la ruta, pueden tener un nombre asignado entre llaves
{}
o bien, ser definidos como expresiones regulares; en ambos casos estos harán match con la petición que se haga a través del navegador web. ↩ -
Funciones anónimas, es decir, no tienen un nombre especificado y permiten acceder al ámbito de una función externa. ↩