Debido a las actualizaciones recientes (a fecha de esta entrada) necesitamos actualizar nuestra base de conocimientos. Es muy útil tener un glosario a modo de resumen de ciertas definiciones. Además, los pasos para crear un proyecto desde cero han variado ligeramente.

Crear un proyecto

Cuando creamos un proyecto nuevo de Laravel 9 con los comandos habituales, elegiremos en más de una ocasión usar Jetstream. En el momento de ejecutar el comando de instalación de Laravel con Jetstream, la consola de comandos nos preguntará si queremos usar Liveware o Inertia. Al elegir Liveware, se creará el proyecto con esas opciones. Se ejecutará el comando «npm install» (instalando todas las dependencias de npm dentro del proyecto) seguido de «npm run dev», que creará un nuevo servidor de Vite.

Cuando necesitemos subir el proyecto tendremos que hacer uso del comando «npm run build» para que se cree el manifiesto donde estará compilado todo nuestro js y css. El servidor de Vite ya no será necesario, pero el proyecto esperará tener los manifiestos.

Glosario

Jetstream

Jetstream es un kit de inicio de aplicaciones diseñado para Laravel y proporciona la implementación para el inicio de sesión, el registro, la verificación de correo electrónico, la autenticación de dos factores, la gestión de sesiones, la API a través de Laravel Sanctum y las funciones opcionales de gestión de equipos de su aplicación.

A la hora de trabajar con Jetstream hayq ue escoger entre dos interfaces: Livewire e Inertia.js. La elección depende del lenguaje de los templates.

Liveware

Laravel Livewire es una librería que simplifica las interfaces de Blade y las hace más modernas, reactivas y dinámicas.

https://laravel-livewire.com/

Al usar Livewire, se puede elegir qué partes de la aplicación serán un componente de Livewire, mientras que el resto de la aplicación se puede representar como las plantillas Blade tradicionales.

Vite

Vite es un compilador que se mantendrá a la escucha de cualquier cambio en nuestro código de javascript y css, y creará los assets que incluiremos finalmente en nuestra página.

Para poder trabajar en el entorno local y cargar los estilos de nuestra aplicación, necesitamos tener el servidor encendido. Para ello ejecurtaríamos el comando «npm run dev» y ya veríamos los estilos.

Laravel ahora usa Vite: https://www.youtube.com/watch?v=4gxxWTe3pVA&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=32

Bootstrap

Cómo usar Bootstrap en Laravel 9 usando Vite: https://www.youtube.com/watch?reload=9&v=D_-i6ZiWHuQ

Bootstrap es un framework front-end utilizado para desarrollar aplicaciones web y sitios mobile first, o sea, con un layout que se adapta a la pantalla del dispositivo utilizado por el usuario. El propósito del framework es ofrecerle al usuario una experiencia más agradable cuando navega en un sitio.

Por esta razón, tiene varios recursos para configurar los estilos de los elementos de la página de una manera simple y eficiente, además de facilitar la construcción de páginas que, al mismo tiempo, están adaptadas para la web y para dispositivos móviles.

¿Cómo funciona Bootstrap?
Bootstrap está constituido por una serie de archivos CSS y JavaScript responsables de asignar características específicas a los elementos de la página.

¿Cómo configurar y usar Bootstrap?
Hay diferentes formas de configurarlo o estructurarlo para usarlo en una aplicación web. Sin embargo, para que funcione correctamente, es necesario agregar las bibliotecas JQuery y Popper.js, necesarias para la ejecución de algunos componentes de Bootstrap.

Para comenzar a usar Bootstrap en una página, es necesario agregar las referencias de los principales archivos del framework en la página principal de la aplicación.

Tailwind

Tailwind es un framework CSS. Es Brinda un conjunto único de clases de utilidad que hace que el proceso de desarrollo sea muy fácil y da como resultado un diseño único. El código y el diseño web se puede personalizar libremente.

Instalar el framework Tailwind no es tan simple como instalar un framework bootstrap, requiere un poco de configuración con laravel Mix.

Npm

https://docs.npmjs.com/cli/v6/commands/npm

npm es el administrador de paquetes para la plataforma Node JavaScript. Coloca los módulos en su lugar para que el nodo pueda encontrarlos y gestiona los conflictos de dependencia de manera inteligente.

Usualmente npm se necesita porque el proyecto requiere instalaciones.

Laravel Mix (antes de Vite)

https://laravel.com/docs/8.x/mix

Laravel Mix proporciona una API fluida para definir los pasos de compilación de Webpack utilizando varios procesadores de CSS y JavaScript comunes.

El objetivo es, entre otras cosas, procesar todo nuestro código css, minificarlo y combinarlo en un solo archivo. Lo mismo para el código javascript, minificar, ofuscar y combinar todo el código en un solo archivo. Con laravel mix nuestras páginas web son mas seguras y rápidas.

Laravel mix está optimizado para usar con Laravel, pero se puede utilizar en cualquier aplicación web, aunque no se use Laravel.

¿Qué es Jetstream?

Directamente de la documentación de Laravel:

Jetstream provides the implementation for your application’s login, registration, email verification, two-factor authentication, session management, API…

Instalación

Nuevo proyecto con Jetstream

Vamos a la consola de Gitbash, y dentro de nuestro directorio htdocs de XAMPP la línea de más abajo para un nuevo proyecto. A este lo llamaremos «bootstrap».

Tendremos que elegir entre liveware y inertia. Si estás familiarizado con blade, es más sencillo liveware. Con inertia es mejor si estás más familiarizado con javascript, por ejemplo Vue. Lo haremos con liveware.

laravel new bootstrap --jet

Base de datos

El siguiente paso es crear una base de datos llamada bootstrap (así la llamaremos) dentro de phpmyadmin.

Desde la misma consola de Gitbash, podemos ir a nuestro proyecto:

cd bootstrap

Y ejecutar las migraciones:

php artisan migrate

Iniciar el server

Inicializamos el servidor así:

php artisan serve

Se nos informará que podemos acceder mediante una url como: http://127.0.0.1:8000.

Instalar Bootstrap

Abrimos nuestro proyecto desde VSC. En la carpeta principal podemos encontrar el archivo de configuración de tailwind, pero nosotros queremos bootstrap, con lo que instalaremos un paquete llamado Jetstrap.

La siguiente línea la hemos encontrado en el repositorio de Github https://github.com/nascent-africa/jetstrap, en el readme.

composer require nascent-africa/jetstrap --dev

Ahora necesitamos ejecutar el siguiente comando para liveware:

php artisan jetstrap:swap livewire

Nos aparece el siguiente mensaje una vez terminado: Please execute the «npm install && npm run dev» command to build your assets para que los estilos se compilen. Ya estaremos viendo que el archivo de configuración de tailwind no existe, y que en la carpeta resource/sass están las variables y estilos de bootstrap, y que el archivo webpack.mix.js todo se va a compilar con sass y que buscará el archivo de la carpeta sass que se llama app.scss.

Ejecutamos por orden:

npm install
npm run dev

Al ejecutar el segundo comando nos hemos encontrado un error de la versión de node que se explica en el vídeo siguiente:

Descargamos el instalador del node más actualizado para nuestro sistema operativo y los instalamos. Con esto hemos podido ejecutar el comando, pero los estilos no se cargan y probamos a empezar todo de nuevo, esta vez con node actualizado a la última versión.

Y ahora nos encontramos con otro «problema», con una actualización Laravel ahora usa Vite, con lo que los pasos anteriores no funcionan como esperábamos.

IMPORTANTE: Para los estilos es indispensable conocer el funcionamiento de Vite, que es una mejora de Laravel con respecto a las versiones anteriores. Por lo tanto, el uso y la instalación de Tailwind o de Bootstrap va a estar condicionado por el funcionamiento de Vite.

Recursos:

Enlace de Youtube: https://www.youtuhttps://www.youtube.com/watch?v=v-R5FLGAB58&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=30

Archivos:

  • resources/views/layouts/header.blade.php

Agregaremos al menú de navegación un link «contáctanos». Al pulsarlo, nos redirigirá a una vista que mostrará un formulario donde poner el nombre, el mail y un mensaje. Esa información nos será enviada en un correo electrónico.

Enlace en el menú

Abrimos la platilla de headers dentro de la carpeta partials, para añadir un link «contactanos» en nuestro menú de navegación.

            <ul>
                <li><a href="{{route('home')}}" class="{{request()->routeIs('home') ? 'active' : '' }}">Home</a>
                    {{-- @dump(request()->routeIs('home')) --}}
                </li>
                <li><a href="{{route('cursos.index')}}" class="{{request()->routeIs('cursos.*') ? 'active' : '' }}">Cursos</a>
                    {{-- @dump(request()->routeIs('cursos.index')) --}}
                </li>
                <li><a href="{{route('nosotros')}}" class="{{request()->routeIs('nosotros') ? 'active' : '' }}">Nosotros</a>
                    {{-- @dump(request()->routeIs('nosotros')) --}}
                </li>
                <li><a href="">Contactanos</a>
                </li>
</ul>

Ruta para almacenar mail

Abrimos web.php. Le damos a la ruta get que hemos creado para enviar mails el nombre «contactanos.index».

Route::get('contactanos', function() {
    $correo = new ContactanosMailable;
    Mail::to('weblikonet@gmail.com')->send($correo);
    return "mensaje enviado";
})->name('contactanos.index');

Ruta para mostrar el formulario

Hacemos que el nuevo enlace de contacto apunte a esa ruta.


<li><a href="{{route('contactanos.index')}}">Contactanos</a></li>

Controlador para la ruta y métodos

Creamos un controlador para que administre la ruta.

php make:controller ContactanosController 

Creamos dos métodos para este controlador: index() para mostrar el formulario y store() para procesarlo y enviar el mail.

class ContactanosController extends Controller
{
    //
    public function index(){
        
    }

    public function store(){
    
    }

}

Modificamos el controlador

Habíamos escrito esta función en la ruta:

        $correo = new ContactanosMailable;
        Mail::to('weblikonet@gmail.com')->send($correo);
        return "mensaje enviado";    

Lo ponemos esta vez dentro del método store().

class ContactanosController extends Controller
{
    //
    public function index(){
        
    }

    public function store(){
        $correo = new ContactanosMailable;
        Mail::to('weblikonet@gmail.com')->send($correo);
        return "mensaje enviado";    
    }

}

Como estamos creando aquí el objeto y haciendo uso de la clase, debemos importar las clases en el controlador:

use App\Mail\ContactanosMailable;
use Illuminate\Support\Facades\Mail;

Modificamos la ruta para el controlador

Modificamos la ruta para que llame al método index() del controlador ContactanosController:

Route::get('contactanos', [ContactanosController::class, 'index'])->name('contactanos.index');

Vista para el formulario

Hacemos que el método index() del controlador devuelva una vista llamada «contactanos.index»:

    public function index(){
        return view('contactanos.index');
    }

Creamos una carpeta dentro de views llamada «contactanos» y, dentro, la vista index.blade.php. Su contenido puede estar copiado de la vista home, y modificamos a nuestro antojo el title y el h1:

@extends('layouts.plantilla')

@section('title', 'Contactanos')

@section('content')
    <h1>Déjanos un mensaje</h1>
@endsection

Añadimos la clase para que el enlace quede activo cuando se visite:

<a href="{{route('contactanos.index')}}" class="{{request()->routeIs('contactanos.index') ? 'active' : '' }}">Contactanos</a>

Formulario de contacto

Creamos el formulario dentro de la vista:

<form action="">
    <label>
        Nombre:
        <br>
        <input type="text" name="name">
    </label>
    <br>


    <label>
        Correo:
        <br>
        <input type="mail" name="email">
    </label>
    <br>

    <label>
        Mensaje:
        <br>
        <textarea name="mensaje" rows="4"></textarea>    
        </label>
    <br>

    <button type="submit">Enviar mensaje</button>

</form>

Ruta post para el formulario

Creamos una ruta que procese la información del formulario. Va a ser de tipo post y llevará la url «contactanos». Le asignamos el método store() del controlador ContactanosController. Le damos el nombre «contactanos.store»:

Route::post('contactanos',[ContactanosController::class, 'store'])->name('contactanos.store');

Ponemos esta ruta en el action del formulario. Tenemos que especificar también el método del formulario, y el token csrf:

(...)
<form action="{{route('contactanos.store')}}" method="post">
    @csrf
(...)

Ejercicio

Hagamos este pequeño ejercicio antes de continuar. Si añadimos una propiedad a la clase del mailable que hemos creado para enviar el correo, como esta:

(...)
class ContactanosMailable extends Mailable
{
    use Queueable, SerializesModels;

    public $subject="Asunto del mail de contacto";

    public $propiedad="Esta es una propiedad del mailable"; 
(...)

Podemos rescatar esta propiedad dentro de la vista del mail de la siguiente forma:

<!DOCTYPE html>
(...)
<body>
    <h1>Correo Electrónico</h1>
    <p>Primer mail que mando por Laravel</p>
    {{$propiedad}}
</body>
</html>

Y comprobar que se envía correctamente

Recuperar la información del formulario

Al método store del controlador le pasamos el objeto request.

    public function store(Request $request){
        $correo = new ContactanosMailable;
        Mail::to('weblikonet@gmail.com')->send($correo);
        return "mensaje enviado";    
    }

Donde estoy creando la instancia de la clase ContactanosMailable le pasamos la información de request al constructor:

    public function store(Request $request){
        $correo = new ContactanosMailable($request->all());
        Mail::to('weblikonet@gmail.com')->send($correo);
        return "mensaje enviado";    
    }

Y recibimos la información en el constructor de la clase de la siguiente forma:

public $contacto;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($contacto)
    {
        $this->contacto = $contacto;
    }

Ahora podemos modificar la vista para imprimir la información así:

(...)
<body>
    <h1>Correo Electrónico</h1>
    <p>Primer mail que mando por Laravel</p>
    {{$propiedad}}

    <p><strong>Nombre:</strong> {{$contacto['nombre']}}</p>
    <p><strong>Nombre:</strong> {{$contacto['email']}}</p>
    <p><strong>Nombre:</strong> {{$contacto['mensaje']}}</p>
</body>
</html>

Validación

Completamos el método store() para validar el formulario. Todos los campos serán requeridos y el email debe ser un email:

 public function store(Request $request){

        $request->validate([
            'name'=>'required',
            'name'=>'required|email',
            'mensaje'=>'required',
        ]);

        $correo = new ContactanosMailable($request->all());
        Mail::to('weblikonet@gmail.com')->send($correo);
        return "mensaje enviado";    
    }

En el formulario ponemos los mensajes de error en cada campo. Abrimos la directiva error con el parámetro el nombre del campo que queramos validar, y en la directiva un párrafo con el posible error almacenado en la variable $message. Lo hacemos en cada campo:

 <form action="{{route('contactanos.store')}}" method="POST">
    @csrf
    <label>
        Nombre:
        <br>
        <input type="text" name="name">
    </label>
    <br>

    @error('name')
        <p><strong>{{$message}}</strong></p>
    @enderror

    <label>
        Correo:
        <br>
        <input type="mail" name="email">
    </label>
    <br>


    @error('email')
        <p><strong>{{$message}}</strong></p>
    @enderror

    <label>
        Mensaje:
        <br>
        <textarea name="mensaje" rows="4"></textarea>    
        </label>
    <br>
    
    @error('mensaje')
        <p><strong>{{$message}}</strong></p>
    @enderror

    <button type="submit">Enviar mensaje</button>
</form>

Redirección

Con esta redirección no mostraremos el mensaje «mensaje enviado», sino que volveremos a la vista en la que estábamos mostrando un mensaje. Le mandamos a la ruta un mensaje de sesión con el método with con dos parámetros, el nombre de la variable y su contenido.

    public function store(Request $request){

        $request->validate([
            'name'=>'required',
            'email'=>'required|email',
            'mensaje'=>'required',
        ]);

        $correo = new ContactanosMailable($request->all());
        Mail::to('weblikonet@gmail.com')->send($correo);

        return redirect()->route('contactanos.index')->with('info','mensaje enviado');    
    }

Usamos un condicional en la vista para ver si mostramos o no la variable de sesión en una alerta, en la vista del formulario.

(...)
    <button type="submit">Enviar mensaje</button>

</form>

@if (session('info'))
    <script>
        alert("{{session('info')}}")
    </script>
@endif
(...)

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=Ei3RVhdcE0A&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=29

Archivos:

  • .env
  • config/mail.php
  • resources/routes/web.php

Conectar con el proveedor de correo electrónico

Laravel está configurado para conectar con ciertos proveedores que usan APIs, aunque podemos conectarnos con cualquier otro siempre que instalemos los archivos necesarios. Desde config/mail.php podemos ver toda la configuración necesaria.

La primera decisión a tomar es elegir entre API y SMTP. En el archivo mail.php está definida la variable mail_mailer y su valor por defecto es SMTP, que es el protocolo que usaremos aquí. (SMTP: Protocolo para transferencia simple de correo).

    'default' => env('MAIL_MAILER', 'smtp'

Además necesitamos las credenciales del proveedor para conectarnos por SMTP. Desde el archovo mail.php:


    'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
            'port' => env('MAIL_PORT', 587),
            'encryption' => env('MAIL_ENCRYPTION', 'tls'),
            'username' => env('MAIL_USERNAME'),
            'password' => env('MAIL_PASSWORD'),
            'timeout' => null,
            'local_domain' => env('MAIL_EHLO_DOMAIN'),
        ],

Como vemos en este fragmento, estas variables se toman del archivo .env:

MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

Mailtrap

Mailtrap es un servidor falso SMTP para pruebas. Una vez creada la cuenta en https://mailtrap.io/ (que aquí hemos creado con las credenciales de una de nuestras cuentas de Gmail) podemos encontrar los parámetros en el primer correo de prueba que encontraremos en la bandeja de entrada.

Necesitaremos el host, el puerto, el usuario, la contraseña para el archivo .env.

En mail.php vamos a encontrar los nombres de las variables para el remitente:


    'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
        'name' => env('MAIL_FROM_NAME', 'Example'),
    ],

En el archivo .env:

MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

Creación de mailable

Desde la consola:

    php artisan make:mail ContactanosMailable

Dentro de /app se creará uan carpeta /Mail con el archivo que acabamos de crear.

Agregamos propiedades a la clase que se extiende de Mailable.

 class ContactanosMailable extends Mailable
{
    use Queueable, SerializesModels;

    public $subject="Asunto del mail de contacto";

(...)

Más abajo encontramos que la función build() llama a una vista que vamos a renombrar, y luego a crearla:

    public function build()
    {
        return $this->view('emails.contactanos');
    }

En /views creamos una carpeta /emails y dentro nuestra vista llamada contactanos.blade.php. Dentro de la vista creamos el cuerpo de un archivo HTML (recordamos: en VSC basta con escribir ! y tabular). Modificamos el HTML a nuestro antojo, e incluso agregamos un estilo:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        h1 {color:blue}
    </style>
</head>
<body>
    <h1>Correo Electrónico</h1>
    <p>Primer mail que mando por Laravel</p>
</body>
</html>

Ruta para el envío del mail

Vamos al archivo de rutas, en web.php, y creamos nueva ruta tipo get:

Route::get('contactanos', function() {
    //
})

E importamos dos clases a este archivo:


use App\Mail\ContactanosMailable;
use Illuminate\Support\Facades\Mail;

Vamos a generar una instancia de ContactanosMailable y acceder a la clase mail con el método to(), usando como parámetro un mail cualquiera y pasándole una clase send con el objeto creado:

Route::get('contactanos', function() {
    $correo = new ContactanosMailable;
    Mail::to("unmaildepruebaqueexista@gmail.com")->send($correo);
    return "mensaje enviado";
})

Al ir a la url de la ruta se enviará el mail de que hemos creado. Podemos ver los mails en nuestra bandeja de entrada de mailtrap.

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=e0ynchA_sBA&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=28

Archivos:

  • resources/views/layouts/plantilla.blade.php
  • routes/web.php
  • resources/views/layouts/partials

HTML del menú de navegación

Este es el prototipo del código del menú de navegación, que escribiremos en el archivo de plantilla plantilla.blade.php:

(...)

<body>
    <!-- header -->
    <!-- nav -->
    <header>
        <nav>
            <ul>
                <li><a href="">Home</a></li>
                <li><a href="">Cursos</a></li>
                <li><a href="">Nosotros</a></li>
            </ul>
        </nav>
    </header>

(...)

Nombraremos nuestra ruta principal como «Home». En el archivo de rutas:

Route::get('/', HomeController::class)->name('Home');

Vamos a crear una ruta para «Nosotros» con el siguiente código, que utiliza un método distinto: el método view. Este método sólo lo utilizaremos para mostrar contenido estático. Admite dos parámetros, la url y el nombre de la vista. También le pondremos un nombre, la llamaremos «nosotros»:

// Route::view('url','vista')->name('nombre');
Route::view('nosotros','nosotros')->name('nosotros');

Creamos el archivo nosotros.blade.php en la carpeta vistas, le añadimos el contenido html de la vista home y la modificamos así:

@extends('layouts.plantilla')

@section('title', 'Home')

@section('content')
    <h1>Nosotros</h1>
@endsection

Ya podemos rellenar las rutas del nav de la siguiente forma:

    <header>
        <nav>
            <ul>
                <li><a href="{{route('home')}}">Home</a></li>
                <li><a href="{{route('cursos.index')}}">Cursos</a></li>
                <li><a href="{{route('nosotros')}}">Nosotros</a></li>
            </ul>
        </nav>
    </header>

Uso de la función request

Usando la función request con el método routeIs, pasando como parámetro una ruta, obtendremos true si la ruta es la actual, y false si no lo es. Podemos comprobarlo as´í:

    <header>
        <nav>
            <ul>
                <li><a href="{{route('home')}}">Home</a>
                <?php
                    dump(request()->routeIs('home'));
                ?>
</li>
                <li><a href="{{route('cursos.index')}}">Cursos</a></li>
                <li><a href="{{route('nosotros')}}">Nosotros</a></li>
            </ul>
        </nav>
    </header>

Mejor usar una directiva de blade:

@dump(request()->routeIs('home'))

Podríamos usar la misma directiva en cada ruta:

        <nav>
            <ul>
                <li><a href="{{route('home')}}">Home</a>
                    @dump(request()->routeIs('home'))
                </li>
                <li><a href="{{route('cursos.index')}}">Cursos</a>
                    @dump(request()->routeIs('cursos.index'))
                </li>
                <li><a href="{{route('nosotros')}}">Nosotros</a>
                    @dump(request()->routeIs('nosotros'))
                </li>
            </ul>
        </nav>

Añadimos una clase a cada enlace, con un condicional usando operadores ternarios. Así añadimos la clase active al enlace activo, o una clase vacía si la condición no se cumple:

            <ul>
                <li><a href="{{route('home')}}" class="{{request()->routeIs('home') ? 'active' : '' }}">Home</a>
                </li>
                <li><a href="{{route('cursos.index')}}" class="{{request()->routeIs('cursos.index') ? 'active' : '' }}">Cursos</a>
                </li>
                <li><a href="{{route('nosotros')}}" class="{{request()->routeIs('nosotros') ? 'active' : '' }}">Nosotros</a>
               </li>
            </ul>

Para que cualquier ruta que comience con cursos quede como activa (si visitamos la url de cualquier curso) usamos un asterisco:

            <ul>
                <li><a href="{{route('home')}}" class="{{request()->routeIs('home') ? 'active' : '' }}">Home</a>
                </li>
                <li><a href="{{route('cursos.index')}}" class="{{request()->routeIs('cursos.*') ? 'active' : '' }}">Cursos</a>
                </li>
                <li><a href="{{route('nosotros')}}" class="{{request()->routeIs('nosotros') ? 'active' : '' }}">Nosotros</a>
               </li>
            </ul>

Optimizamos la plantilla

Creamos una carpeta llamada partials dentro de la carpeta layouts. Dentro, un archivo de plantilla llamado header.blade.php, con el siguiente contenido (que cortamos desde la plantilla.blade.php):

<header>
        <h1>Proyecto Laravel</h1>
        <nav>
            <ul>
                <li><a href="{{route('home')}}" class="{{request()->routeIs('home') ? 'active' : '' }}">Home</a>
                    {{-- @dump(request()->routeIs('home')) --}}
                </li>
                <li><a href="{{route('cursos.index')}}" class="{{request()->routeIs('cursos.*') ? 'active' : '' }}">Cursos</a>
                    {{-- @dump(request()->routeIs('cursos.index')) --}}
                </li>
                <li><a href="{{route('nosotros')}}" class="{{request()->routeIs('nosotros') ? 'active' : '' }}">Nosotros</a>
                    {{-- @dump(request()->routeIs('nosotros')) --}}
                </li>
            </ul>
        </nav>
    </header>

Y en el lugar donde estaba todo el código del header, una directiva de blade para incluirlo:

    @include('layouts/partials/header')

Ya que hemos hecho esto, añadamos un footer. Dentro de partials, creamos footer.blade.php, lo incluimos también en la plantilla con include. El contenido del footer podría ser:

<footer>
    Esto es un footer
</footer>

Para resumir, el contenido de la plantilla.blade.php quedaría así:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title')</title>
    <!-- favicon -->
    <!-- estilos -->
    <style>
        .active {
            font-weight: bold;
            color: red
        }
    </style>

</head>
<body>
    <!-- header -->
    <!-- nav -->

    @include('layouts/partials/header')

    @yield('content')

    <!-- footer -->

    @include('layouts/partials/footer')

    <!-- script -->
</body>
</html>

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=VVY5RVdpFco&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=27

Archivos:

  • database/migrations
  • database/factories

Crear el campo slug

Genramos un nuevo campo dentro de la migración de la tabla cursos, llamado «slug»

    public function up()
    {
        Schema::create('cursos', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug');
            $table->text('description');
            $table->text('categoria');
            $table->timestamps();
        });
    }

Editamos el factory de esta tabla:

    public function definition()
    {
        return [
            //
            'name'=>$this->faker->sentence(),
            'slug'=>Str::slug($this->faker->sentence(),'-'),
            'categoria'=>$this->faker->randomElement(['Desarrollo web','Diseño web']),
            'description'=>$this->faker->paragraph()
        ];

Hemos llamado a la clase Str y a su método slug. Cambiará todas las mayúsculas por minúsculas y sustituirá los espacios por guiones (porque hemos establecido un guión como segundo parámetro).

En este caso tendremos un slug distinto al título, y queremos que sea el mismo, con lo que lo podemos poner así:

public function definition()
    {
        $name =$this->faker->sentence();

        return [
            //
            'name'=>$name,
            'slug'=>Str::slug($name,'-'),
            'categoria'=>$this->faker->randomElement(['Desarrollo web','Diseño web']),
            'description'=>$this->faker->paragraph()
        ];
    }

No podemos olvidar llamar a la clase Str al principio del archivo, si no lo ha autorrellenado Intelephense:

use Illuminate\Support\Str;

Luego migramos para ver los cambios en la base de datos:

php artisan migrate:fresh --seed

Uso del campo slug

Abrimos la vista index de los cursos, comprobemos la url actual:

@foreach ($cursos as $curso)

        <li>
         <a href="{{route('cursos.show', $curso->id)}}">{{$curso->name}}</a> 
         </li>
          {{route('cursos.show', $curso->id))}}

        @endforeach

Recordamos que en Laravel podemos llamar a la id directamente de la siguiente manera, ya que el recurso de llamar a la id es muy utilizado y se puede hacer directamente así:

@foreach ($cursos as $curso)

        <li>
         <a href="{{route('cursos.show', $curso)}}">{{$curso->name}}</a> 
         </li>
          {{route('cursos.show', $curso))}}

        @endforeach

LA id se la pasamos al controlador mediante la variable curso. Necesitaríamos que cuando utilicemos el método route y le pasemos el objeto curso, en vez de la id, recupere el slug del objeto. Devolverá el registro cuyo slug coincida con lo que le pasemos por la url.

Vamos al modelo Curso, que vemos que se extiende de la clase Model.

Vemos que uno de los muchos métodos que tiene es getroutekeyname:

     public function getRouteKeyName()
    {
        return $this->getKeyName();
    }

Podríamos modificar este mnétodo tal que así, para lograr lo que queremos:

     public function getRouteKeyName()
    {
       // return $this->getKeyName();
 return 'slug';
    }

Pero lo mejor es hacerlo en el modelo Curso:

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=VYuthGIhrS0&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=25

Archivos:

  • routes/web.php

Uso de route resource

Desde la consola de Git Bash, navegamos hasta el directorio de nuestro proyecto y ejecutamos el siguiente comando de artisan: php artisan «route list»

php artisan r:l

Comentamos todas las rutas y vamos a definir la rutas del CRUD de la siguiente forma:

 Route::resource('cursos', CursoController::class);

Si volvemos a ejecutar el comando route list, veremos todas las 7 rutas del CRUD de nuevo en la consola.

El resultado es exactamente el mismo porque anteriormente creamos las rutas una a una siguiendo las convenciones de nombres.

Cambio de rutas creadas con route resource

En la documentación de Laravel se nos indica cómo hacerlo: https://www.oulub.com/docs/laravel/controllers#restful-localizing-resource-uris

Según se nos indica allí, vamos al archivo App/providers/appserviceprovider.php y poner en la parte superior del archivo:

use Illuminate\Support\Facades\Route;

Y dentro del método boot:

    public function boot()
    {
        Route::resourceVerbs([
            'create' => 'crear',
            'edit' => 'editar',
        ]);
    }

Y podemos hacer esto porque una cosa es la url, y otra el nombre de la ruta. Lo que estamos cambiando es la url, mientras que los formularios y lois enlaces siguen apuntando a las rutas por su nombre.

De hecho, podríamos modificar la palabra «cursos» del route resource por cualquier otra, por ejemplo, «asignaturas»; y todo seguiría funcionando como hasta ahora, pero tendríamos que usar un par de métodos más para que no falle todo:

Route::resource('asignaturas', CursoController::class)->parameters(['asignatura' => 'curso'])->names('cursos');

El método names permitirá que las rutas sigan funcionando como hasta ahora (pues están en todas las vistas, y si no, tendríamos que modificar cada vista) y el método parameters incluye un array para los nombres de las variables que usamos para generar las rutas (que, de nuevo, necesitarían ser cambiadas por «asignatura»).

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=PT6BoDQdkXk&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=24

Archivos:

  • routes/web.php
  • CursoController.php

Crearemos una ruta de tipo delete, que apunte a una url llamada curso.destroy.

Route::delete('cursos/{curso}',[CursoController::class, 'destroy'])->name('cursos.destroy');

Crearemos un método destroy() en nuestro controlador. Como se está mandando información por la url, lo rescatamos en el controlador.

    public function destroy(Curso $curso) {
        $curso->delete();
    }    

Añadiremos un botón eliminar en la vista show dentro de un formulario. El formulario tendrá la directiva method con el método del mismo (que no es post, sino delete) y el token de seguridad, además de que en el action pasaremos la nueva ruta creada con el parámetro curso. En la vista show:

<form action="{{route('cursos.destroy',$curso)}}" method="POST">
        @csrf
        @method('delete')
        <button type="submit">Eliminar</button>
    </form>

Tenemos que pedirle al método destroy que nos redirija para que el formulario no muestre una página en blanco,

    public function destroy(Curso $curso) {
        $curso->delete();
        return redirect()->route('cursos.index');
    }    

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=3uUW1IviXns&t=3s

Archivos:

  • resources/views/cursos/create.blade.php

Actualizando método store

Si un formulario tuviese muchísimos campos, resultaría tedioso asignar cada campo a un valor dentro del método del controlador. Para ello podemos asignar todos los campos con una única línea de código, aunque para ello tendremos que tomar ciertas medidas de seguridad.

En el método store, creábamos un objeto Curso y asignábamos los valores provenientes del formulario con las siguientes líneas, y lo salvábamos en la base de datos:

$curso = new Curso();

$curso ->name= $request->name;
$curso ->description= $request->description;
$curso ->categoria= $request->categoria;
$curso ->save();

Ahora usaremos un método llamado create() al que pasaremos como parámetro un array:

        $curso = Curso::create([
            'name' => $request->name,
            'description' => $request->description,
            'categoria' => $request->categoria
        ]);

Lo cual se puede escribir mucho más sencillo así:

        $curso = Curso::create($request->all());

Pero si intentamos ahora usar el formulario obtendremos el siguiente error:

Add [_token] to fillable property to allow mass assignment on [App\Models\Curso].

Así que esto nos dice que, por seguridad, tendremos que especificar qué campos se pueden rellenar y qué no en nuestra base de datos – ya que estamos mandando todos los campos al mismo tiempo, sin importar cuántos ni cuáles. Esto lo podemos hacer de dos formas: usando la propiedad fillable o la propiedad guarded en el módulo Curso.

Propiedad fillable

Así especificamos qué campos queremos que se puedan rellenar:

class Curso extends Model
{
    use HasFactory;
    protected $fillable = ['name','description','categoria'];
}

(Ponemos campos permitidos e ignoramos lo campos protegidos)

Propiedad guarded

Así especificamos qué campos no queremos que se puedan rellenar, lo cual es bastante útil ahora mismo para ahorrar código:

class Curso extends Model
{
    use HasFactory;
    protected $guarded = ['status'];
}

Esto es útil si, por ejemplo, tuviéramos un campo llamado status cuya función sería que el registro se mostrara o no, y estuviera pendiente de ser aprobado o no mientras este campo tuviera cierto valor o se modificara. En nuestro caso, aún no tenemos establecido nada de esto y podemos dejarlo así:

class Curso extends Model
{
    use HasFactory;
    protected $guarded = [];
}

(Ponemos campos protegidos e ignoramos lo campos permitidos)

Actualizando método update

Vamos al método update, para editar registros, y lo modificamos de la siguiente manera: comentamos todas las líneas de creación de objeto y asignación, y le pasamos el método update al objeto $curso que hemos creado, pasándole como parámetro el array de request:

public function update(Request $request, Curso $curso) {
        // return $request->all();

        $request->validate([
            'name' => 'required',
            'description' => 'required',
            'categoria' => 'required',
        ]);

        // $curso->name = $request->name;
        // $curso->description = $request->description;
        // $curso->categoria = $request->categoria;

        // $curso->save();

        $curso->update($request->all());

        return redirect()->route('cursos.show',$curso->id);

    }

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=n_C_HFqS8XY&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=22

Archivos:

  • resources/views/cursos/create.blade.php
  • appHttpControllersCursoController.php
  • lang/
  • config/app.php
  • app/Http/Requests

Validar campos requeridos

Campos no vacíos

Cuando un campo de la base de datos no tenga la propiedad nullable, no deberíamos intentar guardar campos vacíos sin mostrar un error. Para eso tenemos que validar los campos del formulario.

En la función store, usaremos un método del objeto request llamado validate. Dentro del método tendremos un array con las reglas de validación:

        $request->validate([

        ]);

Así especificamos que queremos que los campos sean requeridos:

        $request->validate([
            'name' => 'required',
            'description' => 'required',
            'categoria' => 'required',
        ]);

Mostrar errores

Debajo de cada campo, mostraremos un mensaje de error, usando una directiva de blade:

        @error('record')
            *{{$message}}
        @enderror

El error lo maquetaremos de la siguiente manera:

(...)
<label>
 Nombre:<br>
 <input type="text" name="name">
        </label>      
         @error('name')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror
<br>

Hacemos lo mismo con todos los campos:

<br>    <form action="{{route('cursos.store')}}" method="POST">

        @csrf

        <label>
            Nombre:<br>
            <input type="text" name="name">
        </label>

        @error('name')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror
        <br>

        <label>
            Descripción:<br>
            <textarea name="description" rows="5"></textarea>
        </label>
        @error('description')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror
        <br>

        <label>
            Categoría:<br>
            <input type="text" name="categoria">
        </label>
           @error('categoria')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror 
        <br>
        <button type="submit">Enviar formulario</button>
    
    </form>

Traducción de los mensajes

Vamos a la carpeta lang, donde podemos ver todos los mensajes en inglés de la paginación, la valicación…. Duplicamos la carpeta y le cambiamos el nombre a es.

Los mensajes ya están traducidos por la comunidad de Laravel, los podemos descargar de aquí: https://github.com/Laraveles/spanish. Reemplazamos el contenido de nuestros archivos con el contenido de los de spanish/resources/lang/es/.

Nos dirigimos al archivo config/app.php y editamos el valor «locale»:

    'locale' => 'es',

Más personalización

Dentro del archivo de traducción español validation.php encontraremos este array:

    'attributes' => [],

El campo nombre de nuestro formulario se llama name, con lo que podemos hacer lo siguiente:

        'attributes' => [
        'name' => 'nombre'
    ],

Asi nuestro mensaje de validación no será:

*El campo name es obligatorio.

Sino así:

*El campo nombre es obligatorio.

Dejaremos el array de la siguiente forma:

    'attributes' => [
        'name' => 'nombre del curso',
        'description' => 'descripción'
    ],

Mantener los valores de los campos aunque haya un error

Si el usuario olvida un campo pero rellena otros, el valor de los campos válidos se mantendrá cuando se recargue el formulario.

Usamos el método old en tags value de los input del formulario, y dentro del textarea:

<form action="{{route('cursos.store')}}" method="POST">

        @csrf

        <label>
            Nombre:<br>
            <input type="text" name="name" value="{{old('name')}}">
        </label>

        @error('name')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror
        <br>

        <label>
            Descripción:<br>
            <textarea name="description" rows="5">{{old('description')}}</textarea>
        </label>
        @error('description')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror
        <br>

        <label>
            Categoría:<br>
            <input type="text" name="categoria" value="{{old('categoria')}}">
        </label>
           @error('categoria')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror 
        <br>
        <button type="submit">Enviar formulario</button>
    
    </form>

En el proyecto tenemos otro formulario, el de edición. Antes de continuar con el siguiente paso de la validación, haremos los cambios convenientes al formulario de edición: añadimos la regla de validación al método update y los mensajes de error a cada uno de los labels.

Un caso específico de los formularios de edición

Comentemos un caso concreto: Si en el formulario de editar curso hago ciertos cambios en un campo, pero dejo otro vacío, al recargar el formulario mostrará el mensaje de error, pero no hará caso de los recientes cambios, sino que mostrará el valor anterior ya almacenado en la base de datos.

Para ello, el método old que antes utilizamos podemos volver a usarlo con un segundo parámetro, el valor por defecto del campo:

(...)
    <h1>Edición de curso</h1>
    <form action="{{route('cursos.update',$curso)}}" method="POST">

        @csrf

        @method('put')

        <label>
            Nombre:<br>
            <input type="text" name="name" value="{{old('name',$curso->name)}}">
        </label>

        @error('name')
        <br>
            <small>*{{$message}}</small>
        <br>
    @enderror 
        <br>

        <label>
            Descripción:<br>
            <textarea name="description" rows="5">{{old('description',$curso->description)}}</textarea>
        </label>

        @error('description')
        <br>
            <small>*{{$message}}</small>
        <br>
    @enderror 
        <br>

        <label>
            Categoría:<br>
            <input type="text" name="categoria" value="{{old('categoria',$curso->categoria)}}">
        </label>
        @error('categoria')
            <br>
                <small>*{{$message}}</small>
            <br>
        @enderror 

        <br>
        <button type="submit">Actualizar curso</button>
    
    </form>

Es decir, el campo por defecto seguirá siendo el mismo, pero rescatará el valor más reciente antes de actualizar el formulario gracias al método old.

Más de una regla de validación

Si quisiéramos que el campo nombre, además de que no estuviera vacío, tuviera un máximo de 10 caracteres y la descripción un mínimo de 5, usaremos una segunda regla de validación con el símbolo |. En la función store:

        $request->validate([
            'name' => 'required|max:10',
            'description' => 'required|min:5',
            'categoria' => 'required',
        ]);

Hemos hecho la validación en el controlador. En ocasiones, las reglas de validación son muchas, y en este caso es recomendable hacerlo en un archivo a parte. De hecho, es lo recomendado en cualquier caso.


Archivo de validación

Enlace a la documentación de Laravel: https://laravel.com/docs/9.x/validation#form-request-validation

Usaremos un comando de artisan para crear un archivo de validación del formulario para guardar registros:

php artisan make:request StoreCurso

Se ha creado un archivo php en la ruta:Http/Requests/ . En el mismo hay dos métodos: auth() sirve para establecer las reglas de los permisos de usuario, el cual debemos hacer que de momento devuelva un valor true. En el método rules() allí ponemos nuestras reglas de validación, que antes estaban en el método store:

    public function authorize()
    {
        return true;
    }
    
(...)

public function rules()
    {
        return [
            'name' => 'required|max:10',
            'description' => 'required|min:5',
            'categoria' => 'required',
        ];
    }

En la parte superior de controlador añadimos la línea:

   use App\Http\Requests\StoreCurso;

Borramos donde antes teníamos las reglas de validación (el método store) y añadimos la nueva clase como parámetro de la función:

    public function store(StoreCurso $request){
(...)

Los campos ya deberían validarse como anteriormente.

Función attributes

Bajo el método rules() tenemos la posibilidad de usar este otro método para personalizar el nombre de los atributos del formulario en el mensaje de error, como ya hicimos en el archivo de traducción:

   public function attributes()
    {
        return [
        'name' => 'del nombre del curso'
        ];
    }

Función message

   public function attributes()
    {
        return [
        'name' => 'del nombre del curso'
        ];
    }

Para personalizar el mensaje de error completo del campo description, para la regla de validación required:

    public function messages()
    {
        return [
            'description.require' => 'Es obligatoria una descripción.'
        ];
    }

Vemos que como el campo tiene dos reglas de validación, necesitamos especificar para qué regla vamos a mostrar este mensaje personalizado.

Recursos:

Enlace de Youtube: https://www.youtube.com/watch?v=KbpbqZshUus&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=20, https://www.youtube.com/watch?v=Ze-Sg2BT3mc&list=PLZ2ovOgdI-kWWS9aq8mfUDkJRfYib-SvF&index=21