Laravel Docker and Breeze Part 1 Tutorial
me@grafxflow

Written by me@grafxflow

21 Jun, 2023

0

3,525

Laravel, Docker, Breeze and VueJS easy startup

Updated: 3 April 2024.

As I am sure everybody is aware setup with Laravel can be fairly easy with the help of dependencies. So for this tutorial we will be setting up a basic user login and dashboard, then adding a CRUD user example.

  • Laravel 11 (php 8.2+)
  • Laravel/breeze
  • Laravel/sail
  • Tailwind css
  • Inertia.js
  • Vue.js
  • Docker link

Here are the steps of the tutorial.

  1. Create a Laravel app
  2. Create database settings and docker via sail
  3. Add user and password
  4. Create the Login and Dashboard pages
  5. Add a table to list the current Active Users
  6. Implement the Inertia Link tag
  7. Add the Edit User page
  8. Add the Delete User function and Modal
  9. Create a User page and functions

Source code can be found at github - here is the link.

Step 1. Create a Laravel app

So lets start by creating a new project by first choosing your default directory.

cd /Users/[your-username]/Sites 

Then setup Laravel in a folder named laravel-inertia-vue-app.

composer create-project laravel/laravel laravel-inertia-vue-app
cd /Users/[your-username]/Sites/laravel-inertia-vue-app

Step 2. Create database settings and docker via sail

NOTE: Make sure the Docker desktop app is running first

For this project we will be integrating Docker via sail.

composer require laravel/sail --dev

Then let sail setup the Docker instance.

php artisan sail:install

For now choose the default 0 for mysql - we can add additional services to it later.

 Which services would you like to install? [mysql]:
  [0] mysql
  [1] pgsql
  [2] mariadb
  [3] redis
  [4] memcached
  [5] meilisearch
  [6] minio
  [7] mailpit
  [8] selenium
  [9] soketi
 > 

NOTE: This initial setup will take longer

Let's get the docker instance up and running.

./vendor/bin/sail up -d 

You shoud be able to make sure it's working.

http://localhost

And see the following default page.

Now by default you should already have the .env created and keys generated. So now lets migrate the initial setup.

./vendor/bin/sail php artisan migrate

Step 3. Add user and password

We now need to add add a user to login into the dashboard - so let's go back in the terminal start adding it with tinker.

./vendor/bin/sail php artisan tinker

Add whatever details you want to use.

$user = new App\Models\User();
$user->password = Hash::make('the-password-of-choice');
$user->email = 'the-email@example.com';
$user->name = 'Admin User';
$user->save();

exit

Step 4. Create the Login and Dashboard pages

Now we need to add the Login and Dashboard pages so for this we will use the startup kit via Laravel/breeze.

composer require laravel/breeze --dev

For this setup we will be integrating vue.js for the frontend framework (but it could be react etc) and running the below terminal input. This should add inertiajs and tailwind by default plus add inertia's default middleware to the routes.

./vendor/bin/sail php artisan breeze:install vue
npm install
npm run dev

Once done you will notice Vite will startup in the terminal.

  VITE v5.3.3  ready in 409 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

   LARAVEL v11.14.0  plugin v1.0.4

  ➜  APP_URL: http://localhost

So lets go back to the browser. You should notice the added navigation for 'Login In' and 'Register'.

So login to see if your user details work.

... if so you will be redirected to the dashboard.

Step 5. Add a table to list the current Active Users

Now lets have some fun and add some custom pages for CRUD integration for the Active Users.

You should have the following directory 'resources/js/Pages/', so add a new folder called Users - 'resources/js/Pages/Users/'.

Next create the controller for new Users section.

./vendor/bin/sail php artisan make:controller UsersController

This should have created the following 'app/Http/Controllers/UsersController.php' so now add the following content.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use Inertia\Inertia;
use Inertia\Response;

class UsersController extends Controller
{
    /**
     * Display the active users.
    */
    public function index(Request $request): Response
    {
        return Inertia::render('Users/Index', [
            'users' => User::all(),
        ]);
    }
}

We also want to limit this section to logged in users so make the following change to the web routes file 'routes/web.php'.

<?php

use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

// Users CRUD Controller
use App\Http\Controllers\UsersController;

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
    
    // Users CRUD routes
    Route::get('/users', [UsersController::class, 'index'])->name('users.index');
});

Also lets add the Active Users link page in the navigation so edit the 'resources/js/Layouts/AuthenticatedLayout.vue'.

<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
    Dashboard
</NavLink>
<!-- Add this below the Dashboard Link -->
<NavLink :href="route('users.index')" :active="route().current('users.index')">
    Active Users
</NavLink>

Then create a page to show the users index 'resources/js/Pages/Users/Index.vue'. I have added a blank Actions column for now which will contain the 'edit' and 'delete' links.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';

// Used to access Users Object from UsersController
defineProps({
  users: {
    type: Object,
  }
});
</script>

<template>
  <Head title="Users Index" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">Users Index</h2>
    </template>

    <div class="py-12">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
        <div class="overflow-hidden bg-white shadow-md sm:rounded-lg">
          <div class="flex flex-col">
            <div class="overflow-x-auto -my-2 sm:-mx-6 lg:-mx-8">
              <div class="inline-block py-2 min-w-full align-middle sm:px-6 lg:px-8">
                <div class="overflow-hidden border-b border-gray-200 shadow sm:rounded-lg">

                  <table class="min-w-full divide-y divide-gray-200 table-fixed">
                    <thead class="bg-indigo-500">
                      <tr>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('id')">
                            ID
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('email')">
                            Email
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('name')">
                            Name
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between">
                            Actions
                          </span>
                        </th>
                      </tr>
                    </thead>
                    <tbody class="bg-white divide-y divide-gray-200">
                      <!-- Start looping the Users Object -->
                      <tr v-for="(user, index) in users" :key="user.id">
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.id }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.email }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.name }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          <!-- Start Actions -->
                          <!-- End Actions -->
                        </td>
                      </tr>
                      <!-- End looping the Users Object -->
                    </tbody>
                  </table>

                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

  </AuthenticatedLayout>
</template>

You should now see both the navigation and the Users Index page.

Step 6. Implement the Inertia Link tag

For the next few stages I will be using the 'inertia-link' tag so lets add this universally.

Open the following file 'resources/js/app.js' and import the Component from inertiajs/vue3, then make it generic across the app.

import './bootstrap';
import '../css/app.css';

import { createApp, h } from 'vue';
// Use Link Component from InertiaJS/Vue
import { createInertiaApp, Link } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue, Ziggy)
            // Add the Generic InertiaLink
            .component('InertiaLink', Link)
            .mount(el);
    },
    progress: {
        color: '#4B5563',
    },
});

Step 7. Add the Edit User page

Now lets add edit button to the table 'resources/js/Pages/Users/Index.vue'.

<!-- Start Actions -->
<!-- Start Edit User -->
    <inertia-link
        :href="route('users.edit', { user: user.id })"
        class="mr-1 mb-1 px-4 py-2 uppercase text-sm leading-4 border rounded-md hover:bg-white focus:border-indigo-500 focus:text-indigo-500"
    >
     Edit
    </inertia-link>
<!-- End Edit User -->
<!-- End Actions -->

Add then add the Edit User page.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';

const props = defineProps({
    user: {
    type: Object,
    default: () => ({}),
  },
});

const form = useForm({
  userId: props.user.id,
  name: props.user.name,
  email: props.user.email,
  password: '',
  password_confirmation: '',
});

const submit = () => {
  form.patch(route('users.update', {'user': props.user.id}), {
    onFinish: () => form.reset(),
  });
};
</script>

<template>
  <Head title="User Edit" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">User Edit</h2>
    </template>

    <div class="py-12">
      <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
        <div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">

          <form @submit.prevent="submit">

            <div class="mt-4">
              <InputLabel for="name" value="Name" />

              <TextInput
                id="name"
                type="text"
                class="mt-1 block w-full"
                v-model="form.name"
                required
                autofocus
                autocomplete="name"
              />

              <InputError class="mt-2" :message="form.errors.name" />
            </div>

            <div class="mt-4">
              <InputLabel for="email" value="Email" />

              <TextInput
                id="email"
                type="email"
                class="mt-1 block w-full"
                v-model="form.email"
                required
                autocomplete="username"
              />

              <InputError class="mt-2" :message="form.errors.email" />
            </div>

            <div class="mt-4">
              <InputLabel for="password" value="Password" />

              <TextInput
                id="password"
                type="password"
                class="mt-1 block w-full"
                v-model="form.password"
                autocomplete="new-password"
              />

              <InputError class="mt-2" :message="form.errors.password" />
            </div>

            <div class="mt-4">
              <InputLabel for="password_confirmation" value="Confirm Password" />

              <TextInput
                id="password_confirmation"
                type="password"
                class="mt-1 block w-full"
                v-model="form.password_confirmation"
                autocomplete="new-password"
              />

              <InputError class="mt-2" :message="form.errors.password_confirmation" />
            </div>

            <div class="flex items-center justify-end mt-4">
              <PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                Edit User
              </PrimaryButton>
            </div>
          </form>

        </div>
      </div>
    </div>
  </AuthenticatedLayout>
</template>

Now before adding the 'Update' function to the UserController lets set up the request rules. So in the terminal input.

php artisan make:request UserUpdateRequest 

And edit the following file 'app/Http/Requests/UserUpdateRequest.php'.

<?php

namespace App\Http\Requests;

use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UserUpdateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'lowercase', 'email:filter', 'max:255', Rule::unique(User::class)->ignore($this->user)],
            'password' => ['sometimes', 'nullable', 'confirmed', 'min:8'],
            'current_password' => ['sometimes', 'required_with:password', 'same:password'],
        ];
    }
}

Now back to the controller 'app/Http/Controllers/UsersController.php' add the following.

use Illuminate\Http\RedirectResponse;
use App\Http\Requests\UserUpdateRequest;
use Illuminate\Support\Facades\Redirect;

....

    /**
     * Edit the user account.
     */
    public function edit(Request $request): Response
    {
        return Inertia::render('Users/Edit', [
            'user' => User::find($request->user),
        ]);
    }

    /**
     * Update the user information.
     */
    public function update(UserUpdateRequest $request): RedirectResponse
    {
        // Removes password field if it's null
        if (!$request->password) {
            unset($request['password']);
        }

        // Update the User details
        User::find($request->user)->update($request->all());
    
        // Redirect to the User Index page
        return Redirect::route('users.index');
    }

Then the web routes 'routes/web.php'.

// Users CRUD routes
Route::get('/users', [UsersController::class, 'index'])->name('users.index');
Route::get('/users/{user}/edit', [UsersController::class, 'edit'])->name('users.edit');
Route::patch('/users/{user}/update', [UsersController::class, 'update'])->name('users.update');

Here is the edit page.

Step 8. Add the Delete User function and Modal

Now we want to add a Delete User function, but for this we want to make it a soft delete - so edit 'app/Models/User.php'.

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
// Use SoftDeletes
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
    // Add SoftDeletes
    use HasApiTokens, HasFactory, Notifiable, SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}

And add the column in a migration.

php artisan make:migration add_soft_delete_to_users_table --table=users

And add the following to the new migration file.

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
};

Then run the migration.

./vendor/bin/sail php artisan migrate

Lets go back to the users index page 'resources/js/Pages/Users/Index.vue' and add the delete button, modal and function.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import DangerButton from '@/Components/DangerButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import Modal from '@/Components/Modal.vue';
import { Head, useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';

// Used to access Users Object from UsersController
defineProps({
  users: {
    type: Object,
  }
});

const confirmingUserDelete = ref(false);
const userId = ref(null);

const form = useForm({});

const confirmUserDelete = (id) => {
  userId.value = id;
  confirmingUserDelete.value = true;
};

const deleteUser = () => {
  form.delete(route('users.delete', {'user': userId.value}), {
    preserveScroll: true,
    onSuccess: () => closeModal(),
    onFinish: () => form.reset(),
  });
};

const closeModal = () => {
  confirmingUserDelete.value = false;
  form.reset();
};
</script>

<template>
  <Head title="Users Index" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">Users Index</h2>
    </template>

    <div class="py-12">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
        <div class="overflow-hidden bg-white shadow-md sm:rounded-lg">
          <div class="flex flex-col">
            <div class="overflow-x-auto -my-2 sm:-mx-6 lg:-mx-8">
              <div class="inline-block py-2 min-w-full align-middle sm:px-6 lg:px-8">
                <div class="overflow-hidden border-b border-gray-200 shadow sm:rounded-lg">

                  <table class="min-w-full divide-y divide-gray-200 table-fixed">
                    <thead class="bg-indigo-500">
                      <tr>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('id')">
                            ID
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('email')">
                            Email
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('name')">
                            Name
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between">
                            Actions
                          </span>
                        </th>
                      </tr>
                    </thead>
                    <tbody class="bg-white divide-y divide-gray-200">
                      <!-- Start looping the Users Object -->
                      <tr v-for="(user, index) in users" :key="user.id">
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.id }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.email }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.name }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          <!-- Start Actions -->
                          <!-- Start Edit User -->
                          <inertia-link
                            :href="route('users.edit', { user: user.id })"
                            class="mr-1 mb-1 px-4 py-2 uppercase text-sm leading-4 border rounded-md hover:bg-white focus:border-indigo-500 focus:text-indigo-500"
                          >
                            Edit
                          </inertia-link>
                          <!-- End Edit User -->
                          <!-- Start Soft Delete User -->
                          <DangerButton @click="confirmUserDelete(user.id)">
                            Delete
                          </DangerButton>
                          <!-- End Soft Delete User -->
                          <!-- End Actions -->
                        </td>
                      </tr>
                      <!-- End looping the Users Object -->
                    </tbody>
                  </table>

                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <Modal :show="confirmingUserDelete" @close="closeModal">
      <div class="p-6">
        <h2 class="text-lg font-medium text-gray-900">
          Are you sure you want to delete the user?
        </h2>
        <div class="mt-6 flex justify-end">
          <SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
          <DangerButton
            class="ml-3"
            :class="{ 'opacity-25': form.processing }"
            :disabled="form.processing"
            @click="deleteUser(id)"
          >
            Delete User
          </DangerButton>
        </div>
      </div>
    </Modal>

  </AuthenticatedLayout>
</template>

Then add the delete route to the web routes 'routes/web.php'.

// Users CRUD routes
Route::get('/users', [UsersController::class, 'index'])->name('users.index');
Route::get('/users/{user}/edit', [UsersController::class, 'edit'])->name('users.edit');
Route::patch('/users/{user}/update', [UsersController::class, 'update'])->name('users.update');
Route::delete('/users/{user}/delete', [UsersController::class, 'delete'])->name('users.delete');

Now lets add the delete function to the UsersController.

  /**
   * Delete the user account.
   */
  public function delete(Request $request): RedirectResponse
  {
    User::find($request->user)->delete();

    return Redirect::route('users.index');
  }

Here is now what should appear on the User Index page.

Step 8. Create a User page and functions

We also want the option to be able to add a new user, so lets start by creating the add user page.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';

const form = useForm({
  name: '',
  email: '',
  password: '',
  password_confirmation: '',
});

const submit = () => {
  form.post(route('users.store'), {
    onFinish: () => form.reset(),
  });
};
</script>

<template>
  <Head title="User Create" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">User Create</h2>
    </template>

    <div class="py-12">
      <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
        <div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">

          <form @submit.prevent="submit">
            <div>
              <InputLabel for="name" value="Name" />

              <TextInput
                id="name"
                type="text"
                class="mt-1 block w-full"
                v-model="form.name"
                required
                autofocus
                autocomplete="name"
              />

              <InputError class="mt-2" :message="form.errors.name" />
            </div>

            <div class="mt-4">
              <InputLabel for="email" value="Email" />

              <TextInput
                id="email"
                type="email"
                class="mt-1 block w-full"
                v-model="form.email"
                required
                autocomplete="email"
              />

              <InputError class="mt-2" :message="form.errors.email" />
            </div>

            <div class="mt-4">
              <InputLabel for="password" value="Password" />

              <TextInput
                id="password"
                type="password"
                class="mt-1 block w-full"
                v-model="form.password"
                required
                autocomplete="new-password"
              />

              <InputError class="mt-2" :message="form.errors.password" />
            </div>

            <div class="mt-4">
              <InputLabel for="password_confirmation" value="Confirm Password" />

              <TextInput
                id="password_confirmation"
                type="password"
                class="mt-1 block w-full"
                v-model="form.password_confirmation"
                required
                autocomplete="new-password"
              />

              <InputError class="mt-2" :message="form.errors.password_confirmation" />
            </div>

            <div class="flex items-center justify-end mt-4">
              <PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                Create User
              </PrimaryButton>
            </div>
          </form>

        </div>
      </div>
    </div>
  </AuthenticatedLayout>
</template>

Then update the web routes in order to store the new user.

// Users CRUD routes
Route::get('/users', [UsersController::class, 'index'])->name('users.index');
Route::get('/users/{user}/edit', [UsersController::class, 'edit'])->name('users.edit');
Route::patch('/users/{user}/update', [UsersController::class, 'update'])->name('users.update');
Route::delete('/users/{user}/delete', [UsersController::class, 'delete'])->name('users.delete');
Route::get('/users/create', [UsersController::class, 'create'])->name('users.create');
Route::post('/users/store', [UsersController::class, 'store'])->name('users.store');

Add the request rules for the User Store/Create, so input.

php artisan make:request UserStoreRequest 

Then amend the created file 'app/Http/Requests/UserStoreRequest.php'.

<?php

namespace App\Http\Requests;

use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;

class UserStoreRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            'name' => 'required', 'string', 'max:255',
            'email' => 'required', 'string', 'email', 'max:255', 'unique:' . User::class,
            'password' => ['required', 'string', 'confirmed', 'min:8'],
        ];
    }
}

Now add the Store and Create functions in the UsersController.

use App\Http\Requests\UserStoreRequest;
use Illuminate\Support\Facades\Hash;

...

    /**
     * Create the user account.
     */
    public function create(): Response
    {
        return Inertia::render('Users/Create');
    }   
   
    /**
     * Store the user account.
     */
    public function store(UserStoreRequest $request): RedirectResponse
    {
        // Store the User details
        $request->user()->create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
        
        return Redirect::route('users.index');
    }

And now add a Create User button on the users index page.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import DangerButton from '@/Components/DangerButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import Modal from '@/Components/Modal.vue';
import { Head, useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';

// Used to access Users Object from UsersController
defineProps({
  users: {
    type: Object,
  }
});

const confirmingUserDelete = ref(false);
const userId = ref(null);

const form = useForm({});

const confirmUserDelete = (id) => {
  userId.value = id;
  confirmingUserDelete.value = true;
};

const deleteUser = () => {
  form.delete(route('users.delete', {'user': userId.value}), {
    preserveScroll: true,
    onSuccess: () => closeModal(),
    onFinish: () => form.reset(),
  });
};

const closeModal = () => {
  confirmingUserDelete.value = false;
  form.reset();
};
</script>

<template>
  <Head title="Users Index" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">Users Index</h2>
    </template>

    <div class="py-12">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">

        <inertia-link
          :href="route('users.create')"
          class="inline-flex items-center my-4 px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150"
        >
            Create User
        </inertia-link>

        <div class="overflow-hidden bg-white shadow-md sm:rounded-lg">
          <div class="flex flex-col">
            <div class="overflow-x-auto -my-2 sm:-mx-6 lg:-mx-8">
              <div class="inline-block py-2 min-w-full align-middle sm:px-6 lg:px-8">
                <div class="overflow-hidden border-b border-gray-200 shadow sm:rounded-lg">

                  <table class="min-w-full divide-y divide-gray-200 table-fixed">
                    <thead class="bg-indigo-500">
                      <tr>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('id')">
                            ID
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('email')">
                            Email
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('name')">
                            Name
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between">
                            Actions
                          </span>
                        </th>
                      </tr>
                    </thead>
                    <tbody class="bg-white divide-y divide-gray-200">
                      <!-- Start looping the Users Object -->
                      <tr v-for="(user, index) in users" :key="user.id">
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.id }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.email }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.name }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          <!-- Start Actions -->
                          <!-- Start Edit User -->
                          <inertia-link
                            :href="route('users.edit', { user: user.id })"
                            class="mr-1 mb-1 px-4 py-2 uppercase text-sm leading-4 border rounded-md hover:bg-white focus:border-indigo-500 focus:text-indigo-500"
                          >
                            Edit
                          </inertia-link>
                          <!-- End Edit User -->
                          <!-- Start Soft Delete User -->
                          <DangerButton @click="confirmUserDelete(user.id)">
                            Delete
                          </DangerButton>
                          <!-- End Soft Delete User -->
                          <!-- End Actions -->
                        </td>
                      </tr>
                      <!-- End looping the Users Object -->
                    </tbody>
                  </table>

                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <Modal :show="confirmingUserDelete" @close="closeModal">
      <div class="p-6">
        <h2 class="text-lg font-medium text-gray-900">
          Are you sure you want to delete the user?
        </h2>
        <div class="mt-6 flex justify-end">
          <SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
          <DangerButton
            class="ml-3"
            :class="{ 'opacity-25': form.processing }"
            :disabled="form.processing"
            @click="deleteUser(id)"
          >
            Delete User
          </DangerButton>
        </div>
      </div>
    </Modal>

  </AuthenticatedLayout>
</template>

You will now have a Create User button and this Create User page.

In part 2 of the tutorial we will make more advanced functions especially with the Soft Delete for the Users.

Hope this part has been helpful!

Add comment

Smart Search

133 Following
50 Followers

me@grafxflow

Hull, United Kingdom

I am a Full-stack Developer who also started delving into the world of UX/UI Design a few years back. I blog and tweet to hopefully share a little bit of knowledge that can help others around the web. Thanks for stopping by!

Follow