En este articulo te voy a mostrar, paso a paso, cómo está construido el módulo de ejemplo que trae LegoBox v5 — un CRUD completo de Personas con PHP y MySQL — para que puedas usarlo como plantilla en tu propio proyecto.
En el artículo anterior presenté LegoBox PHP, el micro-framework MVC que uso desde 2016 como base de mis sistemas en producción.
La idea es simple: si entiendes cómo funciona este módulo, ya sabes cómo construir cualquier otro. Modelo, servicio, rutas y vistas siguen siempre el mismo patrón.

Estructura del módulo Personas
El módulo de personas utiliza la arquitectura desacoplada del framework:
lb-min-5/
├── index.php # Definición de rutas y handlers
├── core/app/model/PersonData.php # Modelo de datos (Extiende de LbModel)
├── core/app/service/PersonService.php # Lógica de negocio (App\Service\PersonService)
└── public/persons/ # Vistas HTML en Twig 3
├── index.html.twig # Listado principal
├── new.html.twig # Formulario de creación
├── edit.html.twig # Formulario de edición
└── show.html.twig # Detalle de la personaCuatro capas, cuatro responsabilidades: el modelo habla con la base de datos, el servicio contiene las reglas de negocio, las rutas conectan la petición HTTP con el servicio, y las vistas en Twig muestran el resultado. Nada se mezcla.
Paso 1: Definición de la tabla SQL
Todo empieza en schema.sql, donde se define la tabla person:
create table person(
id int not null auto_increment primary key,
name varchar(50),
lastname varchar(50),
email varchar(255),
address varchar(255),
phone varchar(255),
image varchar(255),
created_at datetime
);Esta tabla se importa automáticamente al instalar el framework, así que no hay que escribirla a mano para empezar a probar.
Paso 2: El modelo de datos
El modelo representa la tabla y hereda de Extra (que a su vez hereda de LbModel, el mini-ORM sobre PDO):
<?php
class PersonData extends Extra {
public static $tablename = "person";
public $id, $name, $lastname, $email, $phone, $created_at, $address, $image;
public function __construct(){
parent::__construct();
$this->name = "";
$this->lastname = "";
$this->email = "";
$this->created_at = "NOW()";
}
}Con solo heredar de LbModel, PersonData obtiene gratis los métodos que vas a usar todo el tiempo:
PersonData::all()— obtener todos los registros.PersonData::find($id)— buscar por ID.$person->save()— crear o actualizar un registro.$person->delete()— eliminar un registro.
Este es el punto donde LegoBox te ahorra más código repetido: no hay que escribir el INSERT, el UPDATE ni el SELECT a mano para cada módulo nuevo.
Paso 3: La capa de servicio
El servicio encapsula las reglas de negocio e interactúa con el modelo. Aquí es donde vive la lógica, no en el controlador ni en la vista:
<?php
namespace App\Service;
/**
* Clase PersonService
* Lógica de negocio y operaciones CRUD para el módulo de Personas.
*/
class PersonService {
/**
* Obtiene el listado completo de personas
*/
public function getAllPersons(): array {
return \PersonData::all();
}
/**
* Obtiene una persona por su ID
*/
public function getPersonById(int $id) {
return \PersonData::find($id);
}
/**
* Crea una nueva persona
*/
public function createPerson(array $data): bool {
$person = new \PersonData();
$person->name = $data['name'] ?? '';
$person->lastname = $data['lastname'] ?? '';
$person->email = $data['email'] ?? '';
$person->phone = $data['phone'] ?? '';
$person->address = $data['address'] ?? '';
return $person->save();
}
/**
* Actualiza una persona existente
*/
public function updatePerson(int $id, array $data): bool {
$person = \PersonData::find($id);
if (!$person) return false;
$person->name = $data['name'] ?? $person->name;
$person->lastname = $data['lastname'] ?? $person->lastname;
$person->email = $data['email'] ?? $person->email;
$person->phone = $data['phone'] ?? $person->phone;
$person->address = $data['address'] ?? $person->address;
return $person->save();
}
/**
* Elimina una persona
*/
public function deletePerson(int $id): bool {
$person = \PersonData::find($id);
if (!$person) return false;
return $person->delete();
}
}Fíjate que el servicio nunca toca SQL directamente — todo pasa por PersonData. Si mañana cambias de motor de base de datos o agregas una capa de caché, este archivo casi no se toca.
Paso 4: Registro de rutas y handlers en index.php
Con el modelo y el servicio listos, falta conectar las peticiones HTTP. Esto se hace con FastRoute y un switch de handlers:
// 1. Registro de rutas en FastRoute
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/persons', 'list_persons');
$r->addRoute('GET', '/person/new', 'show_new_person');
$r->addRoute('POST', '/person/create', 'process_create_person');
$r->addRoute('GET', '/person/{id:\d+}', 'show_person');
$r->addRoute('GET', '/person/{id:\d+}/edit', 'show_edit_person');
$r->addRoute('POST', '/person/{id:\d+}/update', 'process_update_person');
$r->addRoute('POST', '/person/{id:\d+}/delete', 'process_delete_person');
});
// 2. Handlers en el switch principal
$personService = new PersonService();
switch ($handler) {
case 'list_persons':
$persons = $personService->getAllPersons();
ViewEngine::render('persons/index.html.twig', ['persons' => $persons]);
break;
case 'show_new_person':
ViewEngine::render('persons/new.html.twig');
break;
case 'process_create_person':
$errors = Request::validate([
'name' => 'required',
'lastname' => 'required'
]);
if (!empty($errors)) {
Session::flash('error', implode(' ', $errors));
header('Location: ' . $baseFolder . '/person/new');
exit;
}
$personService->createPerson(Request::post());
Session::flash('success', 'Persona registrada correctamente.');
header('Location: ' . $baseFolder . '/persons');
break;
case 'show_person':
$person = $personService->getPersonById((int)$vars['id']);
ViewEngine::render('persons/show.html.twig', ['person' => $person]);
break;
case 'show_edit_person':
$person = $personService->getPersonById((int)$vars['id']);
ViewEngine::render('persons/edit.html.twig', ['person' => $person]);
break;
case 'process_update_person':
$personService->updatePerson((int)$vars['id'], Request::post());
Session::flash('success', 'Persona actualizada correctamente.');
header('Location: ' . $baseFolder . '/persons');
break;
case 'process_delete_person':
$personService->deletePerson((int)$vars['id']);
Session::flash('info', 'Persona eliminada correctamente.');
header('Location: ' . $baseFolder . '/persons');
break;
}Nota el uso de Request::validate() en la creación: es la validación integrada de LegoBox, así que no hay que escribir el chequeo de campos requeridos a mano. Y los Session::flash() son los mensajes de un solo uso que verás aparecer después de cada redirección.
Paso 5: Las vistas del Modulo Personas en Twig
Por último, las vistas — completamente desacopladas de la lógica, viviendo en public/persons/.
Listado de Personas (public/persons/index.html.twig)
{% extends "layouts/main.html.twig" %}
{% block title %}Lista de Personas{% endblock %}
{% block content %}
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h4 class="mb-0 text-primary fw-bold">Listado de Personas</h4>
<a href="{{ base_url }}/person/new" class="btn btn-success fw-semibold">+ Nueva Persona</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th class="ps-3">ID</th>
<th>Nombre</th>
<th>Email</th>
<th>Teléfono</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
{% for person in persons %}
<tr>
<td class="ps-3 fw-bold">{{ person.id }}</td>
<td>{{ person.name }} {{ person.lastname }}</td>
<td>{{ person.email }}</td>
<td>{{ person.phone }}</td>
<td class="text-center">
<a href="{{ base_url }}/person/{{ person.id }}" class="btn btn-sm btn-info text-white">Ver</a>
<a href="{{ base_url }}/person/{{ person.id }}/edit" class="btn btn-sm btn-warning">Editar</a>
<form action="{{ base_url }}/person/{{ person.id }}/delete" method="POST" class="d-inline" onsubmit="return confirm('¿Seguro que deseas eliminar esta persona?');">
<button type="submit" class="btn btn-sm btn-danger">Eliminar</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">No hay personas registradas.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}Nueva Persona (public/persons/new.html.twig)

Ahora el formulario para crear o dar de alta a una persona en el sistema.
{% extends "layouts/main.html.twig" %}
{% block title %}Nueva Persona{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white py-3 d-flex justify-content-between align-items-center">
<h4 class="mb-0 fw-bold">Agregar Nueva Persona</h4>
<a href="{{ base_url }}/persons" class="btn btn-light btn-sm text-primary fw-semibold">Volver</a>
</div>
<div class="card-body p-4">
<form action="{{ base_url }}/person/create" method="POST">
<div class="row mb-3">
<div class="col-md-6">
<label for="name" class="form-label">Nombre</label>
<input type="text" name="name" id="name" class="form-control" required>
</div>
<div class="col-md-6">
<label for="lastname" class="form-label">Apellido</label>
<input type="text" name="lastname" id="lastname" class="form-control" required>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" id="email" class="form-control">
</div>
<div class="col-md-6">
<label for="phone" class="form-label">Teléfono</label>
<input type="text" name="phone" id="phone" class="form-control">
</div>
</div>
<div class="mb-3">
<label for="address" class="form-label">Dirección</label>
<textarea name="address" id="address" class="form-control" rows="2"></textarea>
</div>
<button type="submit" class="btn btn-success">Guardar Persona</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}Las vistas de edición y detalle (edit.html.twig y show.html.twig) siguen exactamente el mismo patrón que new.html.twig, solo que precargan los datos del registro ({{ person.name }}, {{ person.email }}, etc.) en lugar de mostrar campos vacíos.
Descargar
Este módulo de Personas ya viene incluido en LegoBox PHP v5 — no necesitas armarlo desde cero para probarlo.
Descarga el framework completo, con el módulo de autenticación y este CRUD de ejemplo listos para usar, en el artículo de presentación de LegoBox PHP, donde también están el repositorio en GitHub y el enlace de descarga directa.
Probar el módulo
- Iniciar sesión: entra a
http://localhost/lb-min-5/login(usuarioadmin, contraseñaadmin). - Listado: navega a
http://localhost/lb-min-5/persons. - Crear persona: presiona
+ Nueva Personay llena el formulario. Al guardar, recibirás un mensaje flash de confirmación. - Editar/eliminar: usa los botones correspondientes en la tabla de acciones.
De aquí en adelante
Este módulo de Personas es literalmente una plantilla: copia las cuatro piezas (tabla, modelo, servicio, rutas y vistas), cambia los nombres y campos, y en minutos tienes un CRUD nuevo funcionando — sea para productos, clientes, citas o lo que necesite tu sistema.
Te invito a ver Inventio Max, mi sistema de inventario y ventas multi-sucursal construido sobre LegoBox PHP.
¿Tienes dudas sobre cómo adaptar este módulo a tu propio proyecto? Déjamelas en los comentarios.