En este post te voy a explicar y mostrar un ejemplo de código con PHP, MySQL, Twig y Bootstrap en donde se gestionan notas y usuarios.
La app de notas es totalmente responsiva y hecha completamente con PHP, usando MySQL para la persistencia de datos.
El correo es posible gracias a Twig para renderizar la vista, y PHPMailer para enviarlos.
Como lo ves, está muy enfocado a la gestión de usuarios.
El código fuente está en GitHub, basado en mi caja de herramientas de PHP. El código contiene:
La demostración la puedes ver en mi página de apps.
Para descargar el código simplemente ve a GitHub y clona o descarga el proyecto. Recuerda que debes tener Composer y PHP instalado.
Ahora voy a explicar cómo funciona esta app de notas con PHP y MySQL.
La gestión de usuarios no la explicaré, pues es más larga, aunque muy parecida a las notas, excepto por la parte de restauración de contraseñas y contraseñas olvidadas.
No olvides que debes crear las tablas definidas en esquema.sql, y si hace falta instala un servidor de correo de pruebas para la gestión de usuarios.
Comenzamos viendo las rutas de las notas en rutas.php:
<?php
$enrutador
->group(["before" => "logueado"], function ($enrutadorVistasPrivadas) {
$enrutadorVistasPrivadas
->get("/perfil/cambiar-password", ["Parzibyte\Controladores\ControladorUsuarios", "perfilCambiarPassword"])
->post("/perfil/cambiar-password", ["Parzibyte\Controladores\ControladorUsuarios", "perfilGuardarPassword"])
->get("/notas", ["Parzibyte\Controladores\ControladorNotas", "index"])
->get("/notas/agregar", ["Parzibyte\Controladores\ControladorNotas", "agregar"])
->post("/notas/guardar", ["Parzibyte\Controladores\ControladorNotas", "guardar"])
->get("/notas/editar/{idNota}", ["Parzibyte\Controladores\ControladorNotas", "editar"])
->get("/notas/eliminar/{idNota}", ["Parzibyte\Controladores\ControladorNotas", "confirmarEliminacion"])
->post("/notas/eliminar", ["Parzibyte\Controladores\ControladorNotas", "eliminar"])
->post("/notas/editar", ["Parzibyte\Controladores\ControladorNotas", "guardarCambios"])
->get("/logout", ["Parzibyte\Controladores\ControladorLogin", "logout"]);
});
Además de las que tienen que ver con el perfil y el cierre de sesión, tenemos las rutas que son:
Cada ruta llama a un método del controlador de notas, el cual es el siguiente:
<?php
namespace Parzibyte\Controladores;
use Parzibyte\Modelos\ModeloNotas;
use Parzibyte\Redirect;
use Parzibyte\Servicios\SesionService;
use Parzibyte\Validator;
class ControladorNotas
{
public static function index()
{
return view("notas/mostrar", ["notas" => ModeloNotas::deUsuario(SesionService::leer("idUsuario"))]);
}
public static function agregar()
{
return view("notas/agregar");
}
public static function editar($idNota)
{
$nota = ModeloNotas::unaDeUsuario($idNota, SesionService::leer("idUsuario"));
if (!$nota) {
Redirect::to("/notas")->do();
}
return view("notas/editar", ["nota" => $nota]);
}
public static function confirmarEliminacion($idNota)
{
$nota = ModeloNotas::unaDeUsuario($idNota, SesionService::leer("idUsuario"));
if (!$nota) {
Redirect::to("/notas")->do();
}
return view("notas/eliminar", ["nota" => $nota]);
}
public static function guardarCambios()
{
Validator::validateOrRedirect($_POST, [
"required" => ["idNota", "contenido"],
"numeric" => "idNota",
]);
$idNota = $_POST["idNota"];
$contenido = $_POST["contenido"];
$mensaje = "Nota guardada";
$tipo = "success";
if (!ModeloNotas::actualizarDeUsuario($idNota, SesionService::leer("idUsuario"), $contenido)) {
$mensaje = "Error guardando nota";
$tipo = "warning";
}
Redirect::to("/notas")->with(["tipo" => $tipo, "mensaje" => $mensaje])->do();
}
public static function eliminar()
{
Validator::validateOrRedirect($_POST, [
"required" => "idNota",
],
"/notas");
$idNota = $_POST["idNota"];
$mensaje = "Nota eliminada";
$tipo = "success";
if (!ModeloNotas::eliminarDeUsuario($idNota, SesionService::leer("idUsuario"))) {
$mensaje = "Error eliminando nota";
$tipo = "warning";
}
Redirect::to("/notas")->with(["tipo" => $tipo, "mensaje" => $mensaje])->do();
}
public static function guardar()
{
Validator::validateOrRedirect($_POST, [
"required" => "contenido",
]);
$contenido = $_POST["contenido"];
$mensaje = "Nota agregada";
$tipo = "success";
if (!ModeloNotas::agregar(SesionService::leer("idUsuario"), $contenido)) {
$mensaje = "Error agregando nota";
$tipo = "warning";
}
Redirect::to("/notas")->with(["tipo" => $tipo, "mensaje" => $mensaje])->do();
}
}
El controlador se encarga de funcionar como pegamento entre el usuario y el software; pues valida los datos, muestra vistas (como el formulario) y redirige a otras ubicaciones.
Hay muchas llamadas al modelo de notas, el cual se encarga de comunicarse con la base de datos:
<?php
namespace Parzibyte\Modelos;
use Parzibyte\Servicios\BD;
use \PDO;
class ModeloNotas
{
public static function agregar($idUsuario, $contenido)
{
$bd = BD::obtener();
$fechaYHora = date("Y-m-d H:i:s");
$sentencia = $bd->prepare("insert into notas(fecha_hora, id_usuario, contenido) VALUES(?, ?, ?);");
return $sentencia->execute([$fechaYHora, $idUsuario, $contenido]);
}
public static function deUsuario($idUsuario)
{
$bd = BD::obtener();
$sentencia = $bd->prepare("SELECT id, fecha_hora, contenido FROM notas WHERE id_usuario = ? ORDER BY fecha_hora DESC");
$sentencia->execute([$idUsuario]);
return $sentencia->fetchAll(PDO::FETCH_OBJ);
}
public static function eliminarDeUsuario($idNota, $idUsuario)
{
$bd = BD::obtener();
$sentencia = $bd->prepare("DELETE FROM notas WHERE id = ? AND id_usuario = ?");
return $sentencia->execute([$idNota, $idUsuario]);
}
public static function actualizarDeUsuario($idNota, $idUsuario, $contenido)
{
$bd = BD::obtener();
$fechaYHora = date("Y-m-d H:i:s");
$sentencia = $bd->prepare("UPDATE notas SET fecha_hora = ?, contenido = ? WHERE id = ? AND id_usuario = ?");
return $sentencia->execute([$fechaYHora, $contenido, $idNota, $idUsuario]);
}
public static function unaDeUsuario($idNota, $idUsuario)
{
$bd = BD::obtener();
$sentencia = $bd->prepare("SELECT id, fecha_hora, contenido FROM notas WHERE id_usuario = ? AND id = ?");
$sentencia->execute([$idUsuario, $idNota]);
return $sentencia->fetchObject();
}
}
Son consultas simples, es decir, no estoy usando ningún ORM.
Puedes ver que se hace el CRUD completo de las notas con PHP, ya que hacemos un SELECT, UPDATE, DELETE e INSERT.
El formulario que agrega una nota es el siguiente:
{% extends "master.twig" %}
{% block titulo %}Agregar nota
{% endblock %}
{% block contenido %}
<main class="container-fluid">
<div class="row">
<div class="col-12 col-sm-6">
<h1 class="text-center">Agregar nota</h1>
<form action="{{URL_RAIZ}}/notas/guardar" method="post">
{% include "componentes/sesion_flash.twig" %}
<div class="form-group">
<label for="contenido">Contenido</label>
<textarea class="form-control" cols="10" id="contenido" name="contenido" required rows="3"></textarea>
</div>
<button class="btn btn-success" type="submit">Guardar</button>
<a class="btn btn-warning" href="{{URL_RAIZ}}/notas">Ver notas</a>
</form>
</div>
</div>
</main>
{% endblock %}
Su action
es la ruta notas/guardar definido en las rutas que vimos anteriormente. Tiene un campo de tipo textarea
que se llama contenido
.
Cuando una nota se guarda con éxito, esta es la salida:
Es decir, si es exitoso, se redirige a las notas y se muestra una alerta que desaparece al recargar la página.
Cuando ya tenemos notas, las mismas se muestran en un ciclo (mira el modelo de notas en el método deUsuario
) y la vista es la de arriba.
Su código es el siguiente:
{% extends "master.twig" %}
{% block titulo %}Notas
{% endblock %}
{% block contenido %}
<main class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="text-center">Notas</h1>
</div>
<div class="col-12">
<a class="btn btn-success mb-2" href="{{URL_RAIZ}}/notas/agregar">Agregar</a>
</div>
<div class="col-12">
{% include "componentes/sesion_flash.twig" %}
{% for nota in notas %}
<div class="card mb-2">
<div class="card-body">
<h5 class="card-title">{{nota.fecha_hora}}</h5>
<pre>{{nota.contenido}}</pre>
<a class="btn btn-warning" href="{{URL_RAIZ}}/notas/editar/{{nota.id}}">
<i class="fa fa-edit"></i>
</a>
<a class="btn btn-danger" href="{{URL_RAIZ}}/notas/eliminar/{{nota.id}}">
<i class="fa fa-trash"></i>
</a>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
{% endblock %}
Se hace un ciclo for
con twig y se dibujan div
s de tipo tarjeta (este estilo lo da Bootstrap)
Además, se ponen dos enlaces: uno para editar la nota y otro para eliminarla.
Cuando se hace click en editar nota, se muestra un formulario que tiene el id de la nota como un campo hidden
, y se rellena el contenido de la misma:
{% extends "master.twig" %}
{% block titulo %}Editar nota
{% endblock %}
{% block contenido %}
<main class="container-fluid">
<div class="row">
<div class="col-12 col-sm-6">
<h1 class="text-center">Editar nota</h1>
<form action="{{URL_RAIZ}}/notas/editar" method="post">
{% include "componentes/sesion_flash.twig" %}
<input type="hidden" name="idNota" value="{{nota.id}}">
<div class="form-group">
<label for="contenido">Contenido</label>
<textarea class="form-control" cols="10" id="contenido" name="contenido" required rows="3">{{nota.contenido}}</textarea>
</div>
<button class="btn btn-success" type="submit">Guardar</button>
<a class="btn btn-warning" href="{{URL_RAIZ}}/notas">Ver notas</a>
</form>
</div>
</div>
</main>
{% endblock %}
Si no entiendes de dónde vienen estos datos, mira el controlador de notas en su método editar
, y al modelo en su método unaDeUsuario
(pues recuerda que solo se pueden editar notas que le pertenecen a un usuario)
Si se guarda correctamente, se regresa a donde se muestran todas las notas con una alerta:
Cuando se elimina una nota se pide una confirmación que muestra un formulario que, al ser enviado, elimina realmente la nota (solo si le pertenece al usuario).
El formulario de confirmación es el siguiente:
{% extends "master.twig" %}
{% block titulo %}Eliminar nota
{% endblock %}
{% block contenido %}
<main class="container-fluid">
<div class="row">
<div class="col-12"></div>
<div class="col-12 col-sm-6">
<h1 class="text-center">Confirmar</h1>
<p>
¿Eliminar nota con el siguiente contenido?
<pre>{{nota.contenido}}</pre>
</p>
<form action="{{URL_RAIZ}}/notas/eliminar" method="post">
<input type="hidden" name="idNota" value="{{nota.id}}">
<button class="btn btn-danger" type="submit">Eliminar</button>
<a class="btn btn-info" href="{{URL_RAIZ}}/notas">Ver notas</a>
</form>
</div>
</div>
</main>
{% endblock %}
Y se ve así:
En caso de que se confirme y la eliminación sea correcta, de nuevo se muestran todas las notas:
Así es como funciona, a grandes rasgos, esta app de notas con PHP. Recuerda que utiliza Bootstrap 4 para ese bonito diseño, y como storage o almacenamiento una base de datos con PHP que bien podría ser cambiado por otro motor.
Te repito que esta app está basada en mi caja de herramientas que presenté anteriormente.
Eres libre de ver el código en GitHub, o registrarte y usar la app.
Mira más aplicaciones que he creado.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.
Ver comentarios
Te hago una consulta entre composer y phpunit que es mas practico para hacer integracion continua con travis en github ?
Hola, lo siento, no tengo mucha experiencia con ello por el momento así que no te puedo aconsejar.
Saludos :)
ah ok muchas gracias le voy a pegar una mirada , y una consulta se podria llegar a adaptar la poo que usas en tu codigo a php estructurado ya que yo ya poseeo el archivo index y la maquetacion en html
No entiendo bien tu pregunta, ya que el estilo de programación no afecta la maquetación. Pero de cualquier modo, sí, no veo problema en dejar la poo de lado.
Saludos
Que tal quiero hacer una funcionalidad como la que realizaste por lo que me llamo la atencion tu aplicacion. Cuando baje el codigo y lo quiero probar me tira el siguiente error
warning: require(C:\wamp64\www\notasapp/vendor/autoload.php): failed to open stream: No such file or directory in C:\wamp64\www\notasapp\index.php on line 2
es posible que haya falta un archivo para que pueda funcionar ? Te agradeceria la ayuda
Gracias
Sí, hay que instalar las dependencias. Primero: https://parzibyte.me/blog/2019/02/02/primeros-pasos-composer-explicacion-funcionamiento/
Después
composer install
Saludos