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.