Aplicación CRUD en Laravel 12
Creación de una aplicación CRUD en Laravel 12
1. Creación del VPS
Servidor VPS en DigitalOcean.com: LEMP
Getting started after deploying LEMP
2. Configuración del servidor
El servidor VPS debe tener instalada la versión mínima de PHP (8.2) y las extensiones necesarias.
Conexión al droplet por ssh ( con un usuario diferente a root:):
ssh usuario@alumno.me
Instalar paquetes de PHP
sudo apt update sudo apt install curl -y sudo apt install php php-common php-gd php-mysql php-curl php-intl php-mbstring php-bcmath php-xml php-zip -y
Server configuration for Nginx
sudo nano /etc/nginx/sites-available/laravel
# /etc/nginx/sites-available/laravel server { # sustituir usuario por el nombre de usuario usado root /home/paco/notesApp/public; # Add index.php to the list if you are using PHP index index.php index.html; # sustituir alumno.me por el dominio usado server_name laravel.alumno.me; location / { # First attempt to serve request as file, then as directory, then f> # try_files $uri $uri/ =404; try_files $uri $uri/ /index.php?$query_string; } # versión 8.4 de PHP location ~ .php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; } }
3. Instalación de Composer y node.js
How to Install Composer in Ubuntu 22.04 LTS
curl -sS https://getcomposer.org/installer -o composer-setup.php sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer composer -V
Node Installation instructions
#Using Ubuntu curl -fsSL https://deb.nodesource.com/setup_23.x | sudo -E bash - && sudo apt-get install -y nodejs node -v npm -v
error: trying to overwrite ‘/usr/include/node/common.gypi’
https://github.com/nodesource/distributions/issues/1691
sudo apt purge libnode-dev sudo apt-get install -y nodejs node -v npm -v
4. Instalación de Laravel
(Eliminar la carpeta notesApp, si ya estaba creada)
cd ~ # borrar la carpeta notesApp, si ya existe rm -r notesApp
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Crear el proyecto
composer create-project laravel/laravel notesApp cd notesApp composer require laravel/jetstream php artisan jetstream:install livewire
¿Qué es Laravel Jetstream y cómo empezar?
Otra forma: Usando el instalador de Laravel
composer global require laravel/installer laravel new notesApp
Error al usar el instalador de Laravel
Error en el droplet con 1 Gb de RAM (son convenientes 2 Gb de RAM, como mínimo):Redimensionar el droplet con 2 Gb de memoria RAM (cuando se termine la instalación se puede volver a redimensionar a 1 Gb):
5. Base de datos
Crear la base de datos notesDB y el usuario user_notesDB con la password Malaga2425*
Conectarse al servidor de bases de datos
sudo mysql
Ejecutar estos comandos:
CREATE DATABASE notesDB; CREATE USER 'user_notesDB'@'localhost' IDENTIFIED BY 'Malaga2425*'; GRANT ALL PRIVILEGES ON notesDB.* TO 'user_notesDB'@'localhost'; FLUSH PRIVILEGES; exit
Modificar el fichero .env:
nano .env
Información de conexión a la base de datos:
# DB_CONNECTION=sqlite DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=notesDB DB_USERNAME=user_notesDB DB_PASSWORD=Malaga2425* DB_COLLATION=utf8mb4_unicode_ci
Instalar phpMyAdmin en el VPS (opcional)
ssh usuario@alumno.me sudo apt install phpmyadmin sudo apt install php8.4-mbstring sudo ln -s /usr/share/phpmyadmin /var/www/html/phpmyadmin sudo nginx -t sudo systemctl restart nginx sudo systemctl status nginx exit
6. Prueba
(php artisan migrate)
Cambiar el propietario de la carpeta storage y bootstrap/cache:
cd ~/notesApp sudo chown -R www-data:www-data storage sudo chown -R www-data:www-data bootstrap/cache
Permisos en bootstrap/cache y storage/logs/laravel.log:
sudo chmod 777 bootstrap/cache sudo chmod 777 storage/logs/laravel.log
Ejecutar
npm install npm run build
Comprobación:
7. Montar en el equipo local el sistema de ficheros remoto del VPS
Uso de SSH FS en Windows/Linux
Nota: Estos comandos se ejecutan en el equipo local, no en el VPS
mkdir servidor sudo apt install sshfs sshfs usuario@alumno.me:/home/usuario/notesApp/ servidor -p 22
Abrir la carpeta con el IDE Visual Studio Code
cd servidor code .
Para desmontarlo:
fusermount -u servidor
8. Instalar extensiones en Visual Studio Code
Extensión Database Client
Configurar la extensión en las pestañas main y ssh
![]() |
![]() |
Usar sshfs en Visual Studio Code:
Instalar la extensión SSH FS
Configurarla con el host, ruta a la carpeta remota y usuario
Instalar la extensión PHP Intelephense
Instalar la extensión GitHub Copilot y GitHub Copilot Chat
9. MVC en Laravel
10. Creación de la aplicación CRUD
CRUD To-do Application in Laravel
Modelo:
con la opción -m para crear la migración
php artisan make:model Note -m
app/Models/Note.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Models\User; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Note extends Model { use HasFactory; protected $fillable = ['title', 'description']; public function user():BelongsTo { return $this->belongsTo(User::class); } }
app/Models/User.php
<?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Jetstream\HasProfilePhoto; use Laravel\Sanctum\HasApiTokens; use App\Models\Note; use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Authenticatable { use HasApiTokens; /** @use HasFactory<\Database\Factories\UserFactory> */ use HasFactory; use HasProfilePhoto; use Notifiable; use TwoFactorAuthenticatable; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for serialization. * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', 'two_factor_recovery_codes', 'two_factor_secret', ]; /** * The accessors to append to the model's array form. * * @var array<int, string> */ protected $appends = [ 'profile_photo_url', ]; /** * Get the attributes that should be cast. * * @return array<string, string> */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } public function notes(): HasMany { return $this->hasMany(Note::class); } }
Migraciones:
app/database/migrations/2025_05_07_084758_create_notes_table.php
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('notes', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('description'); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('notes'); } };
Realizar la migración:
php artisan migrate o php artisan migrate:refresh
Migrar una sola tabla:
php artisan migrate --path=/database/migrations/2025_05_07_084758_create_tasks_table.php
Rutas
routes/web.php
<?php use Illuminate\Support\Facades\Route; use App\Http\Controllers\NoteController; Route::get('/', function () { return view('welcome'); }); Route::middleware([ 'auth:sanctum', config('jetstream.auth_session'), 'verified', ])->group(function () { //Route::get('/dashboard', function () { // return view('dashboard'); //})->name('dashboard'); Route::get('/dashboard',[NoteController::class, 'index'])->name('dashboard'); Route::get('/note',[NoteController::class, 'add']); Route::post('/note',[NoteController::class, 'create']); Route::get('/note/{note}', [NoteController::class, 'edit']); Route::put('/note/{note}', [NoteController::class, 'update']); Route::delete('/note/{note}', [NoteController::class, 'destroy']); });
Controlador
php artisan make:controller NoteController
app/Http/Controllers/NoteController.php
<?php namespace App\Http\Controllers; use App\Models\Note; use Illuminate\Http\Request; class NoteController extends Controller { /** * Display a listing of the resource. */ public function index() { $notes = auth()->user()->notes(); // return view('dashboard', compact('notes')); return view('dashboard')->with('notes', $notes); } /** * Show the form for creating a new resource. */ public function create(Request $request) { //$this->validate($request, [ // 'title' => 'required', // 'description' => 'required' //]); // Validar los datos del formulario $request->validate([ 'title' => 'required|max:255', 'description' => 'required' ]); $note = new Note(); $note->title = $request->title; $note->description = $request->description; $note->user_id = auth()->user()->id; $note->save(); return redirect('/dashboard'); } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * Add a newly created resource in storage. */ public function add() { return view('add'); } /** * Display the specified resource. */ public function show(Note $note) { // } /** * Show the form for editing the specified resource. */ public function edit(Note $note) { if (auth()->user()->id == $note->user_id) { return view('edit', compact('note')); } else { return redirect('/dashboard'); } } /** * Update the specified resource in storage. */ public function update(Request $request, Note $note) { //$this->validate($request, [ // 'title' => 'required', // 'description' => 'required' //]); $request->validate([ 'title' => 'required|max:255', 'description' => 'required' ]); $note->title = $request->title; $note->description = $request->description; $note->save(); return redirect('/dashboard'); } /** * Remove the specified resource from storage. */ public function destroy(Note $note) { $note->delete(); return redirect('/dashboard'); // return redirect('/dashboard')->with('success', 'Note deleted successfully'); } }
Vistas
Blade, Bootstrap y TailwindCSS
Se encuentran en resources/views
- dashboard.blade.php ( Dashboard muestra la lista de tareas)
- add.blade.php (Formulario para añadir una nueva tarea)
- edit.blade.php (Formulario para añadir una nueva tarea)
- welcome.blade.php (página inicial)
resources/views/dashboard.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{ __('Dashboard') }} </h2> </x-slot> <div class="py-12 dark:bg-gray-800"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-5"> <table class="w-full text-md rounded mb-4 dark:bg-gray-800"> <thead> <tr> <th> <div class="flex-auto text-left text-2xl mt-4 mb-4 dark:text-white">Notes List</div> </th> <th> <div class="flex-auto text-right float-right mt-4 mb-4"> <a href="/note" class="bg-blue-500 dark:bg-cyan-700 hover:bg-gray-700 text-white font-bold mr-8 py-2 px-4 rounded">Add a new note</a> </div> </th> </tr> <tr class="border-b dark:text-white text-center"> <th class="text-center p-3 px-5">Title</th> <th class="text-center p-3 px-5">Description</th> <th class="text-right p-3 px-5">Actions</th> </tr> </thead> <tbody> @foreach(auth()->user()->notes as $note) <tr class="border-b hover:bg-orange-100 dark:text-white text-center"> <td class="p-3 px-5"> {{$note->title}} </td> <td class="p-3 px-5"> {{$note->description}} </td> <td class="p-3 px-5"> <td> <a href="/note/{{$note->id}}" class="btn btn-primary">Edit</a> </td> <td> <form action="/note/{{$note->id}}" method="post"> @csrf @method('DELETE') <button type="submit" class="btn btn-danger">Delete</button> </form> </td> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </x-app-layout>
resources/views/add.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{ __('Add Note') }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-5"> <form method="POST" action="/note"> @csrf <div class="form-group"> <textarea name="title" class="bg-gray-100 dark:text-white dark:bg-gray-800 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white" placeholder='Enter the title'></textarea> @if ($errors->has('title')) <span class="text-danger dark:text-white dark:bg-gray-500">{{ $errors->first('title') }}</span> @endif </div> <div class="form-group"> <textarea name="description" class="bg-gray-100 dark:text-white dark:bg-gray-800 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white" placeholder='Enter the description'></textarea> @if ($errors->has('description')) <span class="text-danger dark:text-white dark:bg-gray-500">{{ $errors->first('description') }}</span> @endif </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Add note</button> </div> </form> </div> </div> </div> </x-app-layout>
resources/views/edit.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{ __('Edit Note') }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-5"> <form method="POST" action="/note/{{ $note->id }}"> @csrf @method('PUT') <div class="form-group"> <textarea name="title" class="bg-gray-100 dark:text-white dark:bg-gray-800 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white">{{$note->title }}</textarea> @if ($errors->has('title')) <span class="text-danger dark:text-white dark:bg-red-500">{{ $errors->first('title') }}</span> @endif </div> <div class="form-group"> <textarea name="description" class="bg-gray-100 dark:text-white dark:bg-gray-800 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white">{{$note->description }}</textarea> @if ($errors->has('description')) <span class="text-danger dark:text-white dark:bg-red-500">{{ $errors->first('description') }}</span> @endif </div> <div class="form-group"> <button type="submit" name="update" class="btn btn-primary">Update note</button> </div> </form> </div> </div> </div> </x-app-layout>
public/css/styles.css (obtenido de welcome.blade.php)
public/css/styles.css
Contenido del fichero styles.css
fichero public/css/colors.css
.bg-blue-500 {background-color:#66bbe3} .bg-blue-700 {background-color:#1f93c9} .bg-red-500 {background-color:#e37966} .bg-red-700 {background-color:#b73f24}
Añadir Bootstrap
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
resources/views/layouts/app.blade.css
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Fonts --> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> <link rel="stylesheet" href="css/styles.css"> <link rel="stylesheet" href="css/colors.css"> <!-- Bootstrap --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <!-- Scripts --> @vite(['resources/css/app.css', 'resources/js/app.js']) <!-- Styles --> @livewireStyles </head> <body class="font-sans antialiased"> <x-banner /> <div class="min-h-screen bg-gray-100"> @livewire('navigation-menu') <!-- Page Heading --> @if (isset($header)) <header class="bg-white shadow"> <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> {{ $header }} </div> </header> @endif <!-- Page Content --> <main> {{ $slot }} </main> </div> @stack('modals') @livewireScripts </body> </html>
resources/views/welcome.blade.php
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> <!-- Fonts --> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" /> <!-- Bootstrap --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <!-- Styles --> <!-- normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css --> <!-- Styles / Scripts --> @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) @vite(['resources/css/app.css', 'resources/js/app.js']) @else <link rel="stylesheet" href="css/styles.css"> @endif </head> <body class="bg-[#FDFDFC] dark:bg-[#0a0a0a] text-[#1b1b18] flex p-6 lg:p-8 items-center lg:justify-center min-h-screen flex-col"> <header class="w-full lg:max-w-4xl max-w-[335px] text-sm mb-6 not-has-[nav]:hidden"> @if (Route::has('login')) <nav class="flex items-center justify-end gap-4"> @auth <a href="{{ url('/dashboard') }}" class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal" > Dashboard </a> @else <a href="{{ route('login') }}" class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] text-[#1b1b18] border border-transparent hover:border-[#19140035] dark:hover:border-[#3E3E3A] rounded-sm text-sm leading-normal" > Log in </a> @if (Route::has('register')) <a href="{{ route('register') }}" class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal"> Register </a> @endif @endauth </nav> @endif </header> <div class="flex items-center justify-center w-full transition-opacity opacity-100 duration-750 lg:grow starting:opacity-0"> <main class="flex max-w-[335px] w-full flex-col-reverse lg:max-w-4xl lg:flex-row"> <div class="max-w-4xl mx-auto sm:px-6 lg:px-4"> <div class="flex justify-center text-primary"> <h1>Notes</h1> </div> <div class="flex justify-center text-success"> <h3>Manage your notes online</h3> </div> <br /> <div class="flex justify-center text-secondary"> <h4>info@alumno.me</h4> </div> <br /> <br /> <div class="flex justify-center"> <div class="ml-4 text-center text-sm text-gray-500"> Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }}) </div> </div> </div> </div> @if (Route::has('login')) <div class="h-14.5 hidden lg:block"></div> @endif </body> </html>
11. Limpiar la caché de Laravel
php artisan cache:clear php artisan config:clear php artisan route:clear php artisan view:clear
12. Prueba
13. Subir los ficheros a un repositorio en GitHub
ssh usuario@alumno.me
Instalar y configurar Git en el VPS
sudo apt install git git config --global user.name "John Doe" git config --global user.email johndoe@example.com
Crear el repositorio en GitHub
Subir los ficheros al repositorio (sustituyendo el TOKEN por el valor que tenga cada uno)
cd ~/notesApp echo "# Notes App with Laravel" >> README.md git init git branch -M main git remote add origin https://TOKEN@github.com/paco-portada/notesapplaravel2025.git git add . git commit -m "first commit" git push -u origin main
Ayuda: Crear un repositorio y subir nuestro proyecto local a Github con VSCode
Más información:
Laravel 12 CRUD
Laravel 11 CRUD:
Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.