Aplicación CRUD en Laravel

Creación de una aplicación ToDo en Laravel

1. Creación del VPS

Servidor VPS en DigitalOcean.com: LEMP

Getting started after deploying LEMP

In addition to the package installation, the 1-Click also:

  • Enables the UFW firewall to allow only SSH (port 22, rate limited), HTTP (port 80), and HTTPS (port 443) access.
  • Sets the MySQL root password and runs mysql_secure_installation.
  • Sets up the debian-sys-maint user in MySQL so the system’s init scripts for MySQL will work without requiring the MySQL root user password.

After you create a LEMP 1-Click Droplet:

  • You can view the LEMP instance immediately by visiting the Droplet’s IP address in your browser.
  • You can log into the Droplet as root using either the password you set when you created the Droplet or with an SSH key, if you added one during creation.
  • The MySQL root password is in /root/.digitalocean_password.
  • The web root is /var/www/html.
  • You can get information about the PHP installation by logging into the Droplet and running php -i.

2. Configuración del servidor

Alojamiento para Laravel

Server Requirements

El servidor VPS debe tener instalada la versión mínima de PHP y estas extensiones:

  • PHP >= 8.1
  • Ctype PHP Extension
  • cURL PHP Extension
  • DOM PHP Extension
  • Fileinfo PHP Extension
  • Filter PHP Extension
  • Hash PHP Extension
  • Mbstring PHP Extension
  • OpenSSL PHP Extension
  • PCRE PHP Extension
  • PDO PHP Extension
  • Session PHP Extension
  • Tokenizer PHP Extension
  • XML PHP Extension

Conexión al droplet por ssh ( con un usuario diferente a root:):

ssh usuario@alumno.me

How to install all required PHP extensions for Laravel 8?

sudo apt install openssl php-common php-curl php-json php-mbstring php-mysql php-xml php-zip

Installation of PHP Extensions for Laravel 9

sudo apt install php8.2-common php8.2-mysql php8.2-xml php8.2-xmlrpc php8.2-curl php8.2-gd php8.2-imagick php8.2-cli php8.2-dev php8.2-imap php8.2-mbstring php8.2-opcache php8.2-soap php8.2-zip php8.2-intl -y

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

Instalar node.js y npm en Ubuntu 20.04

How to install Node.js in Ubuntu 22.04

Installation instructions

#Using Ubuntu
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\
sudo apt-get install -y nodejs
node -v
npm -v

4. Instalación de Laravel

Installation Via Composer

Do not run Composer as root/super user! See https://getcomposer.org/root for details

Usando Composer

cd ~
# borrar la carpeta todo, si existe
rm -r todo
composer create-project laravel/laravel todo
 
cd todo

ls -la

o usando el instalador de Laravel

composer global require laravel/installer

laravel new todo

Error en el droplet con 1 Gb de RAM (son necesarios 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 todoDB, el usuario todoUser y la password Malaga2122*

sudo mysql
 
CREATE DATABASE todoDB;
CREATE USER 'todoUser'@'localhost' IDENTIFIED BY 'Malaga2122*';
GRANT ALL PRIVILEGES ON todoDB.* TO 'todoUser'@'localhost';
FLUSH PRIVILEGES;
exit

Modificar el fichero .env:

nano .env

Información de conexión a la base de datos:

DB_CONNECTION=mysql

DB_HOST=127.0.0.1

DB_PORT=3306

DB_DATABASE=todoDB

DB_USERNAME=todoUser

DB_PASSWORD=Malaga2122*

Montar en el equipo local el sistema de ficheros remoto en el VPS:

Nota: Estos comandos se ejecutan en el equipo local, no en el VPS

mkdir servidor

sshfs usuario@alumno.me:/home/usuario/todo/ servidor -p 22

Para desmontarlo:

fusermount -u servidor

6. MVC en Laravel

HTTP:

Using HTTP Methods for RESTful Services

7. Creación de la aplicación Todo

CRUD To-do Application in Laravel

Aviso: ejecutar 2 veces el comando

npm install && npm run dev

Resultado final:

  Laravel Mix v6.0.43   
                         

✔ Compiled Successfully in 3666ms
┌───────────────────────────────────────────────────────────────────┬──────────┐
                                                              File  Size     
├───────────────────────────────────────────────────────────────────┼──────────┤
                                                        /js/app.js  694 KiB  
                                                       css/app.css  48.4 KiB

Livewire

Laravel Livewire, una nueva forma de replantear tus desarrollos con Laravel

Primeros pasos con Livewire

Rutas:

php artisan route:list

Blade:

Let’s Blade

Create Layout Using Laravel Blade Templating Engine

 

8. Permisos en la carpeta storage

Poner permiso de escritura para www-data en carpeta storage o cambiar el propietario/grupo de la carpeta:

cd todo

sudo chown -R www-data:www-data storage

How to set up file permissions for Laravel?

 

9. Modificación de las vistas

fichero public/css/styles.css (obtenido de welcome.blade.php)

nano public/css/styles.css

Contenido del fichero:

html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-center{text-align:center}.text-gray-200{--text-opacity:1;color:#edf2f7;color:rgba(237,242,247,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#e2e8f0;color:rgba(226,232,240,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.dark\:bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.dark\:border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.dark\:text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.dark\:text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}}

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}

fichero 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">

        <!-- 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>

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">

fichero 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 href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" 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  -->
        <link rel="stylesheet" href="css/styles.css">
        <!-- <link rel="stylesheet" href="https://raw.githubusercontent.com/necolas/normalize.css/master/normalize.css">  -->
        
        <style>
            body {
                font-family: 'Nunito', sans-serif;
            }
        </style>
    </head>
    <body class="antialiased">
        <div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
            @if (Route::has('login'))
                <div class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
                    @auth
                        <a href="{{ url('/dashboard') }}" class="text-sm text-gray-700 underline">Dashboard</a>
                    @else
                        <a href="{{ route('login') }}" class="text-sm text-gray-700 underline">Log in</a>

                        @if (Route::has('register'))
                            <a href="{{ route('register') }}" class="ml-4 text-sm text-gray-700 underline">Register</a>
                        @endif
                    @endauth
                </div>
            @endif
                      

            <div class="max-w-4xl mx-auto sm:px-6 lg:px-4">
                
                <div class="flex justify-center text-primary">
                    <h1>Todo Tasks</h1>					
                </div>
                <div class="flex justify-center text-success">
                    <h3>Manage your tasks online</h3>
                </div>
                <div class="flex justify-center text-secondary">
                    <h4>info@alumno.me</h4>
                </div>


                <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>
    </body>
</html>

fichero 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">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-5">
                <table class="w-full text-md rounded mb-4">
                    <thead>
                    <tr>
                        <th>
                        <div class="flex-auto text-left text-2xl mt-4 mb-4">Tasks List</div>
                        </th>
                        <th><div class="flex-auto text-right float-right mt-4 mb-4">
                        <a href="/task" class="bg-gray-500 hover:bg-gray-700 text-white font-bold mr-8 py-2 px-4 rounded">Add a new Task</a>                        
                    </div></th>
                    </tr>
                    <tr class="border-b">
                        <th class="text-left p-3 px-5">Task</th>
                        <th class="text-left p-3 px-5">Actions</th>
                    </tr>
                    </thead>
                    <tbody>
                    @foreach(auth()->user()->tasks as $task)
                        <tr class="border-b hover:bg-orange-100">
                            <td class="p-3 px-5">
                                {{$task->description}}
                            </td>
                            <td class="p-3 px-5">                                  
                            <form action="/task/{{$task->id}}" class="inline-block">
                                <a href="/task/{{$task->id}}" name="edit" class="text-sm bg-gray-500 hover:bg-gray-700 text-white py-1 px-4 rounded focus:outline-none focus:shadow-outline">Edit</a>    
                                <button type="submit" name="delete" formmethod="POST" class="text-sm bg-red-500 hover:bg-red-700 text-white py-1 px-2 rounded focus:outline-none focus:shadow-outline">Delete</button>
                                {{ csrf_field() }}
                            </form>
                            </td>
                        </tr>
                    @endforeach
                    </tbody>
                </table>
                
            </div>
        </div>
    </div>
</x-app-layout>

fichero resources/views/add.blade.php

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Add Task') }}
        </h2>
    </x-slot>
    
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-5">
            
                <form method="POST" action="/task">
    
                    <div class="form-group">
                        <textarea name="description" class="bg-gray-100 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 your task'></textarea>  
                        @if ($errors->has('description'))
                            <span class="text-danger">{{ $errors->first('description') }}</span>
                        @endif
                    </div>
    
                    <div class="form-group">
                        <button type="submit" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">Add Task</button>
                    </div>
                    {{ csrf_field() }}
                </form>
            </div>
        </div>
    </div>
</x-app-layout>

 

fichero resources/views/edit.blade.php

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Edit Task') }}
        </h2>
    </x-slot>
    
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-5">
            
                <form method="POST" action="/task/{{ $task->id }}">
    
                    <div class="form-group">
                        <textarea name="description" class="bg-gray-100 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">{{$task->description }}</textarea>	
                        @if ($errors->has('description'))
                            <span class="text-danger">{{ $errors->first('description') }}</span>
                        @endif
                    </div>
    
                    <div class="form-group">
                        <button type="submit" name="update" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">Update task</button>
                    </div>
                {{ csrf_field() }}
                </form>
            </div>
        </div>
    </div>
</x-app-layout>

 

10. Limpiar la caché de Laravel

sudo php artisan cache:clear
sudo php artisan config:clear
sudo php artisan route:clear
sudo php artisan view:clear

 

11. Comprobar el funcionamiento

https://todo.alumno.me

Todo Laravel

Register Laravel

Login

Dashboard

13. Instalación de Telescope y Toolbar

Laravel Telescope

Introducción a Laravel Telescope

sudo chmod 777 storage/logs/laravel.log

 

#carpeta del proyecto de Laravel
composer require laravel/telescope --dev

php artisan telescope:install

php artisan migrate

Fichero de configuración: config/telescope.php

nano config/telescope.php

Acceso:
todo.alumno.me/telescope

 

Toolbar

composer require fruitcake/laravel-telescope-toolbar --dev

php artisan vendor:publish --provider="Fruitcake\\TelescopeToolbar\\ToolbarServiceProvider"

La barra se mostrará por defecto en la pate inferior del navegador cuando Telescope esté habilitado y AP_DEBUG sea True.

Toolbar

Otra herramienta de depuración:

Lravel Debugbar

Repositorio de Debugbar en GitHub

__________________________________________

Ejemplo de plantillas con Blade en Laravel 9:

Laravel 9 Bootstrap Auth Scaffolding Tutorial

Laravel 9 CRUD Application Tutorial Example

CSRF Protection in Laravel

Deja una respuesta