Developing a Simple Contact Form CMS (Content Management System) Using Laravel, Integrating API, and Docker

Introduction

💡

This article has a related root article:

Prerequisites


Step 1: Install Laravel

First, you need to install Laravel globally using Composer if you haven't already.

composer global require laravel/installer

Ensure that the Composer global bin directory is in your system's PATH.


Step 2: Create a New Laravel Project

Create a new Laravel project named myapp.

laravel new cms-contact-form


Step 3: Navigate to the Project Directory

cd cms-contact-form

Step 4: Update Session Driver

Since this application doesn't use a database and relies only on APIs, we need to update the session driver in the .env file. Change only on SESSION_DRIVER become file.

....

SESSION_DRIVER=file

...

Step 5: Create a Controller

Generate a new controller named ContactController to handle the form submission.

php artisan make:controller ContactController

Then edit the controller code:


Step 6: Create a Service

Generate a new service ContactService to handle the API call.


Step 7: Update Config

Add contacts_api.base_uri config key to config/service.php. This required by ContactService to get API base URI.

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Third Party Services
    |--------------------------------------------------------------------------
    |
    | This file is for storing the credentials for third party services such
    | as Mailgun, Postmark, AWS and more. This file provides the de facto
    | location for this type of information, allowing packages to have
    | a conventional file to locate the various service credentials.
    |
    */

    'postmark' => [
        'token' => env('POSTMARK_TOKEN'),
    ],

    'ses' => [
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    ],

    'resend' => [
        'key' => env('RESEND_KEY'),
    ],

    'slack' => [
        'notifications' => [
            'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
            'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
        ],
    ],

    'contacts_api' => [
        'base_uri' => env('API_CONTACT_FORM_BASE_URI', 'http://localhost:8080/contacts'),
    ],

];

Step 8: Set Up Routes

Open the routes/web.php file and add the following routes:

<?php

/**
 * Author: Tri Wicaksono
 * Website: https://triwicaksono.com
 */

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ContactController;

// Redirect the root URL to the contacts index page
Route::get('/', function () {
    return redirect()->route('contacts.index');
});

// Define a resource route for the ContactController
// This automatically sets up standard routes for index, create, store, show, edit, update, and destroy actions
Route::resource('contacts', ContactController::class);

Step 9: Create View

View structure like this:

Create a new file and add the following content:

Layouts View

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CMS Contact Form</title>
    
    <!-- Correct Tailwind CSS CDN Link -->
    <script src="https://cdn.tailwindcss.com"></script>
    
    <!-- Optional: Include Alpine.js for interactivity if needed -->
    <!-- <script src="//unpkg.com/alpinejs" defer></script> -->
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">

    <!-- Navbar -->
    @include('partials.navbar')

    <!-- Main Content -->
    <main class="flex-1 container mx-auto p-4">
        @yield('content')
    </main>

    <!-- Footer -->
    @include('partials.footer')

    <!-- Scripts -->
    <!-- No need for app.js since we're using CDN for Tailwind -->
</body>
</html>

Partials View

resources/views/partials/errors.blade.php
@if ($errors->any())
    <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
        <ul class="list-disc list-inside">
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

@if ($message = Session::get('error'))
    <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
        {{ $message }}
    </div>
@endif

resources/views/partials/flash.blade.php
@if ($message = Session::get('success'))
    <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
        {{ $message }}
    </div>
@endif

resources/views/partials/footer.blade.php
<footer class="bg-[#2A4D7D] text-white py-4 mt-auto">
    <div class="container mx-auto text-center">
        <p>
            &copy; 2024 - 
            <a href="https://triwicaksono.com/?utm_source=labs&utm_medium=demo-projects&utm_campaign=cms-contact-form" 
               target="_blank"
               class="text-blue-400 hover:underline">
               Tri Wicaksono
            </a>
        </p>
    </div>
</footer>

resources/views/partials/navbar.blade.php
<nav class="bg-[#2A4D7D] p-4">
    <div class="container mx-auto">
        <a href="{{ route('contacts.index') }}" class="text-white text-xl font-bold">CMS Contact Form</a>
    </div>
</nav>

Contacts View

resources/views/contacts/create.blade.php
@extends('layouts.app')

@section('content')
    <h1 class="text-3xl font-bold mb-6 text-center">Create New Contact</h1>

    @include('partials.errors')

    <form 
        action="{{ route('contacts.store') }}" 
        method="POST" 
        class="max-w-lg mx-auto bg-white p-6 rounded shadow-md"
        onsubmit="return confirm('Are you sure to create this contact?');"
    >
        @csrf
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Name</label>
            <input type="text" name="name" value="{{ old('name') }}" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
        </div>
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Email</label>
            <input type="email" name="email" value="{{ old('email') }}" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
        </div>
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Phone</label>
            <input type="text" name="phone" value="{{ old('phone') }}" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
        </div>
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Message</label>
            <textarea name="message" class="w-full h-32 resize-y border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>{{ old('message') }}</textarea>
        </div>
        <div class="flex justify-between">
            <a href="{{ route('contacts.index') }}" class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">Cancel</a>
            <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Submit</button>
        </div>
    </form>
@endsection
resources/views/contacts/edit.blade.php
@extends('layouts.app')

@section('content')
    <h1 class="text-3xl font-bold mb-6 text-center">Edit Contact</h1>

    @include('partials.errors')

    <form 
        action="{{ route('contacts.update', $contact['id']) }}" 
        method="POST" 
        class="max-w-lg mx-auto bg-white p-6 rounded shadow-md"
        onsubmit="return confirm('Are you sure to update this contact?');"
    >
        @csrf
        @method('PUT')
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Name</label>
            <input type="text" name="name" value="{{ old('name', $contact['name']) }}" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
        </div>
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Email</label>
            <input type="email" name="email" value="{{ old('email', $contact['email']) }}" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
        </div>
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Phone</label>
            <input type="text" name="phone" value="{{ old('phone', $contact['phone']) }}" class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
        </div>
        <div class="mb-4">
            <label class="block mb-2 font-semibold">Message</label>
            <textarea name="message" class="w-full h-32 resize-y border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>{{ old('message', $contact['message']) }}</textarea>
        </div>
        <div class="flex justify-between">
            <a href="{{ route('contacts.index') }}" class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">Cancel</a>
            <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Update</button>
        </div>
    </form>
@endsection
resources/views/contacts/index.blade.php
@extends('layouts.app')

@section('content')
    <h1 class="text-3xl font-bold mb-6 text-center">All Contacts</h1>

    @include('partials.flash')

    @include('partials.errors')

    <div class="flex justify-end mb-4">
        <a href="{{ route('contacts.create') }}" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Add Contact</a>
    </div>

    <div class="overflow-x-auto">
        <table class="min-w-full bg-white shadow-md rounded-lg overflow-hidden">
            <thead class="bg-blue-700 text-white">
                <tr>
                    <th class="py-3 px-4">ID</th>
                    <th class="py-3 px-4">Name</th>
                    <th class="py-3 px-4">Email</th>
                    <th class="py-3 px-4">Phone</th>
                    <th class="py-3 px-4">Message</th>
                    <th class="py-3 px-4">Created At</th>
                    <th class="py-3 px-4">Updated At</th>
                    <th class="py-3 px-4">Actions</th>
                </tr>
            </thead>
            <tbody>
                @forelse ($contacts as $contact)
                    <tr class="text-center border-t hover:bg-gray-50">
                        <td class="py-2 px-4">{{ $contact['id'] }}</td>
                        <td class="py-2 px-4">{{ $contact['name'] }}</td>
                        <td class="py-2 px-4">{{ $contact['email'] }}</td>
                        <td class="py-2 px-4">{{ $contact['phone'] }}</td>
                        <td class="py-2 px-4">
                            {{ \Illuminate\Support\Str::limit($contact['message'], 50) }}
                            @if (strlen($contact['message']) > 50)
                                <a href="{{ route('contacts.show', $contact['id']) }}" class="text-blue-500 hover:underline">Read More</a>
                            @endif
                        </td>
                        <td class="py-2 px-4">{{ $contact['created_at'] }}</td>
                        <td class="py-2 px-4">{{ $contact['updated_at'] }}</td>
                        <td class="py-2 px-4 flex justify-center space-x-2">
                            <a href="{{ route('contacts.show', $contact['id']) }}" class="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600">View</a>
                            <a href="{{ route('contacts.edit', $contact['id']) }}" class="bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">Edit</a>
                            <form action="{{ route('contacts.destroy', $contact['id']) }}" method="POST" onsubmit="return confirm('Are you sure to delete this contact?');">
                                @csrf
                                @method('DELETE')
                                <button type="submit" class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">Delete</button>
                            </form>
                        </td>
                    </tr>
                @empty
                    <tr>
                        <td colspan="8" class="py-4 text-center text-gray-500">No contacts found.</td>
                    </tr>
                @endforelse
            </tbody>
        </table>
    </div>
@endsection
resources/views/contacts/show.blade.php
@extends('layouts.app')

@section('content')
    <h1 class="text-3xl font-bold mb-6 text-center">Contact Details</h1>

    <div class="max-w-lg mx-auto bg-white p-6 rounded shadow-md">
        <p class="mb-2"><strong>ID:</strong> {{ $contact['id'] }}</p>
        <p class="mb-2"><strong>Name:</strong> {{ $contact['name'] }}</p>
        <p class="mb-2"><strong>Email:</strong> {{ $contact['email'] }}</p>
        <p class="mb-2"><strong>Phone:</strong> {{ $contact['phone'] }}</p>
        <p class="mb-4"><strong>Message:</strong> {{ $contact['message'] }}</p>
        <p class="mb-2"><strong>Created At:</strong> {{ $contact['created_at'] }}</p>
        <p class="mb-2"><strong>Updated At:</strong> {{ $contact['updated_at'] }}</p>
        <div class="flex justify-end space-x-2">
            <a href="{{ route('contacts.index') }}" class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">Back</a>
            <a href="{{ route('contacts.edit', $contact['id']) }}" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Edit</a>
            <form action="{{ route('contacts.destroy', $contact['id']) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this contact?');">
                @csrf
                @method('DELETE')
                <button type="submit" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">Delete</button>
            </form>
        </div>
    </div>
@endsection

Step 10: Run the Laravel Application

Start the Laravel development server.

php artisan serve

By default, the application will be served at http://127.0.0.1:8000.


Step 11: Test the Application

  1. Open your web browser and navigate to http://127.0.0.1:8000.
  1. You should see the contact form CMS page.
  1. You can test all functionalities from
    1. Add Contact
    1. View Contact
    1. Edit Contact
    1. Delete Contact.

Troubleshooting

Feel free to customize and expand upon this basic application to suit your needs!


Step 12: Dockerizing the Application

Containerizing your application ensures consistency across different environments and simplifies deployment. We will using separate containers for Laravel (PHP-FPM) and Nginx to enhances scalability, maintainability, and flexibility.

Why are we using separate containers for Laravel (PHP-FPM) and Nginx when dockerizing this project?

Create Nginx & PHP Configurations

The nginx.conf and local.ini files provide necessary configurations for Nginx and PHP, ensuring that your application runs smoothly with the desired settings within the Docker environment.

Dockerfile

Contains instructions to build the Docker image for the Contact Form CMS. It typically includes steps like setting the base image, installing dependencies, copying source code, building the application, and specifying the command to run the application.

.dockerignore

Specifies files and directories to exclude from the Docker build context, optimizing build performance and enhancing security by preventing unnecessary or sensitive files from being included in the Docker image.

Docker Compose

Orchestrates multi-container Docker applications, managing services like the API and the database seamlessly. It defines how containers should be built, their configurations, networks, and volumes.

Step 13: Building and Running the Application

With the configuration and setup complete, it's time to build and run your application.

Building the Docker Image

Navigate to the project root directory and build the Docker image using Docker Compose.

docker compose build

Run multiple Docker Container

After that run container using this command:

docker compose up -d

Verifying Container Status

Ensure all containers are up and running without issues.

docker compose ps

You should see application marked as Up.

Verify the Deployment

Open your browser and navigate to http://localhost:8081 to see your deployed Contact Form CMS.


Suggested Next Steps for Improvement

Conclusion

You've now created a Laravel web application with a single index page that consumes an external API. The application features a responsive design with a grey background and handles form submission by sending a POST request to the API endpoint. It displays the response from the API directly on the page.