dulivu / irbis
Micro-Framework MVC
README
PHP MVC Micro-Framework.
Enfocado en desarrollo modular.
Contenido
- Introducción
- Parametrización
- Modularidad
- Instalación
- Configuración
- Seguridad
- Gestión de aplicaciones
- Arquitectura
- Controladores
- Solicitud y Respuesta
- Base de datos
- ORM
- Modelo
- Recordset y Record
- Extensibilidad
- Interfaces
- Hooks
- Setup y Session
1. Introducción, características escenciales
Irbis es un framework PHP ligero y modular que combina:
- Un core MVC minimalista (Request > Controller > Action > Response)
- Un sistema de módulos desacoplado, permitiendo añadir logica por capas
- Un ORM declarativo propio con soporte nativo para SQLite, MySQL y PostgreSQL
El objetivo es ofrecer una base sólida, extensible y entendible para construir aplicaciones web y APIs.
index.php punto de entrada del sistema:
require 'vendor/autoload.php'; Irbis\Server::listen();
Esto es todo lo que requieres en tu punto de entrada, el framework está hecho para trabajar con urls amigables y composer, asegurate de configurar bien tu servidor web apache o ngix correctamente para esto.
.htaccess, de ejemplo para apache
<Files ~ "\.(ini|db3)$"> Order allow,deny Deny from all </Files> Options +FollowSymLinks RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php?$1 [QSA,L]
Tip
Esta configuración deniega el acceso a archivos .ini y .db3 por seguridad.
Tip
También hace que toda solicitud de cliente este direccionada al archivo index.php.
1.1. Parametrización del sistema
El framework genera de forma automática un archivo state.ini para persistir su parametrización, este archivo se irá ajustando conforme vayas configurando tu sistema.
state.ini
[server] debug = "on" terminal = "" [database] dsn = "sqlite:database.db3" user = "" pass = ""
Para el ejemplo:
- El modo
debugestá activado, variable que recorre el sistema. - La conexión de base de datos apuntan a
SQlitepor defecto.
Note
A nivel de código se puede manipular por medio de métodos setState() y getState() del objeto Server.
$server = Irbis\Server::getInstance(); // apuntando a una base de datos mysql $server->setState('database.dsn', 'mysql:host=localhost;dbname=mydb;charset=utf8mb4'); $server->setState('database.user', 'db_username'); $server->setState('database.pass', 'db_password');
1.2. Modularidad
El framework permite encapsular lógica de negocio en paquetes llamados módulos o aplicaciones, estos son directorios que contienen una estructura que el objeto Server leerá y gestionará.
Cada aplicación añadida, agregará lógica y/o funcionalidad al sistema.
Toda aplicación se estructura en un directorio principal, que es el paquete, y sus sub-directorios que son las aplicaciones. Pudiendo tener así un Paquete llamado MyApps y dentro varias aplicaciones como Test, Rest, etc.
MyApps/Test y MyApps/Rest ejemplo:
MyApps
> Test
> assets
- script.js
- style.css
> components
> models
- users.php
- groups.php
> views
- index.html
- info.html
- Controller.php
- Hooks.php
- Setup.php
> Rest
- Controller.php
- index.php
Important
Toda aplicación debe tener una clase Controller.php que es el controlador de la aplicación.
Tip
Adicionalmente puede implementar las clases Hooks.php y Setup.php para implementar ganchos de instalación y lógica de configuración como middlewares y otras funcionalidades.
Tip
Dentro del sistema una aplicación se identifica por su espacio de nombre namespace que sería la combinación de su paquete y su nombre, e: MyApps/Test
2. Instalación, el código fuente
Important
- Se recomienda utilizar
composerpara instalar los fuentes, información aqui! - Se recomienda instalar
Git, ya que algunas operaciones pueden requerirlo.
composer require dulivu/irbis
El comando de arriba instalará los fuentes del framework, luego sólo es necesario preparar el archivo index.php con el siguiente contenido y listo, estarás listo para iniciar.
index.php
require 'vendor/autoload.php'; Irbis\Server::listen();
Una vez instalado puedes ingresas a la url, direccion IP, o localhost, dependiendo del ambiente donde lo hayas instalado, podrás observar una pantalla en el navegador parecido a un terminal de comandos. Desde este terminal puedes continuar la configuración del sistema.
3. Configuración, usando el terminal
El terminal es una interfaz de texto que permite interactuar directamente con la aplicación mediante la escritura de comandos en lugar de elementos gráficos.
Warning
Esta terminal sólo es una simulación, no tiene acceso real a comandos del equipo donde este instalado el sistema.
3.1. Seguridad
Lo primero es asegurar el acceso al terminal.
- El comando
usermodcambia el nombre de usuario del terminal - El comando
passwdcambia la contraseña de acceso.
Note
cambia <username> y <password> por el nombre de usuario y contraseña que desees usar.
$ usermod <username> $ passwd <password>
Warning
Si sólo cambias la contraseña, el usuario por defecto es irbis
- El comando
clearrecarga un nuevo terminal limpio.
$ clear
Note
Luego de este ultimo paso, el navegador te solicitará ingreses el usuario y contraseña para poder continuar de ahora en adelante.
3.2. Gestión de aplicaciones
Como vimos anteriormente, una aplicación tiene un espacio de nombre, compuesto por su directorio paquete y sub-directorio aplicación, resultando en un nombre similar a MayApps/Test ó MyApps/Rest esta nomenclatura identificará a cada aplicación dentro del sistema de forma única.
Tip
Te comparto un repositorio con aplicaciones del mismo framework.
Es un buen ejemplo para revisar como se estructura un paquete de aplicaciones y varias son muy útiles si deseas probarlas.
https://github.com/dulivu/IrbisApps
- El comando
git clonereplica un repositorio en tu sistema.
$ git clone https://github.com/dulivu/IrbisApps
- El comando
newappte permite crear una aplicación vacia.
$ newapp Test
Note
Se crea dentro del paquete MyApps por lo que su namespace sería MyApps/Test.
No debe contener espacios ni carácteres especiales.
- El comando
installinstala una aplicación disponible.
$ install MyApps/Test
- El comando
ls appsmuestra las aplicaciones disponibles.
$ ls apps
[ ] IrbisApps/Base
[ ] IrbisApps/Cms
[*] MyApps/Test
Note
Las aplicaciones ya instaladas se muestran con un asterisco.
Si la aplicación instalada tuviera alguna dependencia de otra, las dependencias se intentarán instalarán automáticamente.
- El comando
infomuestra información detallada de alguna aplicación.
$ info MyApps/Test
- El comando
removeintentará quitar una aplicación del sistema, pero ten cuidado que esto no siempre puede ser beneficioso. Si deseas probar aplicaciones se recomienda hacerlo en un ambiente de prueba.
$ remove MyApps/Test
Tip
Si terminas instalando la aplicación creada y navegas a la raíz del sistema (ip, url o localhost) podrás observar que ya tienes respuesta :).
- El comando
nanoabré una ventana para editar un archivo. Si el archivo no exite lo crea.
$ nano Test/Controller.php
Note
Sólo puedes editar archivos que estén dentro del paquete MyApps.
- Algunos archivos se crean con una plantilla base.
$ nano Test/views/index.html
- Finalmente el comando
showabre una ventana con la vista principal del sistema.
$ show $ show /ruta/amigable
4. Arquitectura, lo básico e importante
A nivel técnico y de forma resumida dentro del patrón MVC implementado, existen 5 objetos que gestionan todo el flujo de una solicitud de cliente.
- Server: Orquestador principal del framework
- Request: Envoltura a los datos de la solicitud (GET, POST, HEADERS, COOKIES, etc.)
- Controller: El manejador principal de cada aplicación.
- Action: Envoltura de métodos auto-ejecutables por la solicitud del cliente.
- Response: Gestiona los datos y la forma de entregar una respuesta al cliente.
4.1. Controladores
Todo controlador debe heredar de la clase Irbis\Controller y debe declarar una propiedad $name que debe ser única en la lista de aplicaciones instaladas.
MyApps/Test/Controller.php
namespace MyApps\Test; use Irbis\Controller as iController; class Controller extends iController { public static $name = 'test'; /** * @route / */ final public function index () { return '@test/index.html'; } }
Cada método del controlador declarado con la palabra clave final representa una solicitud (Action) que el cliente puede invocar, además tambien debe tener una sección de comentarios correctamente estructurada (DocBlock) y dentro un decorador @route que indica a que ruta responde dicha acción.
Important
Server revisará la solicitud Request y pedirá a cada Controller que entregue sus Action que coínciden con la solicitud, estos Action serán entregados a Response para que pueda determinar la respuesta.
Métodos de utilidad
// siendo $ctrl el objeto Controller $ctrl->namespace(); // 'MyApps/Test' $ctrl->namespace('php'); // '\MyApps\Test\' $ctrl->namespace('snake'); // 'myapps_test_' $ctrl->namespace('dir'); // '/var/www/MyApps/Test'
// ejecuta la última acción de la pila $ctrl->super(); // obtiene la instancia de un controlador del sistema $ctrl->application('MyApps/Test') // fabrica y devuelve un objeto componente $ctrl->component('Model');
4.2. Solicitud y Respuesta
El objeto Request es un singleton, por lo que sólo puede existir una sola instancia en cada ejecución, si necesitas utilizarlo puedes llamarlo por su método getInstance().
$request = Irbis\Request::getInstance();
Métodos de utilidad
$request->isGet(); $request->isPost(); $request->isPut(); $request->isPatch(); $request->isDelete(); $request->isJson(); // si la solicitud lleva cabecera 'application/json' $request->cookie($key) // devuelve un valor en las cookies
$request->cookie($key) // devuelve un valor de las cookies enviadas $request->header($key) // devuelve un valor de las cabeceras de solicitud
- Leyendo datos de cliente
$request->query($key) // devuelve un valor de la url (GET) $request->input($key) // devuelve un valor del body (POST) // otro ejemplos // URL: /search?q=irbis&page=2&filter= $request->query('q'); // 'irbis' $request->query('*'); // ['q' => 'irbis', 'page' => '2', 'filter' => ''] $request->query('*!'); // ['q' => 'irbis', 'page' => '2'] $request->query(['q', 'page']); // ['q' => 'irbis', 'page' => '2']
// normalmente datos enviados por formularios POST $request->input('username'); // el valor del input[@name='username'] $request->input('*'); // todos los valores enviados por POST
// @route /users/(:num) // URL: /users/5 $request->path(0); // 5 // @route /blog/(:any)/section/(:num) // URL: /blog/entrada-1/section/7 $request->path(0); // 'entrada-1' $request->path(1); // 7
- Leyendo archivos cargados por el cliente
$request->hasUploads(); // true si hay archivos, ó false $request->hasUploads($key); // validar si se subieron archivon con clave '$key' // guardar un archivo subido por el cliente // $key, es la clave con la que se sube el archivo $request->upload($key, '/path/to/file');
- Un ejemplo de un método en un controlador para subir multiples archivos. Debes crear un directorio
uploadsdentro de tu aplicación.
/** * @route /upload */ final function uploadFiles ($request, $response) { $key = 'file'; // clave del archivo subido if ($request->isPost() && $request->hasUploads()) { $request->walkUploads($key, fn($upload) => { $basepath = $this->namespace('dir') . 'uploads/'; $filepath = $basepath.$upload['name']; move_uploaded_file($upload['tmp_name'], $filepath); }); } return '@test/upload.html'; }
Note
En el último ejemplo se observa que a cada Action se le pasa tambien dos parámetros Request y Response que puedes utilizar dentro de la lógica de la acción para tomar decisiones y determinar la información entregada al cliente.
El objeto Response es pasado al método del controlador Action como un segundo parámetro (opcionalmente utilizable).
Métodos de utilidad
$response->view($view); // establece una vista para mostrar $response->hasView(); // true si tiene una vista establecida
$response->header($key, $value); // establece una cabecera de respuesta $response->body($data); // establece los datos para pasar a la vista $response->append($key, $value); // agrega un valor al cuerpo de respuesta $response->remove($key); // quita un valor al cuerpo de respuesta
- Para simplificar la gestión de la respuesta existe algunas interpretaciones que
Responsereconoce en función de lo que la acción del controlador devuelva.
Si se devuelve un texto plano u otro tipo de dato,
Responselo interpreta comobody
/** * @route / */ final public function index () { return 'Mi nueva aplicación'; }
Si se devuelve una ruta a una vista html,
Responselo interpreta comoviewAdemás el acortador
@testes reemplazado por la rutaMyApps/Test/viewsde la aplicación, para buscar la plantilla exactamente en ese directorio.
/** * @route / */ final public function index () { return '@test/index.html'; }
Si se devuelve una combinación [
view,data],Responseestablece precisamente esa información usando internamenteview()ybody()y dentro de la plantillaindex.htmlesos datos estarán disponibles.
/** * @route / */ final public function index () { return ['@test/index.html', ['msg' => 'Hola mundo!']]; }
Lo anterior es lo mismo que aplicar dichos métodos de forma explicita, y usando el objeto
Responseademás podemos controlar otros aspectos como las cabeceras de respuesta.
/** * @route / */ final public function index ($request, $response) { $response->header('Content-Type: text/html'); $response->view('@testweb/index.html'); $response->data(['msg' => 'Hola mundo!']); }
Note
Hasta este punto ya puedes levantar un sitio web con información dinámica, controlar el flujo desde la solicitud hasta la entrega de información al cliente, tener ordenadas tus rutas con URLs amigables y las plantillas html que necesites.
Tip
Reto: Crea un archivo css y agregalo en index.html para aplicar estilos a tu sitio web. Una Pista, para agregar un activo utiliza la ruta completa del módulo e: <link href="/MyApps/Test/assets/style.css"/>.
Comandos:
$ nano Test/assets/style.css $ nano Test/views/index.html
4.3. Base de datos
Ahora vamos adentrandonos en temás cada vez más complejos, el framework maneja de forma automatica una conexión a base de datos, vimos en la primera parte que existe un archivo de parametrización state.ini que por defecto apunta a SQLite ahí puedes modificar si deseas utilizar MySQL o PostgreSQL.
- El commando
connpermite configurar los parámetros de conexión a base de datos.
$ conn reset $ conn dsn <dsn> $ conn user <db_user> $ conn pass <db_pass>
resetle indica al sistema que debe reinicializar sus parámetros de conexión, usando por defectoSQLite.
Así que prácticamente con el mínimo (o nada) de configuración puedes usar el objeto Irbis\Orm\Connector para empezar a ejecutar sentencias SQL.
- El comando
sqlabre una ventana para ejecutar sentencias SQL. Sólo funciona con conexiones a SQLite.
$ sql
Note
Para el siguiente ejemplo puedes crear una tabla todo_list en tu base de datos.
CREATE TABLE IF NOT EXISTS "todo_list" ( "name" VARCHAR NOT NULL, "is_done" BOOLEAN );
Inserta un par de datos a la tabla.
INSERT INTO "todo_list" ("name") VALUES ('aprender a usar Irbis'), ('crear mi propio sistema');
Revisa los datos insertados.
SELECT * FROM "todo_list";
Ahora que los datos están listos, vamos a implementar en nuestra aplicación MyApps/Test una vista que muestre esa información.
Primero crearemos la vista
html
$ nano Test/views/index.html
Agrega este codigo dentro del
body
<h1>Lista de cosas por hacer</h1> <ul> <?php foreach ($list as $item): ?> <li><?= $item['name']; ?></li> <?php endforeach; ?> </ul>
Ahora modificaremos el controlador.
$ nano Test/Controller.php
<?php namespace MyApps\Test; use Irbis\Orm\Connector; use Irbis\Controller as iController; class Controller extends iController { public static $name = 'test'; /** * @route / */ final public function todoList () { $db = Connector::getInstance(); $stmt = $db->query("SELECT * FROM todo_list;"); $list = $stmt->fetchAll(); return ["@test/index.html", [ 'list' => $list ]]; } }
Veamos como queda nuestro sitio
$ show
Note
El objeto Connector hereda del objeto nativo de php PDO por lo que puedes usar todos los métodos que este tenga, además implementa Singleton para tener siempre una sóla conexión activa.
5. ORM, mapeo relacional de objetos
Dentro de cada aplicación puede exitir un directorio llamado models, es donde vamos a mapear nuestros modelos de base de datos, una funcionalidad importante y compleja es manejar los datos de nuestra base de datos.
- Crear tablas
- Capturar datos
- Insertar
- Sanitizar
- Actualizar
Son sólo unas cuantas de las muchas operaciones que se pueden hacer con los datos de un sistema y llevar eso a código suele ser caótico, para ello, el framework implementa un motor ORM que ayuda en esta gestión.
5.1. Modelos
Un modelo se declara como una estructura (arreglo asociativo) en un archivo PHP.
Primero creamos nuestro modelo
users.php
$ nano Test/models/users.php
<?php return [ "name" => ["varchar", "required" => true], "pass" => ["varchar", "required" => true, "store" => '$encryptPassword' ], '$encryptPassword' => function ($password) { return str_encrypt($password); }, 'validatePassword' => function ($password) { return str_decrypt($this->pass) === $password; }, '@mapNames' => function () { $names = []; foreach ($this as $user) { $names[] = $user->name; } return $names; } ];
Note
Convenciones a considerar, todo modelo debe estar dentro del directorio models, se recomienda usar nombre en plural y minuscula users.php, y el código siempre debe empezar por la palabra reservada return para entregar esta estructura cuando sea llamada.
Propiedades
Vienen a ser los campos de las tablas en las bases de datos, cada uno describe el tipo de dato y características propias de una columna en una tabla.
"name" => ["varchar", "required" => true] "pass" => ["varhcar", "required" => true, "store" => '$encryptPassword']
- El atributo
storeejecuta lógica durante la inserción del registro. - El atributo
retrieveejecuta lógica durante la obtención del registro.
Métodos
Son las acciones que el modelo puede realizar, se describen como funciones y como se observa tienen acceso a la variable
$thisque apunta al registro actual y todos sus atributos, como si de cualquier otro objeto se tratara.
'validatePassword' => function ($password) { $my_pass = str_decrypt($this->pass); return $my_pass === $password; }
- Usar el signo
$en un método complica su acceso, por lo que puede simular métodos 'private'. - Usar el signo
@en un método simula el acceso 'static' siendo accesible desde un conjunto de registrosRecordSety no desde el mismo registroRecord.
'@mapNames' => function () { $names = []; foreach ($this as $user) { $names[] = $user->name; } return $names; }
- Dentro de esta función
$thisya no apunta al registroRecordsino al conjunto de registrosRecordSet.
$users = new RecordSet('users'); $users->select(); // captura todos los usuarios $users->mapNames(); // el conjunto devuelve [...nombres] $user = $users[0]; $user->validatePassword('my_password'); // validación por registro
Important
Como se observa declarar un modelo, es como declarar un objeto, facilita la organizacion del codigo y encapsula la lógica del modelo en el modelo. De esta forma tus aplicaciones mejoran en cuanto a diseño y mantenibilidad.
5.2. RecordSet y Record
Ya declarados los modelos que necesites puedes utilizarlos en la aplicación por medio de las clases RecordSet y Record.
Warning
A partir de este punto la aplicación MyApps/Test se empezará a
tornar más compleja, con ejemplos más elaborados con el objetivo de
llegar a que entiendas la filosofía del framework.
- Vamos a agregar más lógica al controlador.
$ nano Test/Controller.php
<?php namespace MyApps\Test; use Irbis\Orm\RecordSet; use Irbis\Orm\Record; use Irbis\Exceptions\HttpException; use Irbis\Controller as iController; class Controller extends iController { public static $name = 'test'; /** * @route / */ final public function todoList () { $db = Connector::getInstance(); $stmt = $db->query("SELECT * FROM todo_list;"); $list = $stmt->fetchAll(); return ["@test/index.html", ['list' => $list]]; } /** * @verb GET * @route /users */ final public function usersListAction () { $users = new RecordSet('users'); $users->select(); return [ '@test/users.html', ['users' => $users] ]; } /** * @verb GET,POST * @route /users/new */ final public function usersNewAction ($request) { if ($request->isPost()) { $users = new RecordSet('users'); $users->insert([ 'name' => $request->input('username'), 'pass' => $request->input('password') ]); redirect('/users'); } return '@test/users_form.html'; } /** * @verb GET,POST * @route /users/(:num) */ final public function usersUpdateAction ($request) { $user_id = $request->path(0); $users = new RecordSet('users'); $users->select($user_id); if (!count($users)) { throw new HttpException(404, "usuario no encontrado"); } if ($request->isPost()) { $user = $users[0]; $user->name = $request->input('username'); $user->pass = $request->input('password'); } return ['@test/users_form.html', [ 'user' => $users[0] ]]; } }
Creamos una vista para mostrar los usuarios
$ nano Test/views/users.html
<h1>Lista de usuarios</h1> [ <a href="/users/new">Nuevo usuario</a> ] <br/> <table> <tr> <th>ID</th> <th>Nombre</th> <th>Contraseña</th> <th></th> </tr> <?php foreach ($users as $user): ?> <tr> <td><?= $user->id; ?></td> <td><?= $user->name; ?></td> <td><?= $user->pass; ?></td> <td>[ <a href="/users/<?= $user->id; ?>">Modificar</a> ]</td> </tr> <?php endforeach; ?> </table>
Veamos como va
$ show /users
Podremos observar que sale un error, indicando que la tabla
usersno existe y es correcto.
- El comando
bindmodelpermite enlazar un modelo con la base de datos.
$ bindmodel users $ bindmodel users --rebuild
--rebuildeliminará la tabla antes de enlazar el modelo, perderás los datos existentes.
Prueba nuevamente con
show /users.Ahora crearemos la vista formulario para crear un nuevo usuario.
$ nano Test/views/users_form.html
<form method="POST"> <p> <input type="submit" value="Guardar"/> [ <a href="/users">Volver</a> ] </p> <p> <label>Usuario:</label> <input type="text" name="username" value="<?= isset($user) ? $user->name : '' ?>"/> </p> <p> <label>Contraseña:</label> <input type="password" name="password"/> </p> </form>
Volviendo a probar.
$ show /users
ó
$ show /users/new
Note
¡Listo!, acabas de diseñar un pequeño CRUD de usuarios. Lo tienes todo organizado, un Controlador que maneja las peticiones, un modelo que gestiona la información, vistas organizadas y separadas de la lógica.
No tocaste nada de codigo SQL y lo más resaltante, en cualquier momento puedes quitar la aplicación (usando remove en el terminal). Ó incluso instalar otra aplicación que aumente funcionalidad.
Tip
Con mayor expertiz, puedes acoplar un motor de plantillas de tu gusto, dentro de las aplicaciones IrbisApps la aplicación Base implementa twig como motor de plantillas por poner ejemplo.
Propiedades y Tipos
El ejemplo anterior sirve para entender de forma general como se implementa y maneja un modelo, pero existen más detalles que en un sólo ejemplo es difícil de plantear.
- Tipos básicos:
varchar,text,integer, etc. - Relaciones:
n1→ many-to-one1n→ one-to-manynm→ many-to-many
'name' => ['varchar'] // se recomienda que todo modelo lleve un campo 'name' 'age' => ['integer'] // se puede usar 'int' o lo que soporte el motor de bd
- Las relaciones son campos que apuntan a claves de otras tablas
Note
Si notaste en el ejemplo anterior users llevaba un campo id sin haberlo declarado.
Todo modelo implementa de forma implícita un campo id autoincrementable, y por el que se relacionan con otros modelos.
// esto genera un campo relación con otro modelo llamado 'groups' 'group' => ['n1', 'target' => 'groups']
// captura un 'user' con id = 1 $user = Record::find('users', 1); // crea un nuevo 'group' llamado Admins $group = Record::add('groups', ['name' => 'Admins']); // relaciona ambos registros $user->group = $group;
- Relaciones
1n
groups.php
// dentro del modelo 'groups' // target apunta al modelo (users) y al campo origen (group) 'users' => ['1n', 'target' => 'users(group)']
// captura un 'group' con id = 1 $group = Record::find('groups', 1); $group->users; // devuelve un 'RecordSet' de 'users'
- Relaciones
nm, similar a 1n pero en doble sentido.
Asumiendo que un usuario puede pertencer a varios grupos, y que un grupo puede contener varios usuarios.
users.php, sería así
return [ 'name' => ['varchar'], 'groups_ids' => ['nm', 'target' => 'groups(users_ids)'] ]
groups.php, sería así
return [ 'name' => ['varchar'], 'users_ids' => ['nm', 'target' => 'users(groups_ids)'] ]
Note
En este ejemplo se usa _ids como nombre de propiedad, para no confundir y diferenciar, en la declaración de target, dentro de los () debe indicar el campo relación.
$user = Record::find('users', 1); $user->groups_ids; // Devuelve un recordset de 'groups' $group = Record::find('groups', 1); $group->users_ids // Devuelve un recordset de 'users'
Tip
Las relaciones nm cuando se enlazan generan internamente una tabla intermedia (cuyo nombre empieza con nm_) para guardar la relación entre registros.
Constantes, @delegate y @unique
Existe una forma de declarar variables en el modelo, útiles para configuraciones o parametrizaciones.
@unique, sirve para indicar qué columnas de la tabla deben llevar la condición 'única'.- Puede ser una combinación de varias columnas.
users.php
return [ // esto indica que el campo 'email' debe ser // único por cada registro existente '@unique' => ['email'], 'name' => ['varchar'], // también hacemos que el campo 'email' no acepte nulos 'email' => ['varchar', 'required' => true] ]
@delegate, genera una relación fuerte entre dos modelos un padre y un hijo, haciendo que el modelo hijo se comporte como si ambos fueran uno sólo.
Teniendo dos modelos.
persons.php
return [ 'name' => ['varchar'], 'email' => ['varchar'], 'phone' => ['varchar'] ]
users.php
return [ '@delegate' => 'person', // apunta al campo delegado 'name' => ['varchar'], // campo delegado 'person' => ['n1', 'target' => 'persons'] ]
- Planteando que todo usuario siempre llevará los campos email y teléfono.
- Y que puede haber otros modelos que tambien los necesiten, como
Clientespor ejemplo. - Al designar un delegado hace que el modelo hijo
userstenga comportamientos del modelo padrepersons
// puedes manejarlo como si fuera un sólo registro // pero internamente se genera la relación // y los datos se guardan en ambas tablas $user = Record::add('users', [ 'name' => 'Juan Perez', 'email' => 'juan.perez@mail.com', 'phone' => '123456' ]); // se puede acceder a las propiedades // del modelo padre como propias del hijo $user->email; // juan.perez@mail.com
- Otras variables marcadas con
@, pueden ser accesibles desde elRecordSetoRecord. - Pueden ser utilizadas por ejemplo para indicar que vista debe usar un modelo por defecto.
- Puedes implementar ese u otros tipo de lógica que necesites.
return [ '@view' => '@test/user_form.html', 'name' => ['varchar'] ]
$users = new RecordSet('users'); $users->select(); // captura todos los usuarios $users->{'@view'}; // @test/user_form.html $user = $users[0]; $user->{'@view'} // @test/user_form.html
Métodos DML, manipulación de datos.
En los ejemplos anteriores se ven métodos select(), insert() ó find(), vamos a detallar estos a continuación.
- Estos métodos son descriptivos y funcionan tal cual las sentencias
SQL.
$users = new RecordSet('users'); $users->select(1); // captura registros cuyo id = 1 $users->select('Juan') // captura registros cuyo name = 'Juan' $users->select(['group' => '1']) // captura registros cuyo 'group' lleve id=1 $users->select(); // si no se envia 'condición', captura todos los registros
- cada
select()ejecutado va añadiendo registros al recordset.
$users = new RecordSet('users'); $users->insert(['name' => 'Pedro']); // inserta un nuevo registro $users->insert( ['name' => 'Jhon'], ['name' => 'Jane'] ); // Inserta varios registros simultaneamente
- cada
insert()ejecutado va añadiendo registros al recordset.
$users = new RecordSet('users'); $users->select(); // captura todos los registros // modifica el campo 'name' en los registros capturados $users->update(['name' => 'Juan']); // modifica el campo 'name' en los registros capturados foreach ($users as $user) { $user->name = 'Juan'; } // modifica el campo 'name' del primer registro capturado $users[0]->name = 'Juan';
- el método
update()hace una modificación en el conjunto de registros capturados. - Como se observa, el objeto
RecordSetse comporta como un arreglo. - Pudiendo usar
foreachcomo cualquier otro arreglo, cada elemento recorrido es un objetoRecord.
$users = new RecordSet('users'); $users->select(); // captura todos los registros $users[0]->delete(); // elimina el primer registro capturado $users->delete(); // elimina todos los registros capturados
- Si el registro tiene configurado un
@delegate, durante la eliminación y si la relación lleva el parámetroondelete='CASCADEel registro delegado también se eliminará. - Al determinar
CASCADE(al revés) si el registro padre se elimina, el registro hijo tambien se elimina.
users.php
return [ '@delegate' => 'person', 'name' => ['varchar'], 'person' => ['n1', 'target' => 'persons', 'ondelete'=>'CASCADE'] ]
$users = new RecordSet('users'); $users->select(); // captura todos los registros // elimina el primer registro capturado // pero tambien eliminará el registro 'person' relacionado. $users[0]->delete();
Acortadores, find() y add()
- Cuando se requiere capturar o insertar un registro puntal estos métodos acortan pasos.
// capturar un registro cuyo id=3 $users = new RecordSet('users'); $users->select(3); $user = $users[0]; // de forma corta se puede hacer $user = Record::find('users', 3);
// insertar y usar un registro nuevo $users = new RecordSet('users'); $users->insert(['name' => 'Juan']); $user = $users[0]; // de forma corta se puede hacer $user = Record::add('users', ['name' => 'Juan']);
Métodos DML, manipulando datos relacionados
En las relaciones estos métodos se comporta de una forma ligeramente diferente.
- Para relaciones
n1
$users = new RecordSet('users'); // Se inserta primero el registro en 'groups' // luego se insertará el registro en 'users' $users->insert([ 'name' => 'Juan', 'group' => ['name' => 'Admins'] ]); $users[0]->group; // devuelve una instancia de Record $users[0]->group->name; // Admins
- Para relaciones
1nynm
$users = new RecordSet('users'); // se crean dos registros 'groups' // y se relacionan al nuevo registro 'users' $users->insert([ 'name' => 'Juan', 'groups' => [ ['name' => 'Admins'], ['name' => 'Public'] ] ]); $users[0]->groups; // devuelve una instancia de RecordSet $users[0]->groups[1]->name; // Public
- Particularmente, al devolver instancias de
RecordSetlos métodosselect()oinsert()también están disponibles.
// asumiendo que ya existe un usuario con id=1 $user = Record::find('users', 1); // captura un registro de 'groups' con id=3 // y lo añade a la relación entre user(1) y sus groups $user->groups->select(3); // crea un nuevo registro en 'groups' // y lo añade a la relación entre user(1) y sus groups $user->groups->insert(['name' => 'Tech'])
Note
Como se observa el manejo de modelos es bastante intuitivo, una vez dominado, aleja a tu código de escribir sentencias SQL permitiendo código más limpio, ordenado y estructurado.
6. Extensibilidad
Hasta este punto ya eres capas de crear aplicaciones propias y el framework te ayuda a tener un código organizado, mantenible y extensible. Pero aún faltan algunas herramientas para mejorar el control total.
6.1. Interfaces (Component)
No toda la lógica de la aplicación puede estar en el controlador, so lo haces puedes terminar con Controller largos con demasiado código que probablemente no debe ir ahí.
Para eso existe una interfaz Irbis\Interfaces\ComponentInterface que se apoya en un trait Irbis\Traits\Component.
- Un
Componentees una clase/objeto para organizar y separar lógica de negocio. - Si sientes que hay métodos que no deben estar en el controlador, probablemente deben ir en un componente.
Test/Authorization.php
<?php namespace MyApp\Test; use Irbis\Interfaces\ComponentInterface; use Irbis\Traits\Component; class Authorization implements ComponentInterface { use Component; public function auth ($credentials) { // la lógica va aqui } }
- Si el controlador u otra clase requiere usar esta lógica.
Test/Controller.php
<?php <?php namespace MyApps\Test; use Irbis\Orm\RecordSet; use Irbis\Orm\Record; use Irbis\Exceptions\HttpException; use Irbis\Controller as iController; class Controller extends iController { public static $name = 'test'; /** * @route / */ final public function todoList ($request) { // se obtiene la instancia de Authorization $authorization = $this->component('Authorization'); // se utiliza la logica implementada if (!$authorization->auth($request)) { throw new HttpException(401, 'Acceso denegado'); } $db = Connector::getInstance(); $stmt = $db->query("SELECT * FROM todo_list;"); $list = $stmt->fetchAll(); return ["@test/index.html", ['list' => $list]]; } }
- Todo componente debe declarar
use Componente implementarComponentInterface, para poder mantener una relación de dependencia y ser inyectado fácilmente porController.
6.2. Hooks
Clases Hooks es un componente especial que se utiliza para ejecutar lógica de instalación de aplicaciónes.
Test/Hooks.php
<?php namespace MyApp\Test; use Irbis\Interfaces\ComponentInterface; use Irbis\Interfaces\HooksInterface; use Irbis\Traits\Component; class Hooks implements ComponentInterface, HooksInterface { use Component; public function install () { // aqui puede ejecutar lógica de enlazado de modelos // crear registros de configuración, etc. } public function uninstall () {} }
- Cuando se ejecuta el comando
install <app>el sistema busca si en tu aplicación si existe una claseHooks, si la encuentra lanza sus métodos durante el proceso.
6.3. Setup y Session
Clases Setup es un componente especial que se utiliza para realizar configuraciones y/o parametrizaciones del sistema, agregando lógica cada vez que se ejecuta una petición.
Test/Setup.php
<?php namespace MyApp\Test; use Irbis\Interfaces\ComponentInterface; use Irbis\Interfaces\SetupInterface; use Irbis\Traits\Component; class Setup implements ComponentInterface, SetupInterface { use Component; public function setup () { // aqui puedes colocar middlewares // u otras configuraciones que el sistema requiera } }
- Útil para crear middlewares de acceso, autorización a recursos.
- Configurar entornos de renderizado.
- Control de errores, o respuesta a clientes.
Clases Session sirven para ser asignada como atributo a Request y este pueda saber como manejar la sesion de usuario activa.
- Por ejemplo se crea una clase
Sessionque busca en base de datos un usuario cuyoIDsea igual al de la sesion guardada y devuelva la instanciaRecord.
Test/Session.php
<?php namespace MyApp\Test; use Irbis\Interfaces\ComponentInterface; use Irbis\Interfaces\SessionInterface; use Irbis\Traits\Component; use Irbis\Orm\Record; class Session implements ComponentInterface, SessionInterface { use Component; public function getUser () { session_start(); $id = $_SESSION['user']; $user = Record::find('users', $id); return $user; } }
- Se configura un
Setuppara que asigne eseComponenteaRequest. - Todo componente tiene acceso a un atributo
controllerque es su controlador correspondiente.
Test/Setup.php
<?php namespace MyApp\Test; use Irbis\Interfaces\ComponentInterface; use Irbis\Interfaces\SetupInterface; use Irbis\Traits\Component; class Setup implements ComponentInterface, SetupInterface { use Component; public function setup () { $request = Request::getInstance(); $controller = $this->controller; $request->session = $controller->component('Session'); } }
- Cada que una petición llega al servidor,
Setupes ejecutado. - Pasando el componente
SessionaRequest. Requesttiene un atributo especialsession.- Esto se utiliza llamando a otro atributo de
Requestllamadouser.
$request = Request::getInstance(); $request->user; // Record, con el id de session
- Al invocar
user,Requestinvoca al objetoSessionpreestablecido.
