php

App de notas con PHP – Ejemplo de código

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.

  • Un usuario puede ver, crear, editar y eliminar notas
  • Cualquier usuario puede registrarse usando su correo electrónico
  • Los usuarios pueden cambiar su contraseña
  • Para que el usuario se registre, se debe verificar el correo electrónico
  • Los usuarios pueden resetear su contraseña olvidada
  • Un usuario no puede ver ni modificar las notas de otro usuario
  • Las notas guardadas deben guardar la fecha y hora de creación

Como lo ves, está muy enfocado a la gestión de usuarios.

Código fuente, demostración y descargas

El código fuente está en GitHub, basado en mi caja de herramientas de PHP. El código contiene:

  • Validación con Valitron
  • Renderizado de vistas con Twig
  • Diseño con Bootstrap
  • Sesiones propias con MySQL
  • Flashdata en las sesiones

La demostración la puedes ver en mi página de apps.

App de notas con PHP, MySQL, Bootstrap y Twig

Para descargar el código simplemente ve a GitHub y clona o descarga el proyecto. Recuerda que debes tener Composer y PHP instalado.

Explicación de app de notas con PHP

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.

Rutas y controladores

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:

  • GET notas – Para ver las notas que el usuario tiene guardadas
  • GET notas/agregar – Formulario para agregar una nota
  • POST notas/guardar – Fíjate que es post; se encarga de procesar el formulario
  • GET notas/editar/{idNota} – Mostrar formulario para editar una nota
  • GET notas/eliminar/{idNota} – Confirmación para eliminar nota
  • POST notas/eliminar – Eliminar una nota
  • POST notas/editar – Procesar formulario para guardar la nota

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();
    }
}

Modelo de notas (el que habla con la base de datos)

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.

Formulario para agregar una nota

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.

Agregar nota

Cuando una nota se guarda con éxito, esta es la salida:

Nota agregada en PHP – Alerta con Bootstrap

Es decir, si es exitoso, se redirige a las notas y se muestra una alerta que desaparece al recargar la página.

Mostrar notas

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 divs de tipo tarjeta (este estilo lo da Bootstrap)

Además, se ponen dos enlaces: uno para editar la nota y otro para eliminarla.

Editar nota

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)

Editar nota – app con PHP

Si se guarda correctamente, se regresa a donde se muestran todas las notas con una alerta:

Nota editada. Mostrando alerta de Bootstrap indicando operación exitosa

Eliminar nota

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í:

Formulario para pedir confirmación de eliminación de nota

En caso de que se confirme y la eliminación sea correcta, de nuevo se muestran todas las notas:

Alerta indicando que la nota fue eliminada – App de notas con MySQL y PHP

Conclusión

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

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

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

3 días hace

Esta web usa cookies.