Cómo crear una aplicación de mensajería con Laravel y Vue

LaravelVue

En este tutorial voy a explicar cómo crear una aplicación de mensajería con Laravel y Vue. El objetivo del tutorial consiste en comprender y aprender a utilizar componentes de Vue en el contexto de Laravel. Por este motivo, no usaremos Inertia, aunque a priori pueda parecer la opción más sencilla.

Además, también aprenderás a crear migraciones, factorías, seeders y tests, entre otras cosas. Es un tutorial largo, quizás demasiado largo, pero si estás comenzando con Laravel o si ya sabiendo usar este framework quieres crear una aplicación completa, seguramente te interese, especialmente ahora que el mercado laboral es más competitivo que hace un par de años.

Tienes disponible el repositorio con el código del proyecto en el siguiente enlace:

Introducción

Para seguir tutorial debes tener tanto PHP como composer instalados en tu sistema, así como MySQL. Si no es el caso, consulta el tutorial de instalación de PHP en Windows, en donde verás cómo instalar PHP en tu sistema. También deberías tener Node.js instalado en tu sistema; si no lo tienes, consulta la guía de instalación de Node.

Además de los requisitos anteriores, debes tener ciertos conocimientos de HTML, CSS y PHP. Si no los tienes, es recomendable que consultes las siguientes guías antes de continuar:

Además, tendrás que usar la terminal de comandos. Veremos y explicaremos todos los comandos utilizados. Sin embargo, si no estás acostumbrado a usar la línea de comandos, es recomendable que consultes la siguiente guía antes de continuar:

Y con esto, ya estarías listo para seguir adelante con el tutorial. Comenzaremos por la instalación de Laravel. ¡Vamos allá!

Instalación de Laravel

Vamos a comenzar instalando y configurando Laravel desde cero. Para crear un proyecto con Laravel, sigue estos pasos:

  1. Abre una ventana de línea de comandos y dirígete al directorio en donde quieres instalar Laravel usando el comando cd.
  2. Ahora debes usar el comando create-project de composer para crear el proyecto. Debes indicar que quieres crear un proyecto con Laravel, además de indicar  el nombre del proyecto, que en nuestro ejemplo será composer create-project laravel/laravel tutorial-mensajeria, o algo más corto, según prefieras.
    composer create-project laravel/laravel tutorial-mensajeria
  3. Seguidamente, accede al proyecto usando el comando cd:
    cd tutorial-mensajeria

Con esto ya habremos instalado Laravel. Ahora vamos a instalar y configurar Laravel Breeze como punto de partida de la aplicación. Para ello sigue estos pasos:

  1. Ejecuta este comando en el directorio raíz del proyecto para instalar Laravel Breeze:
    php artisan breeze:install
  2. Mientras instalas Breeze, se mostrarán una serie de opciones. Entre ellas verás verás la opción de instalar Vue con Inertia. Sin embargo, escogeremos la opción de usar Blade a secas.
  3. Seguidamente selecciona que «no» cuando se te pregunte si quieres instalar el modo noche.
  4. Luego selecciona la opción PHPUnit cuando el instalador te pregunte si quieres usar PHPUnit o Pest como framework para los tests. Si quieres puedes seleccionar Pest, que es un framework construído sobre PHPUnit, aunque en este tutorial seguiremos la vía clásica de PHPUnit.

El instalador de Breece creará las vistas básicas, migraciones de la base de datos y demás componentes necesario para registrarse y acceder a la aplicación.

Ahora vamos a crear la base de datos y a ejecutar las migraciones. Para ello, puedes crearla mediante la línea de comandos o PhpMyAdmin. También puedes usar herramientas como HeidiSQL. En mi caso, le he dado el nombre de tutorial_mensajeria a la base de datos. Una vez creada, edita el archivo .env de Laravel y agrega la configuración de tu base de datos:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=tutorial_mensajeria
DB_USERNAME=root
DB_PASSWORD=

*Si el archivo .env no existe, crealo.

Luego edita el archivo /config/database.php de Laravel y accede a la sección «connections», en donde se listan las diferentes conexiones a la base de datos. Modifica el parámetro engine de la conexión mysql y dale le valor de InnoDB:

'connections' => [
  ...
  'mysql' => [
    ...
    'engine' => 'InnoDB',
    ...
  ],
...
];

Luego accede de nuevo a la línea de comandos y ejecuta el siguiente comando para ejecutar las migraciones:

php artisan migrate

Si has creado ya la base de datos, las migraciones se ejecutarán directamente. De lo contrario, Laravel te preguntará si deseas crearla. Debes seleccionar que sí, de modo que la base de datos se cree antes de ejecutarse las migraciones.

Ahora ya puedes iniciar el servidor de desarrollo mediante el siguiente comando:

php artisan serve

Luego compila el código CSS y JS que se incluye por defecto mediante este comando:

npm run dev

Si ahora accedes a la URL http://127.0.0.1:8000/ desde tu navegador, deberías ver la página de inicio de Laravel:

Instalación de Laravel Sanctum

Dado que no vamos a usar Inertia, los componentes de Vue deben comunicarse con el servidor de algún modo. Podríamos usar peticiones AJAX o también peticiones API. En este caso realizaremos peticiones API usando Laravel Sanctum, que nos dotará de un mecanismo de autenticación y acceso mediante API. Podríamos usar Laravel Passport, aunque en este caso no necesitamos dicha complejidad extra.

Para instalar Sanctum ejecuta este comando desde la terminal de comandos:

composer require laravel/sanctum

Una vez finalizada la instalación, ejecuta este comando para publicar la configuración de Sanctum en nuestra aplicación:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

Finalmente, ejecuta este comando para ejecutar las migraciones, ya que es posible que Sanctum cree nuevas tablas en la base de datos:

php artisan migrate

Para autenticarnos mediante Laravel Sanctum vía API, tendríamos que enviar una petición para obtener un token, que se almacenaría en una cookie. Sin embargo, por simplicidad, vamos a mantener el sistema de login de Laravel. Sanctum permite usar este sistema de autenticación sin requerir peticiones extra. Para ello, debes editar el archivo app/Http/Kernel.php y eliminar el comentario del middleware EnsureFrontendRequestsAreStateful, que por defecto estará comentado:

protected $middlewareGroups = [
  'web' => [
    // ...
  ],
  'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    // ...
  ],
];

Con esto ya habremos configurado Sanctum.

Puesta a punto de la base de datos

Hemos creado una base de datos, pero en ella solamente tenemos las tablas que Laravel crea por defecto. La más relevante es la tabla users, en donde se almacenan los usuarios de la aplicación. Por ahora esta tabla está vacía. Además, nos hará falta otra tabla que contenga los mensajes, que todavía no hemos creado. En este apartado crearemos las migraciones de la base de datos, las factorías y los seeders.

Crea las migraciones

Vamos a crear la migración de la tabla de mensajes, que nos permitirá crear la tabla de mensajes. Para ello vamos  a crear una nueva migración mediante el siguiente comando:

php artisan make:migration create_messages_table

Esto creará el archivo xxxx_xx_xx_xxxxxx_create_messages_table en el directorio /databases/migrations de Laravel. Las X contendrán las fecha de creación del archivo, usada internamente por el framework.

Edita el archivo de modo que tenga este contenido:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
  /**
   * Ejecuta las migraciones.
   */
  public function up(): void
  {
    Schema::create('mensajes', function (Blueprint $table) {
      $table->id();
      $table->integer('user_id')->unsigned()->references('id')->on('users'); // Autor
      $table->integer('from')->unsigned()->references('id')->on('users'); // Emisor del mensaje
      $table->integer('to')->unsigned()->references('id')->on('users'); // Receptor del mensaje
      $table->string('subject')->default("(no subject)"); // Asunto
      $table->text('content'); // Contenido
      $table->dateTime('read_at')->nullable(); // Fecha de lectura
      $table->softDeletes();  // Fecha de eliminación
      $table->timestamps(); // Fechas de creación/actualización
    });
  }

  /**
   * Revertir las migraciones.
   */
  public function down(): void
  {
    Schema::dropIfExists('mensajes');
  }
};

En nuestra tabla de mensajes necesitamos el identificador user_id del usuario al que pertenece el mensaje, que será una clave foránea de la tabla users. También el identificador from del usuario que envía el mensaje y el del usuario lo que lo recibe, que son también claves foráneas de la tabla users.

Seguramente estés pensando que estos identificadores son redundantes. Sin embargo, ten en cuenta que un usuario podría eliminar un mensaje, mientras que otro quizás desea conservarlo. Por ello, he decidido duplicar los mensajes con una instancia para cada usuario.

Además, también hemos agregado el asunto subject, el contenido content de tipo text, la fecha de lectura read_at y finalmente la fecha de eliminación mediante la función softDeletes de Laravel, que nos permitirá enviar mensajes a la papelera mediante el campo deleted_at. Finalmente hemos usado le método timestamps, que agregará las fechas de creación created_at y actualización updated_at de los mensajes.

Ahora vamos a usar de nuevo el comando migrate para ejecutar esta nueva migración:

php artisan migrate

Ya tenemos la estructura de la base de datos, aunque por ahora está vacía. Esto es lo que solucionaremos en los próximos apartados.

Crea los modelos y relaciones

Laravel ya crea por nosotros el modelo User. Sin embargo, tenemos que crear un modelo para nuestra tabla messages. Para ello, usa el siguiente comando:

php artisan make:model Message

Esto creará el archivo app/Models/Message.php. Nos gustaría poder enviar mensajes a la papelera, así que por ello vamos a usar el trait SoftDeletes, así que edita el modelo y agrégalo:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;

use App\Models\User;

class Message extends Model
{
  use HasFactory, SoftDeletes;

  protected $guarded = [];
}

Ahora vamos a agregar las relaciones user, fromUser y toUser:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

use App\Models\User;

class Message extends Model
{
  use HasFactory;

  protected $guarded = [];
  public function user()
  {
    return $this->belongsTo(User::class);
  }

  public function fromUser()
  {
    return $this->belongsTo(User::class, 'from');
  }

  public function toUser()
  {
    return $this->belongsTo(User::class, 'to');
  }
}

El método belongsTo se usa para crear relaciones de pertenencia. El primer parámetro es el modelo al que pertenece. En este caso, un mensaje pertenece a un usuario. Del mismo modo, creamos el mismo tipo de relación para obtener el usuario que envía el mensaje y aquel al que es enviado. El segundo parámetro es el campo de la tabla messages que contiene el identificador del usuario.

Crea las factorías

Las factorías nos permiten crear datos de muestra cuando necesitamos testear la aplicación. De este modo no necesitamos especificar manualmente cada valor.

Las factorías de Laravel se encuentran en el directorio database/factories/. Por defecto ya se incluye la factoría UserFactory, así que solamente nos faltaría crear la factoría para los mensajes, a la que daremos el nombre de MessageFactory.

Para crear la factoría MessageFactory, usa el comando make:factory:

php artisan make:factory MessageFactory

El contenido de esta factoría debería ser el siguiente:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Message>
 */
class MessageFactory extends Factory
{
  /**
   * Define the model's default state.
   *
   * @return array<string, mixed>
   */
  public function definition(): array
  {
    do {
      $userId = rand(1, 10);
      $to = rand(1, 10);
    } while($userId === $to);
    
    return [
      'user_id' => $userId,
      'from' => $userId,
      'to' => $to,
      'subject' => fake()->sentence,
      'content' => fake()->paragraph,
      'read_at' => fake()->dateTimeBetween('-15 days', 'this week'),
      'created_at' => fake()->dateTimeBetween('-30 days', 'this week')
    ];
  }
}

Lo que hemos hecho ha sido seleccionar un usuario aleatorio, suponiendo que tendremos al menos 10 usuarios en la base de datos. Este usuario será tanto el autor del mensaje como el que lo envía, almacenando este identificador en los campos user_id y from. Luego seleccionamos otro usuarios aleatorio que no sea el mismo que envía el mensaje. Este usuario será el destinatario, almacenado en el campo to.

Luego hemos escogido datos aleatorios para el asunto, el contenido, la fecha de lectura y la de creación.

Crea los seeders

Los seeders se encargan de agregar datos a la base de datos. usan las factorías que hemos creado en el apartado anterior. Los seeders se almacenen en el directorio database/seeders/.

Para crear un seeder, debes usar el comando make:seeder. A continuación vamos a crear un seeder para los mensajes:

php artisan make:seeder MessageSeeder

Si ahora abres el archivo MessageSeeder.php recién creado, verás hay un método llamado run. Aquí es en donde va nuestro código:

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

use Carbon\Carbon;
use App\Models\Message;
use App\Models\User;

class MessageSeeder extends Seeder
{
  use WithoutModelEvents;

  /**
   * Run the database seeds.
   */
  public function run(): void
  {

    $users = User::all();
    foreach ($users as $user) {
        
      Message::factory()->count(10)->create([
        'user_id' => $user->id,
        'from' => $user->id
      ])->each(function ($message) {
        // Crea el mensaje para el receptor
        Message::factory()->count(1)->create([
          'user_id' => $message->user_id,
          'from' => $message->to,
          'to' => $message->from,
          'subject' => $message->subject,
          'content' => $message->content,
          'created_at' => $message->created_at,
        ]);
      });

      Message::factory()->count(3)->create([
        'user_id' => $user->id,
        'from' => $user->id,
        'deleted_at' => Carbon::now()
      ])->each(function ($message) {

        // Crea el mensaje para el receptor
        Message::factory()->count(1)->create([
          'user_id' => $message->user_id,
          'from' => $message->to,
          'to' => $message->from,
          'subject' => $message->subject,
          'content' => $message->content,
          'created_at' => $message->created_at,
          'deleted_at' => Carbon::now()
        ]);
      });
    }
  }
}

Lo que hemos hecho ha sido recorrer todos los usuarios de la base de datos, usando la factoría del modelo Message con cada uno de ellos. Invocamos a las factorías mediante el método factory, que está disponible para todos los modelos que usen el trait HasFactory.

Mediante el método count indicamos el número de mensajes a crear y mediante el método create, pasamos los argumentos de la instancia a crear. Seguidamente, usamos el método each tras crear los modelos. Este método nos permite recorrer todos los modelos que se han creado. Lo que hacemos es usar la factoría de nuevo para crear los mismos mensajes, aunque esta vez asociados al receptor del mensajes.

Seguidamente, repetimos el proceso con una excepción, y es que el campo deleted_at tendrá un valor. Este valor será la fecha de eliminación de los mensajes. Se trata de un sencillo modo de crear mensajes que estén en la papelera. Hemos indicado que queremos crear tres mensajes eliminados para cada usuario.

Ahora edita el DatabaseSeeder que se incluye por defecto. Aquí debes ejecutar el seeder que cree los usuarios, que no es necesario crear. También debes importar y ejecutar el seeder que crea los mensajes:

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

use App\Models\User;
use Database\Seeders\MessageSeeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        User::factory(10)->create();
        $this->call(MessageSeeder::class);
    }
}

Finalmente ha llegado la hora de la verdad, que es el momento de ejecutar los seeders mediante el siguiente comando:

php artisan db:seed

Si todo va bien, tras ejecutarse los seeders podrás encontrar varios usuarios en tu base de datos y también varios mensajes asociados a cada uno:

Prueba la aplicación en el navegador

Ya tenemos una aplicación a la que es posible acceder, aunque por ahora solo tengamos las vistas por defecto de Laravel. Para ello bata con que accedas a la URL http://localhost:8000/. Desde aquí haz clic en Log in y luego introduce cualquier email de los que encontrarás en tabla users de la base de datos. Como contraseña, introduce password.

Desde la aplicación podrás gestionar tus datos, entre otras cosas. Esto es lo que deberías poder ver tras acceder a la misma:

*Sí, he usado una imagen de 8 bits porque… porque es ligera 🤔

Todas estas vistas usan componentes de Blade. La plantilla o layout que se usa es la que encontrarás en el directorio resources/views/layouts/app.blade.php. Lo que necesitamos, es crear una mini aplicación de mensajería partiendo de lo que tenemos, de modo que las secciones que creemos usen únicamente componentes de Vue.

Para lograr nuestro objetivo, primero necesitamos configurar Vue y, seguidamente, crear una plantilla en la que inyectar Vue. A partir de ahí, ya podremos comenzar a usar componentes.

Así que ahora, amigos y amigas, señoras y señores, ha llegado el momento de meternos en el meollo del tutorial, que es la creación de la lógica de la aplicación y de su interfaz. Os dejo esto para amenizar el tutorial:

Configuración básica de la aplicación

En este apartado vamos a configurar Vue, además de las rutas y plantillas que usaremos con esta librería en Laravel.

Instalación y configuración de Vue

En este apartado vamos a ver cómo configurar Vue, cómo importarlo en el archivo app.js y cómo usarlo en una plantilla.

Para instalar Vue, debes usar este comando:

npm install vue --save-dev

Vamos a instalar también el enrutador de Vue, que nos permitirá gestionar enlaces:

npm install vue-router

Dado que Laravel usa Vite en sus última versiones, también tendrás que usar el plugin de Vue para Vite:

npm install --save-dev @vitejs/plugin-vue

Ahora debes configurar Vite, así que edita el archivo vite.config.js en el directorio raíz del proyecto. Tienes que importar el plugin instalado y configurarlo tal que así:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
    resolve: {
        alias: {
            vue: "vue/dist/vue.esm-bundler.js",
        },
    },
});

La parte más problemática suele ser el alias que hemos agregado. Lo que hemos hecho es que, cada vez que se importe vue, en lugar de importar el archivo sin complicar, se importará el archivo vue/dist/vue.esm-bundler.js. De lo contrario, todo lo que importes en el interior del div en el que montes Vue, al que habitualmente nos referimos mediante el identificador app, se vaciará. De este modo, podremos trabajar con componentes de Vue y de Blade al mismo tiempo.

Todavía tenemos que informar a Tailwind de que también podemos usar sus clases CSS en archivos .vue. Por ello, debes editar el archivo tailwind.config.js y agregar los archivos con esta extensión del directorio resources:

export default {
  // ...
  content: [
    // ...
    './resources/**/*.vue',
  ],
  // ...
};

Ahora edita el archivo resources/js/app.js. En la parte superior, debes importar el componente createApp de Vue, así como los componentes createRouter y createWebHistory del enrutador:

import { createApp }  from 'vue';
import { createRouter, createWebHistory } from 'vue-router'

Luego crea el router usando la función createRouter. Por ahora, el array con las rutas estará vacío:

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // Rutas
  ],
});

Luego, crearemos la aplicación Vue mediante la función createApp, a la que le pasamos el router. Luego montamos la aplicación en un div. Este div, que todavía no hemos creado, tendrá el identificador app:

if (document.getElementById('app')) {
    createApp().use(router).mount('#app');
}

Tal y como ves, solo creamos la aplicación y montamos el componente cuando el div app esté presente, ya que en algunas páginas, como la de edición de perfil, usamos componentes de Blade. El archivo debería quedar así:

import './bootstrap';
import Alpine from 'alpinejs';
import { createApp }  from 'vue';
import { createRouter, createWebHistory } from 'vue-router'

window.Alpine = Alpine;
Alpine.start();
const router = createRouter({
    history: createWebHistory(),
    routes: [
      // Rutas
    ],
});
    

if (document.getElementById('app')) {
    createApp().use(router).mount('#app');
}

Creación de la plantilla o layout

Laravel usa por defecto la plantilla resources/views/layouts/app.blade.php. Vamos a crear otra que use Vue. Para ello crea el archivo VueLayout.php en el directorio app/View/Components/. En su interior, sencillamente vamos a renderizar una nueva plantilla:

<?php

namespace App\View\Components;

use Illuminate\View\Component;
use Illuminate\View\View;

class VueLayout extends Component
{
    /**
     * Renderizamos la vista
     */
    public function render(): View
    {
        return view('layouts.vue');
    }
}

Luego crea el archivo resources/views/layouts/vue.blade.php, copia el contenido de la plantilla app.blade.php y pégalo en el interior del archivo vue.blade.php. Ahora modifícalo tal que así:

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

    <!-- Scripts -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
  </head>
  <body class="font-sans antialiased">
    <div class="min-h-screen bg-gray-100 flex flex-col">
      @include('layouts.navigation')

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

      <!-- Contenido-->
      <main id="app" class="flex grow w-full ">
        <aside id="sidebar" class="bg-gray-700 overflow-auto min-w-[250px]  table-cell">
          <div class="items-center text-center">
            <a href="{{ url('/') }}" class="text-lg font-semibold text-gray-100 no-underline p-2 my-3 block">
              {{ config('app.name', 'Mensajería') }}
            </a>                   
          </div>
          <ul class="list-none text-lg text-white">
            // Aquí va el menú
          </ul>
        </aside>
        <div class="items-center w-full  table-cell p-4">
          {{ $slot }}
        </div>
      </main>
    </div>
  </body>
</html>

Lo que hemos hecho ha sido agregar la etiqueta con identificador app, que es en la que montamos Vue. También he agregado un menú o sidebar lateral que más adelante contendrá los enlaces que usaremos en la aplicación.

Más adelante agregaremos enlaces aquí, así que agrega estos estilos para los enlaces al final del archivo resources/css/app.css:

#sidebar ul li a {
  padding: 1rem;
  font-size: 1.1em;
  display: block;  
  color: #fff;
}

#sidebar ul li a:hover {
  cursor:pointer;
  color: #7386D5;
  background: #fff;
}

#sidebar ul li a.router-link-exact-active {
  background: #6d7fcc !important;
  color: #fff !important;
}

Las vistas de los diferentes componentes que vamos a crear usarán esta plantilla.

Creación de las rutas en Laravel

Cuando usamos Vue, las rutas de la aplicación cambian dinámicamente. Sin embargo, cuando accedes a una URL de forma externa, las rutas deben estar definida en Laravel. Necesitamos crear cuatro rutas; una para los mensajes recibidos, otra para los mensajes enviados, otra para los archivados y finalmente otra en la que mostraremos el formulario que nos permitirá crear mensajes.

Podemos definir todas las rutas en una sola mediante un patrón. Edita el archivo routes/web.php y agrega esta ruta al final del mismo.

Route::middleware('auth')->get('/messages/{any?}', 'App\Http\Controllers\AppController@index')->name('messages')->whereIn('any', [
    'sent',
    'archived',
    'compose',
]);

Tal y como ves, hemos usado el middleware auth, ya que estas rutas requieren autenticación. Hemos hecho referencia al método index del controlador AppController, que todavía no hemos creado. Para crearlo, crea el archivo AppController.php en el directorio app/Http/Controllers con este contenido:

<?php

namespace App\Http\Controllers;

class AppController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Muestra la vista de mensajes
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('mensajeria');
    }
}

Lo único que hacemos en el método index es devolver la vista mensajeria, que todavía no hemos creado. Para ello, crea el archivo mensajeria.blade.php en el directorio resources/views/ con este contenido:

<x-vue-layout>
  <x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
      {{ __('Mensajería') }}
    </h2>
  </x-slot>
  <app :user="{{ auth()->user() }}"></app>
</x-vue-layout>

Lo único que hemos hecho en esta vista ha sido asignarle la plantilla vue.blade.php que hemos creado anteriormente. Luego mostramos un encabezado con el título Mensajería y finalmente mostramos el componente app de Vue, que nodavía no hemos creado. Este componente tendrá una propiedad llamada user con los datos del usuario autenticado. Para pasarle una variable, anteponemos dos puntos : antes del nombre de la propiedad y, seguidamente, le asignamos un valor, que es el objeto auth()->user(). En caso de pasarle información estática, no necesitaríamos usar los dos puntos : antes del nombre de la propiedad.

Información! No necesitamos pasr la propiedad user a este componente. Lo he hecho por puros fines didácticos.
Para facilitarnos la vida, vamos a editar el archivo resources/views/layouts/navigation.blade.php. Lo que vamos a hacer es agregar un enlace a la ruta messages que hemos creado, de forma que se muestre en el menú superior:

<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
  <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
    {{ __('Dashboard') }}
  </x-nav-link>
  <x-nav-link :href="route('messages')" :active="request()->routeIs('messages')">
    {{ __('Mensajes') }}
  </x-nav-link>
</div>
// ...

Creación del componente raíz de Vue

Ahora vamos a crear el componente que usaremos como raíz. Para ello crea el archivo App.vue en el directorio resources/js/views/ con este contenido:

<template>
  <div>
    Hey girls, Hey boys, Superstar DJ's, Here we go.
    <router-view></router-view>
    Hey Boys!
  </div>
</template>

Todo componente de Vue tiene que tener una sección template. En su interior sencillamente mostramos un texto  de ejemplo y luego mostramos el componente router-view, que forma parte del enrutador de Vue. Este componente cargará los diferentes componentes de Vue que indiquemos cada vez que hagamos clic en un enlace.

Ahora edita el archivo app.js e importa el componente App.vue en la parte superior del archivo:

import App from './views/App.vue'

Luego edita la sentencia en la que creamos la aplicación. Debes pasarle el componente App usando el método component:

createApp().component('app', App).use(router).mount('#app');

Así es como debería quedar el archivo app.js en este momento:

import './bootstrap';
import Alpine from 'alpinejs';
import { createApp }  from 'vue';
import { createRouter, createWebHistory } from 'vue-router'
import App from './views/App.vue'

window.Alpine = Alpine;
Alpine.start();
const router = createRouter({
  history: createWebHistory(),
  routes: [
    // Rutas
  ],
});
    

if (document.getElementById('app')) {
  createApp().component('app', App).use(router).mount('#app');
}

Si accedes ahora a la aplicación desde tu navegador y haces clic en el enlace «Mensajes» que hemos agregado antes, deberías ver esto:

Agregando notificaciones Toast

Vamos a agregar un componente adicional que nos permita mostrar notificaciones Toast desde Vue. Para ello, regresa de nuevo a la línea de comandos e instala el paquete @alamtheinnov/flashtoast:

npm install --save @alamtheinnov/flashtoast

Ahora edita el archivo app.js e importe este paquete en la parte superior del archivo:

import { flashToast } from '@alamtheinnov/flashtoast';

Luego, en la línea en la que creas y montas la aplicación Vue, agrega el componente importado mediante la función use:

createApp().component('app', App).use(router).use(flashToast).mount('#app');

Ahora edita de nuevo el componte App.vue y modifícalo tal que así:

<template>
  <div>
    <FlashToast :position="'bottom-right'" v-zIndex:3000 v-class:any-class />
    <router-view></router-view>
  </div>
</template>

De este modo, las notificaciones que invoquemos se mostrarán en el componente raíz, de forma que podamos crearlas desde cualquier otro componente. Esto será útil a la hora de mostrar mensajes de confirmación o mensajes de error. Más adelante veremos cómo mostrarlas.

Por defecto, estas notificaciones no contendrán estilos. Afortunadamente, esta librería incluye una hoja de estilos predefinida. Para agregarla debes editar el archivo resources/css/app.css y agregar la siguiente línea al principio del archivo para importar dicha hoja de estilos:

@import '@alamtheinnov/flashtoast/dist/flashtoast.css';

Creación de los componentes Vue

Vamos a crear los componentes de la aplicación, modificando primero el componente App. Luego crearemos un componente para cada vista y varios subcomponentes.

Actualiza el componente App

Vamos a comenzar por el componente App.js. Los componentes de Vue contienen una plantilla, definida mediante una etiqueta template. También pueden contener scripts y estilos CSS. Lo que vamos a hacer es agregar el siguiente script debajo del cierre de la etiqueta template:

<script>
  export default {
    data() {
      return {
        user: {},
      }
    },
    mounted() {
      axios.get("/api/user").then((response) => {
        this.user = response.data;
      }).catch(error => {
        // Error
      });
    },
  }
</script>

Lo que hace este script es inicializar la variable de estado user. De entrada será un objeto vacío. Para asignarle datos esperamos a que Vue monte el componente, momento en el que se ejecutará la función mounted.

En el interior de la función mounted realizamos una petición mediante axios a la URL /api/user de nuestra aplicación. Si obtenemos una respuesta con éxito, obtendremos el contenido de la misma accediendo al objeto response.data de la respuesta, asignando dicho valor a la variable de estado user.

Tal y como ves, no hemos hecho nada en el caso de mostrase un error. Y esto no puede ser, noooooooooooooooooooo.

Por ello, vamos a mostrar una notificación. Al inicio del script importaremos el componente inject de Vue. Luego inyectaremos las notificaciones toast en la constante toast. Luego, en la sentencia catch de la petición axios, mostraremos el error mediante el método toast.error.

El método toast.error acepta como argumento un objeto compuesto por un título o title, un mensaje o message y un retardo o delay en milisegundos, que es el tiempo durante el cual se mostrará la notificación.

Así es como quedaría el archivo App.js finalmente:

<template>
  <div>
      <FlashToast :position="'bottom-right'" v-zIndex:3000 v-class:any-class />
      <router-view></router-view>
  </div>
</template>

<script>
  import { inject } from 'vue'

  export default {
    data() {
      return {
        user: {},
      }
    },
    mounted() {

      const toast = inject('toast');

      axios.get("/api/user").then((response) => {
        this.user = response.data;
      }).catch(error => {
        toast.error({
          title: 'Error!',
          message: error.response.data.message,
          delay: 5000
        });
      });
    },
  }
</script>

Sin embargo, todavía tenemos que crear la ruta /api/user en el servidor. Para ello edita el archivo routes/api y agrega esta ruta, que sencillamente devuelve los datos del usuario que realiza la petición:

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Tal y como ves, hemos usado el middleware auth:sanctum. Debemos usarlo en todas las rutas que necesiten autenticación.

Si ahora  accedes a la sección de mensajes de la aplicación e inspeccionas las peticiones realizadas, deberías ver cómo el componente de Vue realiza una petición a este endpoint satisfactoriamente:

Muestra la lista de mensajes recibidos

Ahora vamos a crear un componente que nos permita visualizar los mensajes recibidos. Primero vamos a crear la ruta y la lógica en Laravel que nos permita obtener los mensajes.

Crea el controlador MessageController.php en el directorio app/Http/Controllers. Puedes hacerlo mediante este comando:

php artisan make:controller MessageController

En su interior incluye al principio del archivo el modelo Message:

use App\Models\Message;

Luego, en el interior de la clase MessageController, agrega el método index, mediante el cual devolveremos la lista de mensajes del usuario:

public function index(Request $request)
{
  $userId = auth()->user()->id;
  $messages = Message::where([
    'user_id'  =>  $userId,
    'to'  =>  $userId
  ])->with(['user', 'fromUser', 'toUser'])->get();
  return response()->json($messages);
}

Lo que hemos hecho ha sido obtener los mensajes de la tabla messages pertenecientes al usuario autenticado. También hemos obtenido sus relaciones, incluyendo el usuario que envía el mensaje y el que lo recibe mediante el método with.

Hemos obtenemos los mensajes que otros usuarios le han enviado, obteniendo así los mensajes de la bandeja de entrada. Luego los devolvemos en formato JSON. Así es como debería quedar el archivo por ahora:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Message;

class MessageController extends Controller
{
  public function index(Request $request)
  {
    $userId = auth()->user()->id;
    
    $messages = Message::where([
      'user_id' => $userId,
      'to' => $userId
    ])->with(['user', 'fromUser', 'toUser'])->get();
    return response()->json($messages);
  }
}

Sin embargo, estaría bien poder obtener los mensajes enviados y también los eliminados. Por ello, vamos a aceptar la opción mailbox, que por defecto tendrá el valor de inbox. Estos son sus posibles valores:

  • inbox: Obtenemos los mensajes que el usuario ha recibido «to».
  • sent: Obtenemos los mensajes que el usuario ha enviado «from».
  • archived: Obtenemos los mensajes que el usuario ha enviado a la papelera usando el método onlyTrashed.

Para solucionar esto, sencillamente agregamos una sentencia switch. Así es como debería quedar el controlador MessageController por ahora teniendo esto en cuenta:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Message;

class MessageController extends Controller
{
  public function index(Request $request)
  {
    $userId = auth()->user()->id;
    $mailbox = $request->query('mailbox', 'inbox');

    $query = Message::where('user_id', $userId);

    switch ($mailbox) {
      case 'archived':
        $query = $query->onlyTrashed();
        break;
      case 'sent':
        $query = $query->where('from', $userId);
        break;
      default:
        $query = $query->where('to', $userId);
    }

    $messages = $query->with(['user', 'fromUser', 'toUser'])->get();

    return response()->json($messages);
  }
}

Seguidamente, edita el archivo routes/api.php y agrega la siguiente ruta:

Route::middleware('auth:sanctum')->get(
    '/messages', 
    '\App\Http\Controllers\MessageController@index'
);

Ahora deberíamos crear tres vistas. Una para los mensajes recibidos, otra para los enviados y otra para los eliminados. Por ello, necesitaremos los componentes Messages.vue, MessagesSent.vue y MessagesArchived.vue en el directorio resources/js/views. Pero todavía no los vamos a crear.

Todos estos componentes serán muy similares, ya que se limitan a mostrar mensajes. Podría ser interesante crear un componente que pueda ser reutilizado en estas tres vista. Esto es precisamente lo que vamos a hacer. Por ello, vamos a crear el componente MessageList.vue en el directorio resources/js/components/.

Primero expondré el código del componente y luego lo explicaré:

<template>
  <div class="message-list">
    <div>
      <table
        class="w-full flex flex-row flex-no-wrap overflow-hidden sm:shadow-lg"
        v-if="messages.length"
      >
        <thead class="border-b-0">
          <tr
            v-for="message in messages" :key="message.id + '_head'"
            class="flex flex-col flex-no wrap sm:table-row rounded-l-lg sm:rounded-none mb-2 sm:mb-0"
          >
            <th class="p-3 text-left">
              From
            </th>
            <th class="p-3 text-left">
              To
            </th>
            <th class="p-3 text-left">
              Subject
            </th>
          </tr>
        </thead>
        <tbody class="flex-1  sm:flex-none">
          <tr
            v-for="(message) in messages" :key="message.id"
            class="odd:bg-white even:bg-gray-100 flex flex-col flex-no wrap sm:table-row mb-2 sm:mb-0"
            :class="[ message.to === message.user_id && !message.read_at ? 'font-bold' : '' ]"
          >
          <td class="border border-l-0 border-r-0 p-3">
            {{ message.from_user.name }}
          </td>
          <td class="border border-l-0 border-r-0 p-3">
            {{ message.to_user.name }}
          </td>
          <td class="border border-l-0 border-r-0 p-3">
            {{ message.subject }}
          </td>
        </tr>
      </tbody>
      </table>
      <div class="editor w-full p-10 text-gray-400 text-center" v-else> 
        No se han obtenido mensajes
      </div>
    </div>
  </div>
</template>

<script>
  import { inject } from 'vue';
  export default {
    props: {
      mailbox:  {
        type: String,
        default: 'inbox'
      }
    },
    data() {
      return {
        messages: [],
      };
    },
    mounted() {
      this.toast = inject('toast');
      this.getMessages();
    },
    methods: {
      getMessages() {
        axios.get('/api/messages?mailbox=' + this.mailbox).then((response) => {
          this.messages = response.data;
        }).catch(error => {
          this.toast.error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });
        });
      },
    }  
  }
</script>

<style>
  @media (min-width: 640px) {
  table {
    display: inline-table !important;
  }

  thead tr:not(:first-child) {
    display: none;
  }
  }
</style>

En la sección script hemos definido la propiedad mailbox, que por defecto tendrá el valor inbox. Esta propiedad se la pasaremos al componente desde el componente de nivel superior en función del tipo de mensaje que queramos obtener. Luego hemos definido la variable de estado data, que contendrá los mensajes, aunque por ahora será únicamente un array vacío.

Luego hemos usado el método mounted, de modo que se ejecute el método getMessages cuando se monte el componente. Este método lo definimos en el interior de la propiedad methods, como es habitual al usar Vue.

En el interior del método getMessages realizamos una llamada a la ruta /api/messages para obtener los mensajes, asegurándonos de que pasamos el valor de la propiedad mailbox como parámetro en la query string. Si todo va bien, asignamos los mensajes obtenidos a la variable de estado messages. De lo contrario mostramos una notificación de error del mismo modo en el que la hemos mostrado antes.

Vamos ahora con la sección template del componente. Lo que hemos hecho ha sido mostrar una tabla HTML los mensajes. La mostramos condicionalmente usando una sentencia v-if. Si el número de mensajes de la variable de estado messages es mayor que cero, algo que comprobamos mediante la función messages.length, entonces mostramos la tabla. De lo contrario usamos la sentencia v-else para indicar que no se han obtenido mensajes.

Si se obtienen mensajes, mostramos la cabecera de la tabla y también los mensajes, recorriéndolo mediante una bucle v-for. Quizás de hayas percatado de que definimos también varias cabeceras. Pues bien; esto es para la vista móvil, cuando la pantalla es demasiado pequeña, en donde mostramos la lista de mensajes de un modo diferente. Al final del archivo también hemos aplicado estilos CSS en función de esto.

Ahora vamos a crear el componente Messages.vue en el directorio resources/js/views/. Lo único que haremos en su interior es importar y mostrar el componente MessageList que acabamos de crear:

<template>
  <div class="view-messages">
    <MessageList/>
  </div>
</template>

<script>
  import MessageList from '../components/MessageList.vue';

  export default {
    components: {
      MessageList,
    },
  };
</script>

Ya solo nos falta editar el archivo app.js, importar el componente Messages y asociarlo a la ruta /messages. Para ello importa el componente en la parte superior del archivo:

import Messages from './views/Messages.vue';

Luego asocia el componente a la ruta:

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/messages',
      name: 'messages',
      component: Messages,
    },
    // ...
  ],
});

Así es como debe quedar el archivo app.js:

import './bootstrap';
import Alpine from 'alpinejs';
import { createApp }  from 'vue';
import { flashToast } from '@alamtheinnov/flashtoast';
import { createRouter, createWebHistory } from 'vue-router'

import App from './views/App.vue';

import Messages from './views/Messages.vue';

window.Alpine = Alpine;
Alpine.start();

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/messages',
      name: 'messages',
      component: Messages,
    }
  ],
});

if (document.getElementById('app')) {
    createApp().component('app', App).use(router).use(flashToast).mount('#app');
}

Si ahora accedes a la aplicación en tu navegador y luego a la sección de mensajes, deberías ver la lista de mensajes recibidos por el usuario con el que has hecho login en la aplicación:

Muestra la lista de mensajes enviados

En el apartado anterior hemos visto cómo mostrar los mensajes recibidos. Ahora mostraremos los enviados, lo cual no nos costará mucho esfuerzo, ya que el trabajo está ya casi hecho.

Crea el componente MessagesSent.vue en el directorio resources/js/views/. En su interior importaremos y mostraremos el componente MessageList al igual que antes, con la única diferencia de que ahora especificaremos la propiedad mailbox, cuyo valor será sent:

<template>
  <div class="view-messages">
    <MessageList :mailbox="'sent'"/>
  </div>
</template>

<script>
  import MessageList from '../components/MessageList.vue';

  export default {
    components: {
      MessageList,
    },
  };
</script>

Al igual que antes, importa el componente en el archivo app.js:

import MessagesSent from './views/MessagesSent.vue';

Ahora agrega el componente al router aunque esta vez debes asociarlo a la ruta /messages/sent:

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/messages',
      name: 'messages',
      component: Messages,
    },
    {
      path: '/messages/sent',
      name: 'messages.sent',
      component: MessagesSent,
    },
    // ...
  ],
});

Si ahora accedes a la ruta /messages/sent en tu navegador, deberías ver los mensajes enviados. Pero no podemos esperar que el usuario introduzca la ruta manualmente, así que vamos a agregar un enlace en el menú izquierdo. Para ello edita el archivo resources/views/layouts/vue.blade.php y agrega estos dos enlaces a la barra lateral:

<!-- Page Content -->
<main id="app" class="flex grow w-full ">
   <aside id="sidebar" class="bg-gray-700 overflow-auto min-w-[250px]  table-cell">
     <div class="items-center text-center">
       <a href="{{ url('/') }}" class="text-lg font-semibold text-gray-100 no-underline p-2 my-3 block">
         {{ config('app.name', 'Mensajería') }}
       </a>                   
     </div>
     <ul class="list-none text-lg text-white">
       <li>
         <router-link :to="{ name: 'messages' }" class="block p-4">
           <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="stroke-current inline-block"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
           Recibidos
         </router-link>
       </li>
       <li>
         <router-link :to="{ name: 'messages.sent' }" class="block p-4">
           <svg xmlns="http://www.w3.org/2000/svg" height="15" viewBox="0 0 24 24" fill="none" width="15" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="stroke-current inline-block"><path fill="none" d="M38 2v8h-26.34l7.17-7.17-2.83-2.83-12 12 12 12 2.83-2.83-7.17-7.17h30.34v-12z"/></svg>
           Enviados
         </router-link>
       </li>
    </ul>
  </aside>
  <div class="items-center w-full table-cell">
    {{ $slot }}
  </div>
</main>

Verás que para definir los enlaces hemos usado el componente router-link. De este modo usamos el enrutador de Vue para modificar únicamente el contenido de la sección router-view que hemos definido en el componente App.js.

Si ahora accedes a la aplicación desde tu navegador, deberías ver esto:

Muestra la lista de mensajes eliminados

Para mostrar la lista de mensajes eliminados vamos a seguir el mismo procedimiento. Crea el componente MessagesArchived.vue en el directorio resources/js/views/. En su interior importaremos y mostraremos el componente MessageList al igual que antes, con la única diferencia de que ahora especificaremos la propiedad mailbox, cuyo valor será archived:

<template>
  <div class="view-messages">
    <MessageList :mailbox="'archived'"/>
  </div>
</template>

<script>
  import MessageList from '../components/MessageList.vue';

  export default {
    components: {
      MessageList,
    },
  };
</script>

Ahora importa el componente MessagesArchived en el archivo app.js:

import MessagesArchived from './views/MessagesArchived.vue';

Ahora agrega el componente al router, aunque esta vez debes asociarlo a la ruta /messages/archived:

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/messages',
      name: 'messages',
      component: Messages,
    },
    {
      path: '/messages/sent',
      name: 'messages.sent',
      component: MessagesSent,
    },
    {
      path: '/messages/archived',
      name: 'messages.archived',
      component: MessagesArchived,
    },
  ],
});

Vamos a agregar también otro enlace al menú en la plantilla resources/views/layouts/vue.blade.php:

<li>
  <router-link :to="{ name: 'messages.archived' }" class="block p-4">
    <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="stroke-current inline-block"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
    Archivados
  </router-link>
</li>

Si ahora accedes a la aplicación, podrás ver los mensajes eliminados:

Si tienes problemas, asegúrate de que has agregado el trait SoftDeletes al modelo Message.

Cómo crear un nuevo mensaje

Vamos a agregar la funcionalidad que nos permita crear un nuevo mensaje. Para ello vamos a editar el controlador MessageController. Debes agregar el método post, que incluirá la lógica para validar e insertar mensajes.

Dado que para un mensaje existen dos mensajes independientes, haremos uso de transacciones, por lo que debemos agregar la facade DB. También usaremos la librería Carbon.

Agrega Carbon y las facades Auth y DB en la parte superior del archivo usando la sentencia use:

use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Auth;

Luego agrega el método create, que explicaremos tras mostrar el código.

public function create(Request $request)
{
  DB::beginTransaction();

  try {
    $this->validate($request, [
      'subject' => 'required|min:1',
      'content' => 'required|min:1',
      'to' => [
        'required',
        'exists:users,id'
      ]
    ]);

    $message = DB::table('messages')->insert(
      [
        'user_id' => Auth::id(),
        'from' => Auth::id(),
        'to' => $request->to,
        'subject' => $request->subject,
        'content' => $request->content,
        'created_at' => Carbon::now(),
        'updated_at' => Carbon::now()
      ]
    );

    DB::table('messages')->insert(
      [
        'user_id' => $request->to,
        'from' => Auth::id(),
        'to' => $request->to,
        'subject' => $request->subject,
        'content' => $request->content,
        'created_at' => Carbon::now(),
        'updated_at' => Carbon::now()
      ]
    );

    DB::commit();
    return response()->json($message, 201);

  } catch (\Exception $e) {
    DB::rollback();
    abort(400, $e->getMessage());
  }
}

Mediante el método DB::beginTransaction hemos iniciado una transacción, de forma que los cambios de las siguientes consultas que ejecutemos no se realicen en la base de datos hasta que la transacción se confirme.

Luego validamos los datos del mensaje mediante el método validate, asegurándonos de que el asunto subject y el contenido content del mensaje existen. También comprobamos que el destinatario exista en la base de datos.

Luego ejecutamos el método insert en la tabla messages para insertar el mensaje del emisor. Seguidamente insertamos el del destinatario.

Finalmente hemos usado el método commit para confirmar la transacción y que los cambios persistan en la base de datos. Luego devolvemos el mensaje creado como respuesta.

Edita el archivo routes/api.php y agrega una ruta para el método que acabamos de crear:

Route::middleware('auth:sanctum')->post(
  '/messages', 
  '\App\Http\Controllers\MessageController@create'
);

Ahora deberíamos crear un componente de Vue que nos permita crear un mensaje. Sin embargo, necesitamos crear previamente una lista de usuarios, de forma que el usuario pueda seleccionar el destinatario. Por ello, necesitamos crear un nuevo controlador al que llamaremos UserController. Hazlo desde la línea de comandos:

php artisan make:controller UserController

Bastará con agregar el método index, que mostrará la lista de usuarios:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;

use App\Models\User;

class UserController extends Controller
{
  public function index()
  {
    $users = User::where('id', '!=', Auth::id())->orderBy('name', 'ASC')->get();
    return response()->json($users);
  }
}

Lo único que hemos hecho ha sido obtener la lista del usuarios, excluyendo al usuario actual. Ahora agrega la ruta users en el archivo routes/api.php, de forma que podamos realizar la petición al servidor:

Route::middleware('auth:sanctum')->get('/users', '\App\Http\Controllers\UserController@index')->name('users');

Ahora vamos a crear el archivo UserList.vue en el directorio resources/js/components/. En este componente mostraremos la lista de usuarios. Se la pasaremos como propiedad, ya que esta lista nos hará falta en el componente de nivel superior, tal y como veremos:

<template>
  <div class="user-list bg-gray-200 min-h-screen border-r h-screen overflow-y-scroll min-w-64">
    <ul>
      <li v-for="(user, index) in users" :key="user.id"
        @click="selectUser(index, user)"
        class="p-4 border-b font-bold text-sm cursor-pointer"
        :class="[
          selected ===  index  && 'bg-blue-400 hover:bg-blue-400 text-white',
          selected !==  index  && 'hover:bg-gray-300'
        ]"
      >
        <p>{{ user.name }}</p>
        <p>{{ user.email + user.id + selected }}</p>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    props: {
      users: {
        type: Array,
        default: [],
      },
    },
    data() {
      return {
        selected: 0
      };
    },
    methods: {
      selectUser(index, user) {
        this.selected = index;
        this.$emit('selected', user);
      }
    }
  }
</script>

Tal y como ves, hemos recorrido la lista de usuarios usando una sentencia v-for, asegurándonos de que cada uno tiene una clave o key asignada.

En al lista de métodos hemos agregado el método selectUser, que asigna el índice del usuario seleccionado a la variable de estado selected, de forma que podamos aplicar estilos al usuario seleccionado. Además usamos el método $emit al seleccionar el usuario. El método $emit nos permite emitir un evento que podrá ser recibido por ciertos componentes. En este evento incluimos los datos del usuario seleccionado.

Para iniciar el método selectUser hemos usado la sentencia @click en la lista de usuarios.

Ahora vamos a crear una nueva vista en la que incluiremos el componente que hemos creado. Para ello, crea el archivo ComposeMessage.vue en el directorio resources/js/views:

<template>
  <div class="view-compose flex">
    <UserList :users="users" @selected="writeMessageTo" />
  </div>
</template>

<script>
  import { inject } from 'vue';
  import UserList from '../components/UserList.vue';

  export default {
    data() {
      return {
        recipient: null,
        users: [],
      };
    },

    mounted() {
      this.toast = inject('toast');
      axios.get("/api/users").then((response) => {
        this.users = response.data;
      }).catch(error => {
        this.toast.error({
          title: 'Error!',
          message: error.response.data.message,
          delay: 5000
        });
      });
    },
    methods: {
      writeMessageTo(user) {
        this.recipient = user;
      }
    },
    components: { UserList }
  };
</script>

Lo que hemos hecho ha sido importar el componente UserList. Al montar el componente realizamos una petición al endpoint /api/users que hemos creado antes, obteniendo la lista de usuarios y almacenándola en la variable de estado users.

Si te fijas, hemos pasado como propiedad el método writeMessageTo al componente UserList. Lo que ocurrirá es que cuando en el componente UserList se seleccione un usuario y se emita el evento selected, entonces se ejecutaré el método writeMessageTo con el usuario que emitimos junto con el evento. Lo único que hacemos es asignar el usuario a la variable de estado recipient, que será el destinatario del mensaje.

Ahora agrega la vista ComposeMessage al archivo app.js:

import ComposeMessage from './views/ComposeMessage.vue'

Ahora asigna el componente a la ruta messages.compose:

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/messages',
            name: 'messages',
            component: Messages,
        },
        {
            path: '/messages/sent',
            name: 'messages.sent',
            component: MessagesSent,
        },
        {
            path: '/messages/archived',
            name: 'messages.archived',
            component: MessagesArchived,
        },
        {
            path: '/messages/compose',
            name: 'messages.compose',
            component: ComposeMessage,
        },
    ],
});

No nos olvidemos tampoco de agregar un botón que nos permita acceder a esta vista en la plantilla resources/views/layouts/vue.blade.php:

<li>
  <router-link :to="{ name: 'messages.compose' }" class="block p-4">
    <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="stroke-current inline-block"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg> 
    Nuevo
  </router-link>
</li>

Si ahora accedes a la aplicación desde tu navegador y haces clic en «Nuevo», deberías ver la lista de usuarios:

Una vez seleccionado un destinatario, ya podemos pasárselo al editor del mensaje, que vamos a crear ahora. Para ello crea el componente Editor.vue en el directorio resources/js/components. En este componente nos vamos a limitar a crear un formulario. Mediante el evento @submit ejecutamos el método checkForm, que emitirá el evento send, al que le pasamos el asunto del mensaje y el contenido del mismo:

<template>
  <div class="editor w-full p-10" v-if="recipient"> 
    <form @submit="checkForm"  class="w-full p-6">
      <div class="w-full">
        <label class="block text-gray-700 text-sm font-bold mb-2">Destinatario</label>
        <p>{{recipient.name}} ({{recipient.email}})</p>
      </div>

      <div class="w-full my-6">
        <label for="subject" class="block text-gray-700 text-sm font-bold mb-2">Asunto</label>
        <input type="text" name="subject"  v-model="subject" placeholder="Asunto del mensaje..." class="form-input w-full">
        <p class="text-red-500 italic mt-4" v-if="errorSubject">
          {{ errorSubject }}
        </p>
      </div>

      <div class="w-full my-6">
        <label for="content" class="text-gray-700 text-sm font-bold mb-2 ">Mensaje</label>
        <textarea name="content" v-model="content" placeholder="Redacta el contenido del mensaje aquí..." class="form-input w-full h-48 my-1"></textarea>
        <p class="text-red-500 italic mt-4" v-if="errorContent" >
          {{ errorContent }}
        </p>
       </div>
      <div class="w-full my-6">
        <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-gray-100 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
          Enviar mensaje
        </button>
      </div>
    </form>
  </div>
  <div class="editor w-full p-10 text-gray-400 text-center" v-else> 
    Por favor, selecciona un destinatario
  </div>
</template>

<script>
  export default {
    props: {
       recipient: {
        type: Object,
        default: null,
      },
    },
    data() {
      return {
        errorContent: '',
        errorSubject: '',
        content: '',
        subject: '',
      };
    },
    methods: {
      checkForm(e) {
        const errors = [];
        e.preventDefault();

        this.errorContent = '';
        this.errorSubject = '';

        if (this.subject.length < 1) {
          this.errorSubject = 'A subject is required';
          errors.push(this.errorSubject);
        }

        if (this.content.length < 1) {
          this.errorContent = 'A message is required';
          errors.push(this.errorContent);
        }

        if (!errors.length) {
          this.$emit('send', this.subject, this.content);
          this.content = '';
          this.subject = '';
        }
      }
    }

  }
</script>

Tal y como ves, realizamos comprobaciones básicas en el frontend y además solo mostramos el mensaje si existe un destinatario, algo que realizamos mediante una sentencia v-if.

Finalmente, vamos a agregar este componente a la vista ComposeMessage y a mostrarle cada vez que se seleccione un usuario:

<template>
  <div class="view-compose flex">
    <UserList :users="users" @selected="writeMessageTo" />
    <Editor :recipient="recipient"  @send="sendMessage"/>
  </div>
</template>

<script>
  import UserList from '../components/UserList.vue';
  import Editor from '../components/Editor.vue';
  import { inject } from 'vue';

  export default {
    data() {
      return {
        recipient: null,
        users: [],
      };
    },

    mounted() {
      this.toast = inject('toast');
      axios.get("/api/users").then((response) => {
        this.users = response.data;
      }).catch(error => {
        this.toast.error({
          title: 'Error!',
          message: error.response.data.message,
          delay: 5000
        });
      });
    },
    methods: {
      sendMessage(subject, content) {

        if (!this.recipient) {
          this.toast .error({
            title: 'Error!',
            message: 'Destinatario no válido!',
            delay: 5000
          });
          return;
        }
        axios.post('/api/messages', {
          'to': this.recipient.id,
          'subject': subject,
          'content': content,
        }).then((response) => {
          this.toast .success({
            title: 'Éxito!',
            message: 'Mensaje enviado con éxito',
            delay: 5000
          });
        }).catch(error => {
          this.toast .error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });
        });
      },
      writeMessageTo(user) {
        this.recipient = user;
      }
    },
    components: { UserList, Editor }
  };
</script>

Tal y como ves, importamos el componente Editor y luego lo mostramos pasándole un destinatario. Al evento send le asignamos el método sendMessage, que se encargará de llamar al endpoint /api/messages usando el método POST. Luego mostramos un mensaje de éxito o de error en función de la respuesta.

Si ahora envías un mensaje, debería enviarse con éxito:

Cómo leer un mensaje

Hemos visto cómo crear mensajes, pero todavía falta la parte de visualizarlos. Cada vez que vayamos a leer un mensaje debemos marcarlo como leído. Por ello, necesitamos agregar un nuevo método al controlador MessageController que nos permita actualizar mensajes. Bastará con agregar una opción que actualice el campo read_at para así marcar los mensajes como leídos:

public function update(Request $request, Message $message)
{
  if ($message->user_id !== Auth::id()) {
    abort(403, 'Acceso inválido.');
  }

  if (isset($request->read)) {
    $message->read_at = Carbon::now();
  }

  $message->save();

  return response()->json($message, 204);
}

El método update recibe como argumento un mensaje, de forma que Laravel devuelva automáticamente un error 404 en caso de que no exista. Luego comprobamos que el mensaje pertenezca al usuario, devolviendo un error 403 si no es el caso. Finalmente le asignamos la fecha actual al campo read_at y guardamos el mensaje.

Ahora vamos a agregar la ruta PATCH a este método en el archivo routes/api.php:

Route::middleware('auth:sanctum')->patch(
  '/messages/{message}',
  '\App\Http\Controllers\MessageController@update'
)->withTrashed();

Tras terminar con el backend, vamos a crear un nuevo componente llamado Message.vue en el directorio resources/js/components/. Este componente mostrará el emisor del mensaje, el destinatario, el asunto y el cuerpo del mensaje:

<template>
  <div class="message w-full p-10 leading-6 m-auto max-w-6xl ">
    <div class="w-full">
      <label class="block text-gray-700 text-sm font-bold mb-2">
        From
      </label>
      <p>{{message.from_user.name}} ({{message.from_user.email}})</p>
    </div>
  
    <div class="w-full my-6">
      <label class="block text-gray-700 text-sm font-bold mb-2">
        To
      </label>
      <p>{{message.to_user.name}} ({{message.to_user.email}})</p>
    </div>

    <div class="w-full my-6">
      <label class="block text-gray-700 text-sm font-bold mb-2">
        Subject
      </label>
      <p>{{message.subject}}</p>
    </div>

    <div class="w-full my-6">
      <label class="block text-gray-700 text-sm font-bold mb-2">
        Content
      </label>
      <p>{{message.content}} ({{message.content}})</p>
    </div>
    <div class="w-full my-6">
      <button
        type="submit"
        v-on:click="closeMessage"
        class="bg-blue-500 hover:bg-blue-700 text-gray-100 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
      >
        Volver
      </button>
    </div>
  </div>
</template>

<script>
  import { inject } from 'vue'

  export default {
    props: {
      message:  {
        type: Object,
        default: null
      }
    },
    data() {
      return {
        close: 0
      };
    },
    created () {
      this.toast = inject('toast');
      if (this.message.user_id === this.message.to && !this.message.read_at) {
        axios.patch('/api/messages/' + this.message.id, {
          'read': true,
        }).then((response) => {
          this.$emit('readMessage', response.data);
        }).catch(error => {
          this.toast.error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });
        });
      }
    },
    methods: {
      closeMessage() {
        this.$emit('closeMessage');
      },
    }  
  }
</script>

Este componente recibe el mensaje a mostrar como propiedad message. Cuando se cree el componente, usaremos el método created para realizar una petición API para actualizar el campo read_at del mensaje. Para ello usamos la opción read, tal y como la hemos configurado en el controlador. Cuando se marca como leído un mensaje, emitimos el evento readMessage, cuyo objetivo será el de marcar el mensaje como leido en la lista de mensajes.

Hemos usado también un botón que nos permite cerrar el mensaje para volver a ver la lista de mensajes. Le hemos agregado la acción v-on:click="closeMessage", de forma que cuando se pulse sobre el botón se ejecute el método closeMessage, que emitirá el evento closeMessage.

Ahora vamos a importar este componente en el componente MessageList que hemos creado antes, de forma que los mensajes se muestren cuando hagamos clic sobre ellos:

<template>
  <div class="message-list">
    <Message
      :message="message"
      @readMessage="readMessage"
      @closeMessage="closeMessage"
      v-if="message"
    />
    <div v-else>
      <table
        class="w-full flex flex-row flex-no-wrap overflow-hidden sm:shadow-lg"
        v-if="messages.length"
      >
        <thead class="border-b-0">
          <tr
            v-for="message in messages" :key="message.id + '_head'"
            class="flex flex-col flex-no wrap sm:table-row rounded-l-lg sm:rounded-none mb-2 sm:mb-0"
          >
            <th class="p-3 text-left">
              From
            </th>
            <th class="p-3 text-left">
              To
            </th>
            <th class="p-3 text-left">
              Subject
            </th>
          </tr>
        </thead>
        <tbody class="flex-1  sm:flex-none">
          <tr
            v-for="(message) in messages" :key="message.id"
            @click="viewMessage(message)"
            class="odd:bg-white even:bg-gray-100 hover:bg-gray-200 flex flex-col flex-no wrap sm:table-row mb-2 sm:mb-0 cursor-pointer hover:bg-gray-100"
            :class="[ (message.to === message.user_id && !message.read_at) ? 'font-bold' : '' ]"
          >
            <td class="border border-l-0 border-r-0 p-3">
              {{ message.from_user.name }}
            </td>
            <td class="border border-l-0 border-r-0 p-3">
              {{ message.to_user.name }}
            </td>
            <td class="border border-l-0 border-r-0 p-3">
              {{ message.subject }}
            </td>
          </tr>
        </tbody>
      </table>
      <div class="editor w-full p-10 text-gray-400 text-center" v-else> 
        No se han obtenido mensajes
      </div>
    </div>
  </div>
</template>

<script>
  import { inject } from 'vue';
  import Message from './Message.vue';
  export default {
    props: {
      mailbox:  {
        type: String,
        default: 'inbox'
      }
    },
    components: {
      Message,
    },
    data() {
      return {
        message: null,
        messages: [],
      };
    },
    mounted() {
      this.toast = inject('toast');
      this.getMessages();
    },
    methods: {
      getMessages() {
        axios.get('/api/messages?mailbox=' + this.mailbox).then((response) => {
          this.messages = response.data;
        }).catch(error => {
          this.toast.error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });

        });
      },
      viewMessage(message) {
        this.message = message;
      },
      readMessage(message) {
        this.messages.find(m => m.id === message.id).read_at = message.read_at;
      },
      closeMessage() {
        this.message = null;
      },
    }  
  }
</script>

<style>
  @media (min-width: 640px) {
  table {
    display: inline-table !important;
  }

  thead tr:not(:first-child) {
    display: none;
  }
  }
</style>

Hemos realizado cambios al componente MessageList. Hemos importado el mensaje y lo hemos agregado al objeto components. Hemos agregado los eventos viewMessage, readMessage y closeMessage, que se ejecutarán cuando accedamos a un mensaje, cuando se muestre y cuando se cierre respectivamente.

En la plantilla hemos incluido el componente Message, que se mostrará cuando la variable de estado message contenga un mensaje. En caso contrario mostramos la lista de mensajes.

Al componente Message le hemos pasado la propiedad message, así como las funciones readMessage y closeMessage que se ejecutarán desde el componente Message cuando se emitan los eventos correspondientes. La función readMessage sencillamente marcará el mensaje al que hemos accedido como leído, de forma que podamos dejar de mostrarlo en negrita. La función closeMessage sencillamente asigna el valor null a la variable de estado message.

En la lista de mensajes, hemos agregado el evento @click="viewMessage(message)", que asigna el mensaje seleccionado a la variable de estado message.

Si ahora accedes a la aplicación desde tu navegador y accedes a un mensaje, deberías poder leerlo:

Archivando mensajes

Vamos a agregar la posibilidad de archivar y desarchivar mensajes. Para comenzar edita de nuevo el controlador MessageCotroller y agrega el método delete:

public function delete(Request $request, Message $message)
{
  if ($message->user_id !== Auth::id()) {
    abort(401, 'Invalid access.');
  }
  $message->delete();   
  return response()->json([
    'success' => true
  ], 200);
}

Lo que hace el método delete en es enviar a la papelera el mensaje que hemos pasado como argumento. Es decir, que el campo deleted_at dejará de ser nulo, pasando a contener la fecha de eliminación del mensaje. Mientras este campo siga sin ser nulo, Laravel no lo incluirá en las consultas salvo que se lo indiquemos explícitamente.

Vamos a modificar también el método update, de forma que si el parámetro recover está presente, eliminemos el mensaje de la papelera dando del valor null al campo deleted_at:

public function update(Request $request, Message $message)
{
  if ($message->user_id !== Auth::id()) {
    abort(401, 'Invalid access.');
  }

  if (isset($request->read)) {
    $message->read_at = Carbon::now();
  }

  if (isset($request->recover)) {
    $message->deleted_at = null;    
  }

  $message->save();

  return response()->json($message, 204);
}

Para acabar con la parte de PHP, vamos a agregar la ruta DELETE /messages/{message}:

Route::middleware('auth:sanctum')->delete(
  '/messages/{message}',
  '\App\Http\Controllers\MessageController@delete'
);

Ahora vamos a modificar de nuevo el componente MessageList para agregar las opciones que nos permitan archivar y desarchivar mensajes. Vamos a agregar los métodos archiveMessage y recoverMessage, además de los botones con sendos eventos @click que nos permitan realizar tales acciones:

<template>
  <div class="message-list">
    <Message
      :message="message"
      @readMessage="readMessage"
      @closeMessage="closeMessage"
      v-if="message"
    />
    <div v-else>
      <table
        class="w-full flex flex-row flex-no-wrap overflow-hidden sm:shadow-lg"
        v-if="messages.length"
      >
        <thead class="border-b-0">
          <tr
            v-for="message in messages" :key="message.id + '_head'"
            class="flex flex-col flex-no wrap sm:table-row rounded-l-lg sm:rounded-none mb-2 sm:mb-0"
          >
            <th class="p-3 text-left">
              From
            </th>
            <th class="p-3 text-left">
              To
            </th>
            <th class="p-3 text-left">
              Subject
            </th>
            <th class="p-3 text-left" width="110px">
              Actions
            </th>
          </tr>
        </thead>
        <tbody class="flex-1  sm:flex-none">
          <tr
            v-for="(message, index) in messages" :key="message.id"
            @click="viewMessage(message)"
            class="odd:bg-white even:bg-gray-100 hover:bg-gray-200 flex flex-col flex-no wrap sm:table-row mb-2 sm:mb-0 cursor-pointer hover:bg-gray-100"
            :class="[ (message.to === message.user_id && !message.read_at) ? 'font-bold' : '' ]"
          >
            <td class="border border-l-0 border-r-0 p-3">
              {{ message.from_user.name }}
            </td>
            <td class="border border-l-0 border-r-0 p-3">
              {{ message.to_user.name }}
            </td>
            <td class="border border-l-0 border-r-0 p-3">
              {{ message.subject }}
            </td>
            <td
              @click="recoverMessage(index, message)"
              v-on:click.stop
              v-if="message.deleted_at"
              class="border border-l-0 border-r-0 p-3 text-red-400 hover:text-red-600 hover:font-medium cursor-pointer"
            >
              Recover
            </td>
            <td
              @click="archiveMessage(index, message)"
              v-on:click.stop
              v-else
              class="border border-l-0 border-r-0 p-3 text-red-400 hover:text-red-600 hover:font-medium cursor-pointer"
            >
              Archive
            </td>
          </tr>
        </tbody>
      </table>
      <div class="editor w-full p-10 text-gray-400 text-center" v-else> 
        No se han obtenido mensajes
      </div>
    </div>
  </div>
</template>

<script>
  import { inject } from 'vue';
  import Message from './Message.vue';
  
  export default {
    props: {
      mailbox:  {
        type: String,
        default: 'inbox'
      }
    },
    components: {
      Message,
    },
    data() {
      return {
        message: null,
        messages: [],
      };
    },
    mounted() {
      this.toast = inject('toast');
      this.getMessages();
    },
    methods: {
      getMessages() {
        axios.get('/api/messages?mailbox=' + this.mailbox).then((response) => {
          this.messages = response.data;
        }).catch(error => {
          this.toast.error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });
        });
      },
      viewMessage(message) {
        this.message = message;
      },
      readMessage(message) {
        this.messages.find(m => m.id === message.id).read_at = message.read_at;
      },
      closeMessage() {
        this.message = null;
      },
      archiveMessage(index, message) {

        axios.delete('/api/messages/' + message.id).then((response) => {
          this.toast.success({
            title: 'Success!',
            message: 'Mensaje eliminado con éxito.',
            delay: 5000
          });
          this.getMessages();
        }).catch(error => {
          this.toast.error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });
        });
      },
      recoverMessage(index, message) {
        axios.patch('/api/messages/' + message.id, {
          'recover': true,
        }).then((response) => {
          this.toast.success({
            title: 'Success!',
            message: 'Mensaje recuperado con éxito.',
            delay: 5000
          });
          this.getMessages();
        }).catch(error => {
          this.toast.error({
            title: 'Error!',
            message: error.response.data.message,
            delay: 5000
          });
        });
      }
    }  
  }
</script>

<style>
  @media (min-width: 640px) {
  table {
    display: inline-table !important;
  }

  thead tr:not(:first-child) {
    display: none;
  }
  }
</style>

Ahora ya podrás archivar y recuperar mensajes. Con esto ya hemos terminado la aplicación:

Test funcionales de la aplicación

Ahora vamos a crear diversos tests para nuestra aplicación usando PHPUnit. Vamos a crear los tests que nos permitan comprobar si la manipulación de mensajes funciona, así que vamos a crear el archivo FeatureMessageTest.php en el directorio tests/Feature. El contenido inicial del archivo será este:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Route;

use App\Models\User;
use App\Models\Message;

class FeatureMessageTest extends TestCase
{
  use RefreshDatabase;

  // Tests
}

Usamos el trait RefreshDatabase para, tal y como su nombre indica, refrescar los datos de la base de datos antes de ejecutar los tests.

Testeando los mensajes recibidos

Vamos a testear el endpoint que nos permite obtener los mensajes de la bandeja de entrada. Para ello ejecutamos el seeder mediante la sentencia $this->seed, que ejecuta el DatabaseSeeder general, llenando de datos la base de datos:

public function test_get_messages(): void
{
  $this->seed();
  $user = User::first();

  $this->actingAs($user);
  $this->json('GET', 'api/messages', [
    'Accept' => 'application/json'
  ])->assertStatus(200)->assertJsonCount(10);
}

Lo que hemos hecho es obtener un usuario en la variable $user y luego, actuando como él mediante el método actingAs, llamamos al endpoint api/messages. El test terminará con éxito si obtenemos un código de estado 200 y si además el número de mensajes obtenidos es de 10, que es el número de mensajes que agrega el seeder.

Para ejecutar los tests, debes usar el siguiente comando:

php artisan test

Los tests que se ejecutan con éxito se mostrarán junto con la etiqueta PASS.

Testeando los mensajes enviados

Para testear los mensajes enviados seguiremos el mismo procedimiento:

public function test_get_sent_messages()
{
  $this->seed();
  $user = User::first();
    
  $this->actingAs($user);
  $this->json('GET', 'api/messages?mailbox=sent', [
    'Accept' => 'application/json'
  ])->assertStatus(200)->assertJsonCount(10);
}

Testeando los mensajes eliminados

Seguiremos exactamente el mismo procedimiento con los mensajes eliminados:

public function test_get_deleted_essages()
{
  $this->seed();
  $user = User::first();

  $this->actingAs($user);
  $this->json('GET', 'api/messages?mailbox=archived', [
    'Accept' => 'application/json'
  ])->assertStatus(200)->assertJsonCount(6);
}

Testeando la creación de mensajes

Para testear la creación de un mensaje, enviamos una petición POST a la ruta api/messages:

public function test_post_message()
{
  $this->seed();
  $user = User::first();
  $toUser  = User::skip(1)->take(1)->first();
  
  $this->actingAs($user);
  $this->json('POST', 'api/messages', [
    'to' => $toUser->id,
    'subject' => 'Test',
    'content' => 'TestContent'
  ])->assertStatus(201);
}

En este caso hemos obtenido el segundo usuario de la base de datos como destinario mediante la sentencia User::skip(1)->take(1)->first().

En el caso de que falte algún campo requerido, entonces el código de la respuesta debe ser el 400:

public function test_incomplete_post_message()
{
  $this->seed();
  $user = User::first();
  $toUser  = User::skip(1)->take(1)->first();
        
  $this->actingAs($user);
  $this->json('POST', 'api/messages', [
    'to' => $toUser->id,
    'subject' => 'Test',
  ])->assertStatus(400);
}

Testeando la actualización de mensajes

Vamos a establecer un mensaje como leído:

public function test_patch_message()
{
  $this->seed();
  $user = User::first();

  $this->actingAs($user);
  $message = Message::where(['user_id' => $user->id, 'to' => $user->id])->first();

  $this->json('PATCH', 'api/messages/' . $message->id, [
    'read' => true
  ])->assertStatus(204);
}

Si intentamos actualizar un mensaje de otro usuario, el código de respuesta debe ser el 401:

public function test_unauthorized_patch_message()
{
  $this->seed();
  $user = User::first();
  $fakeUser = User::skip(1)->take(1)->first();

  $this->actingAs($user);
  $message = Message::where(['user_id' => $user->id, 'to' => $user->id])->first();

  $this->actingAs($fakeUser);
  $this->json('PATCH', 'api/messages/' . $message->id, [
    'read' => true
  ])->assertStatus(401);
}

Testeando la eliminación de mensajes

Sencillamente eliminamos el primer mensaje que encontremos del usuario:

public function test_delete_message()
{
  $this->seed();
  $user = User::first();

  $this->actingAs($user);
  $message = Message::where(['user_id' => $user->id, 'to' => $user->id])->first();

  $this->json('DELETE', 'api/messages/' . $message->id)->assertStatus(200);
}

Si un usuario intenta eliminar el mensaje de otro usuario, el test debe fallar:

public function test_unauthorized_delete_message()
{
  $this->seed();
  $user = User::first();
  $fakeUser = User::skip(1)->take(1)->first();

  $this->actingAs($fakeUser);
  $message = Message::where([
    'user_id' => $user->id,
    'to' => $user->id
  ])->first();

   this->json('DELETE', 'api/messages/' . $message->id)->assertStatus(401);
}

Y con esto ya hemos terminado los tests funcinales, aunque realmente podríamos haber incluido más:

Conclusiones finales

Ha llevado tiempo, pero ya hemos terminado el tutorial. En este tutorial has realizado una aplicación bastante completa con Laravel.

Has creado migraciones de la base de datos, seeders factorías, tests, modelos, controladores y mucho más. Además, hemos creado componentes con Vue y hemos gestionado su comunicación con Laravel y también entre los propios componentes mediante eventos.

En caso de que quieras ver cómo sería realizar esta misma App usando Laravel Mix, puedes echarle un ojo a esta otra versión, que la que he usado Vue2 y Laravel 8 junto con Laravel Mix.

Esto ha sido todo. Espero que te haya resultado útil.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”