How To Build a Links Landing Page in PHP with Laravel and Docker Compose

68k views

Introduction

Laravel is an open-source PHP framework that provides a set of tools and resources to build modern PHP applications. In this project-based tutorial series, you’ll build a Links Landing Page application with the Laravel framework, using a containerized PHP development environment managed by Docker Compose.

Landing Laravel Demo Application - Final Version

At the end, you’ll have a one-page website built with Laravel and managed via Artisan commands where you can share relevant links to an audience on social channels and presentations.

Prerequisites

To follow this series, you will need:

Once you have these prerequisites set up, you are ready to get started.

To get started, you’ll need to create a containerized environment able to execute PHP and Composer, the PHP dependency management tool. Then, you’ll be able to bootstrap the new Laravel application from scratch, without the need to have a local PHP environment installed on your local machine or development server.

In this guide, we’ll provide streamlined instructions on how to set this environment up based on our tutorial on How To Install Laravel with Docker Compose on Ubuntu 20.04. Please refer to that tutorial for more detailed instructions on each of the options used within the Docker Compose file that will be provided in this guide.

Create a new directory for your application in your home folder:

  • mkdir ~/landing-laravel
  • cd ~/landing-laravel

Next, you’ll create the docker-compose.yml file that will define the containerized environment. In this file, you’ll set up a service named app, which will be based on a custom Docker image built with a Dockerfile you’ll set up later on.

The build arguments user and uid, both defined in the docker-compose.yml file and used in the Dockerfile at build time, should be changed to reflect your own username and uid on your local machine or development server. To find out your current user’s uid, type:

  • echo $UID
Output
1000

The user and uid variables will be available at build time and will be used in the Dockerfile to create a new user in the app service with the same username and uid as your current system user on your local machine or development server. This will avoid permission and ownership issues when working with application files both from the container as well as from the host that executes Docker.

Create a new docker-compose.yml file using your text editor of choice. Here, we’re using nano:

  • nano docker-compose.yml

Copy the following content to this file, and don’t forget to replace the highlighted values with appropriate values depending on your own username and uid on the system that runs Docker:

~/landing-laravel/docker-compose.yml
version: "3.7"
services:
  app:
    build:
      args:
        user: sammy
        uid: 1000
      context: ./
      dockerfile: Dockerfile
    image: landing-app
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
    networks:
      - landing

networks:
  landing:
    driver: bridge

Save and close the file when you are done. If you are using nano, you can do that by pressing CTRL+X, then Y and ENTER to confirm.

Next, you’ll set up the Dockerfile that is referenced in the docker-compose.yml file, which will set up a custom image for the app service:

  • nano Dockerfile

This Dockerfile extends from the default php:7.4-fpm Docker image. It uses the user and uid variables to create a new user able to execute Artisan and Composer commands. It also installs a few PHP dependencies that are required by Laravel, and the Composer executable.

Copy the following content to your Dockerfile:

~/my-todo-list/Dockerfile
FROM php:7.4-fpm

# Arguments defined in docker-compose.yml
ARG user
ARG uid

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Create system user to run Composer and Artisan Commands
RUN useradd -G www-data,root -u $uid -d /home/$user $user
RUN mkdir -p /home/$user/.composer && \
    chown -R $user:$user /home/$user

# Set working directory
WORKDIR /var/www

USER $user

Save and close the file when you’re done. Next, you can bring your environment up with:

  • docker-compose up -d

This command will execute Docker Compose in detached mode, which means it will run in the background. The first time you bring an environment up with a custom image, Docker Compose will automatically build the image for you before creating the required containers. This might take a few moments to finish. You’ll see output similar to this:

Output
Creating network "landing-laravel_landing" with driver "bridge" Building app Step 1/11 : FROM php:7.4-fpm ---> fa37bd6db22a ... Step 10/11 : WORKDIR /var/www ---> Using cache ---> 769afd5d44d8 Step 11/11 : USER $user ---> Using cache ---> 841eb5852b69 Successfully built 841eb5852b69 Successfully tagged landing-app:latest WARNING: Image for service app was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. Creating landing-laravel_app_1 ... done

You can verify that your environment is up and running with:

  • docker-compose ps
Output
Name Command State Ports ------------------------------------------------------------------------ landing-laravel_app_1 docker-php-entrypoint php-fpm Up 9000/tcp

Once the app service is up, you can run Composer, the PHP dependency management tool, to bootstrap a new Laravel application. In order to do that, you’ll use docker compose exec to run commands on the app service, where PHP is installed.

The following command will use Docker Compose to execute composer create-project, which will bootstrap a fresh installation of Laravel based on the laravel/laravel package:

  • docker-compose exec app composer create-project laravel/laravel --prefer-dist application
Creating a "laravel/laravel" project at "./application"
Installing laravel/laravel (v8.4.0)
  - Downloading laravel/laravel (v8.4.0)
  - Installing laravel/laravel (v8.4.0): Extracting archive
Created project in /var/www/application
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
Lock file operations: 104 installs, 0 updates, 0 removals
…
Package manifest generated successfully.
71 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan key:generate --ansi
Application key set successfully.

This installation creates a new .env file based on the default .env.example file that comes with Laravel. The .env file contains database credentials and other sensitive application settings, and should be unique per environment where the app runs. You’ll come back to edit this file after you finish setting up the development environment.

Next, copy the application files to the same directory as the docker-compose.yml file, so that you can share Laravel’s environment variables file with Docker Compose. Then, you can remove the application directory created by Composer:

cp -rT application .
rm -rfv application

Your application is now bootstrapped, but you’ll need to include a couple services in the Docker Compose file in order to be able to access the app from a browser. An nginx service will serve the application using the Nginx web server, and a db service will host the application’s MySQL database.

First, bring your environment down with:

  • docker-compose down
Output
Stopping landing-laravel_app_1 ... done Removing landing-laravel_app_1 ... done Removing network landing-laravel_landing

This will remove all containers and networks associated with this environment. Before editing your docker-compose.yml file to add the new services, create a new directory to share configuration files with containers. You’ll need this to properly set up Nginx to handle the Laravel PHP application.

  • mkdir -p docker-compose/nginx

Next, create a new landing-laravel.conf file containing a custom Nginx server block. Later on, you’ll set up a volume to share this file within the nginx service container.

Open a new Nginx configuration file with:

  • nano docker-compose/nginx/landing-laravel.conf

The following server block configures Nginx to serve a Laravel application using an external service (app) to handle PHP code. Copy this content to your own Nginx configuration file:

docker-compose/nginx/landing-laravel.conf
server {
    listen 80;
    index index.php index.html;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/public;
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}

Save and close the file when you’re done.

Next, open your docker-compose.yml file:

  • nano docker-compose.yml

Include the following configuration for the nginx service, at the same level as the previously configured app service. This will create a new service based on the nginx:alpine image, and all requests on port 8000 of the host where Docker is running will be redirected to port 80 in the service container. In addition to the application files, you’ll also share a volume containing Nginx’s configuration file for a Laravel application:

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - 8000:80
    volumes:
      - ./:/var/www
      - ./docker-compose/nginx:/etc/nginx/conf.d/
    networks:
      - landing

Then, include the following configuration block for the db service. This will create a service based on the default MySQL 8 image, and pull in the values defined in Laravel’s environment file to set up database access:

  db:
    image: mysql:8
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
    networks:
      - landing

This is how your updated docker-compose.yml file should look like once you’re finished:

~/landing-laravel/docker-compose.yml
version: "3.7"
services:
  app:
    build:
      args:
        user: sammy
        uid: 1000
      context: ./
      dockerfile: Dockerfile
    image: landing-app
    restart: unless-stopped
    working_dir: /var/www/
    volumes:
      - ./:/var/www
    networks:
      - landing

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - 8000:80
    volumes:
      - ./:/var/www
      - ./docker-compose/nginx:/etc/nginx/conf.d/
    networks:
      - landing
  db:
    image: mysql:8
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
    networks:
      - landing

networks:
  landing:
    driver: bridge

Note: for more detailed information about containerizing Laravel environments, including explanations about shared volumes and networks, please refer to our full guide on How To Install Laravel with Docker Compose on Ubuntu 20.04.

Save and close the file when you’re done editing. Lastly, update your Laravel dot env file (.env) to point the MySQL database host configuration to the host where the MySQL service will be running, called db:

  • nano .env

The .env file that is automatically generated by Composer upon installation comes with some default values that you might want to change, such as the APP_NAME and the APP_URL. The database DB_HOST variable must be changed to point to the service where MySQL will be running, and you can reference it by its service name, as defined in the docker-compose.yml file. In this example, we’ve used db as name for the database service, so this will be available in the containerized network as a host named db.

Change your .env accordingly, using the following example as base. The highlighted values were updated here to reflect the state of the application under development:

~/landing-laravel/.env
APP_NAME=LandingLaravel
APP_ENV=local
APP_KEY=base64:ffYPNP8kPeQDf8gE/qh3kWjk59p6gFY66kCKhhKUa2w=
APP_DEBUG=true
APP_URL=http://localhost:8000

LOG_CHANNEL=stack
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=landing-db
DB_USERNAME=landing-user
DB_PASSWORD=dev-password

...

You don’t need to change any other sections of this file, but feel free to tweak to your specific use case.

Save and close the file when you’re done editing its contents.

You can now bring the updated environment up with:

  • docker-compose up -d
Output
Creating network "landing-laravel_landing" with driver "bridge" Creating landing-laravel_app_1 ... done Creating landing-laravel_db_1 ... done Creating landing-laravel_nginx_1 ... done

With the full environment up, you can now point your browser to localhost or your remote server’s IP address, on port 8000:

http://localhost:8000

If everything works as expected, you’ll see a page like this:

Laravel Landing Links - basic app

In the next part of this series, you’ll create a database migration to set up a links table.

Laravel database migrations allow developers to quickly bootstrap, destroy, and recreate an application’s database, without the need to log into the database console or run any SQL queries.

In this guide, you’ll create a database migration to set up the table where you’ll save the application links. In order to do that, you’ll use the Artisan command-line tool that comes with Laravel by default. At the end, you will be able to destroy and recreate your database tables as many times as you want, using only artisan commands.

To get started, first make sure you’re in the application’s root directory and your Docker Compose development environment is up and running:

  • cd ~/landing-laravel
  • docker-compose up -d
Output
landing-laravel_app_1 is up-to-date landing-laravel_nginx_1 is up-to-date landing-laravel_db_1 is up-to-date

Next, create a database migration to set up the links table. Laravel Migrations allow developers to programmatically create, update, and destroy database tables, working as a version control system for your database schema.

To create a new migration, you can run the make:migration Artisan command and that will bootstrap a new class on your Laravel application, in the database/migrations folder. This class will contain a default boilerplate code.

Remember to use docker-compose exec app to run the command on the app service container, where PHP is installed:

  • docker-compose exec app php artisan make:migration create_links_table
Output
Created Migration: 2020_11_18_165241_create_links_table

Notice that the migration name is generated based on the current date and time, and the name provided as argument to the make:migration command. For that reason, your migration file name will differ slightly.

Open the generated migration class using your editor of choice:

  • nano database/migrations/2020_11_18_165241_create_links_table

Next, update the up method to include the table columns you’ll need to store the app data.

Replace the current content of your migration class with the following code. The highlighted values are the only lines that need adding, so if you prefer, you can also only copy those highlighted lines and include them into your Schema::create definition:

database/migrations/2020_10_12_171200_create_links_table.php
<?php

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

class CreateLinksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('links', function (Blueprint $table) {
            $table->id();
            $table->string('url', 200);
            $table->text('description');
            $table->boolean('enabled')->default(true);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('links');
    }
}

In addition to the default fields that are included in the table definition that is automatically generated with the Artisan command, you’re including three new fields in this table:

  • url : A string field to save the link URL.
  • description : A text field to save the link description.
  • enabled : A field to store the state of the link, whether it’s enabled or not. The boolean Schema type will generate a tinyint unsigned field to store a value of either 0 of 1.

Save your migration file when you’re done adding these fields. Next, run the migration with:

  • docker-compose exec app php artisan migrate
Output
Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (152.46ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (131.12ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (101.06ms) Migrating: 2020_11_18_165241_create_links_table Migrated: 2020_11_18_165241_create_links_table (60.20ms)

You’ll notice that other migrations were also executed along with the create_links_table. That is because the default Laravel installation comes with migrations for users (with a users table and a password_resets table) and for queued jobs (with a failed_jobs table). Because our demo application won’t use these features, it is safe to remove those migrations now; however, you may also opt to leave them in place if you are working on an application of your own and you plan on developing it further. All migration files are located at database/migrations in the app’s root folder.

For more detailed information on database migrations, please refer to our guide on How To Use Database Migrations and Seeders to Abstract Database Setup in Laravel.

In the next part of this series, you’ll create a custom Artisan command to list, insert, and delete entries in the app’s links table.

Eloquent is an object relational mapper (ORM) included by default within the Laravel framework. It facilitates the task of interacting with database tables, providing an object-oriented approach to inserting, updating, and deleting database records, while also providing a streamlined interface for executing SQL queries.

Eloquent uses database models to represent tables and relationships in supported databases. The name of the database table is typically inferred from the model name, in plural form. For instance, a model named Link will use links as its default table name.

You can use the artisan make:model command line helper to generate new models for your application. To create a new Eloquent model for your links table, run:

  • docker-compose exec app php artisan make:model Link
Output
Model created successfully.

This will generate a new file containing a barebones model class. Even though this class has no apparent properties or methods, when operating the model via facades, you have access to the underlying Eloquent database classes that are able to identify database table structures and represent them as fully-functional objects.

For your reference, this is the automatically generated model class:

app/Models/Link.php
<?php

namespace App\Models;

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

class Link extends Model
{
    use HasFactory;
}

For the purpose of this series, you don’t need to make any changes to this file. If you want to extend the application in the future, you might use this model to create custom methods for the Link class that involve database operations. Additionally, if you want to create relationships between the Link model and other models, you’ll need to include a method representing the relationship in at least one of the sides. For detailed information about Eloquent relationships, please refer to the official documentation.

In the next part of this series, you’ll create Artisan commands that will use this model to select, insert, and delete links on your database.

If you followed along with this series so far, your database tables should be all set. However, you still need to implement a way to let users insert new entries in the links table.

To limit the scope of this series while also making the application fully-functional, you’ll set up Artisan commands to create and delete links in the database. Artisan is the command line tool that comes with Laravel, offering a number of utilities to speed up the development process, from generating boilerplate code to deleting and re-creating the application’s database.

Using the command line interface to manage your application can be an alternative to web forms and secured areas, since it requires a user to be logged on the server in order to execute such commands instead of being authenticated from a browser. If you decide later on to create a secured area for your application, you can create web forms to allow a registered user to submit a new link to the database.

Artisan commands are often used to perform application tasks that should run in the background, either manually or automatically via a scheduling mechanism such as Crontab. They can also be used to facilitate prototyping new application features that need to be configured dynamically, depending on input from an authorized user.

To get started, create a new Artisan command using the make:command helper:

  • docker-compose exec app php artisan make:command LinkNew
Output
Console command created successfully.

This will create a file named LinkNew.php, located at the app/Console/Commands directory. In this class, which extends from the Illuminate\Console\Command parent class, you’ll need to implement a handle method that will be executed when this command is called. To define the signature of the command, you’ll set the $signature protected property to link:new.

Open the new file using your text or code editor of choice. Here, we’ll use nano:

  • nano app/Console/Commands/LinkNew.php

A few different things will need to happen in the handle method so that you are able to save a new link to the database. First, you’ll prompt for the user’s input in order to obtain the link URL.

 $url = $this->ask('Link URL:');

Then, you’ll use the filter_var function to validate that the input obtained from the user is a valid URL. If the link is invalid, you’ll show an error and exit the application with status code 1, which means the application exited in error.

        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            $this->error("Invalid URL. Exiting...");
            return 1;
        }

If the link is valid, you’ll continue and ask for the link description using the same method as before.

 $description = $this->ask('Link Description:');

You’ll then ask for a final confirmation that all data is correct, using the confirm helper. If the user confirms, the link is finally inserted in the database. You’ll use the Link Eloquent model created in a previous part of this series to interact with the database.

        $this->info("New Link:");
        $this->info($url . ' - ' . $description);

        if ($this->confirm('Is this information correct?')) {
            $link = new Link();
            $link->url = $url;
            $link->description = $description;
            $link->save();

            $this->info("Saved.");
        }

The application exits with a 0, representing a success status (0 errors).

return 0;

The following code contains the full implementation of these steps. Replace the current content in your LinkNew class with:

app/Console/Commands/LinkNew.php
<?php

namespace App\Console\Commands;

use App\Models\Link;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class LinkNew extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'link:new';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a New Link';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $url = $this->ask('Link URL:');

        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            $this->error("Invalid URL. Exiting...");
            return 1;
        }

        $description = $this->ask('Link Description:');

        $this->info("New Link:");
        $this->info($url . ' - ' . $description);

        if ($this->confirm('Is this information correct?')) {
            $link = new Link();
            $link->url = $url;
            $link->description = $description;
            $link->save();

            $this->info("Saved.");
        }

        return 0;
    }
}

Save and close the file when you’re done.

To execute the command and insert a new link in the database, run:

  • docker-compose exec app php artisan link:new
Output
Link URL:: > https://digitalocean.com/community Link Description:: > DigitalOcean Community New Link: https://digitalocean.com/community - DigitalOcean Community Is this information correct? (yes/no) [no]: > yes Saved.

Feel free to add a few more links if you want to.

Next, you’ll need to create a new Artisan command to show the list of all links.You can call it link:list. Create the new command with:

  • docker-compose exec app php artisan make:command LinkList

Open the command class using your text or code editor of choice:

  • nano app/Console/Commands/LinkList.php

Within the handle method of this command, you’ll query for all rows in the links table. You can use the Link model to access the underlying database query methods that Eloquent provides. To exhibit the results nicely in the command line, you can use the table output helper:

        $headers = [ 'id', 'url', 'description' ];
        $links = Link::all(['id', 'url', 'description'])->toArray();
        $this->table($headers, $links);

        return 0;

The following code contains the full implementation of the link:list command. Replace the content in your LinkList.php file with :

app/Console/Commands/LinkList.php
<?php

namespace App\Console\Commands;

use App\Models\Link;
use Illuminate\Console\Command;

class LinkList extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'link:list';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'List links saved in the database';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $headers = [ 'id', 'url', 'description' ];
        $links = Link::all(['id', 'url', 'description'])->toArray();
        $this->table($headers, $links);

        return 0;
    }
}

Save and close the file when you are done.

To run this command and show a list of all links already inserted in the linkstable, run:

  • docker-compose exec app php artisan link:list
Output
+----+------------------------------------+--------------+ | id | url | description | +----+------------------------------------+--------------+ | 1 | https://digitalocean.com/community | DO Community | | 2 | https://laravel.com | Laravel | +----+------------------------------------+--------------+

Finally, you’ll create a command to delete links:

  • docker-compose exec app php artisan make:command LinkDelete
Output
Console command created successfully.

Open the new file using your text or code editor of choice:

  • nano app/Console/Commands/LinkDelete.php

You can name this command link:delete. To know which link must be deleted, you’ll need to require that users provide an additional argument when calling the command: the ID of the link. This is also set within the $signature variable, which defines how your command is called and what arguments, mandatory or not, should be provided:

protected $signature = 'link:delete {link_id}';

The handle method for this command will implement a few different instructions. First, you’ll obtain the Link ID that should have been provided within the command call.

$link_id = $this->argument('link_id');

Then, you’ll obtain the referenced link from the database, using the Eloquent method find that is available through your Link model.

$link = Link::find($link_id);

When the find method doesn’t find a database record with that ID, it will return null. You’ll check if that is the current value contained in the $link variable, and return an error in that case. The program will exit in error (code 1).

        if ($link === null) {
            $this->error("Invalid or non-existent link ID.");
            return 1;
        }

When $link is not null, the command continues execution. You then use the confirm helper to ask for a user confirmation.

if ($this->confirm('Are you sure you want to delete this link? ' . $link->url)) {
    // deletes link
}

When the user confirms the action by typing yes and hitting ENTER, you’ll call the delete method from the Link Eloquent model to delete the specified link from the database.

            $link->delete();
            $this->info("Link deleted.");

The following code contains the full implementation for the list:delete command. Replace the content in your LinkDelete.php file with the following:

app/Console/Commands/LinkDelete.php
<?php

namespace App\Console\Commands;

use App\Models\Link;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class LinkDelete extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'link:delete {link_id}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Deletes a link from the database.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $link_id = $this->argument('link_id');
        $link = Link::find($link_id);

        if ($link === null) {
            $this->error("Invalid or non-existent link ID.");
            return 1;
        }

        if ($this->confirm('Are you sure you want to delete this link? ' . $link->url)) {
            $link->delete();
            $this->info("Link deleted.");
        }

        return 0;
    }
}

Save and close the file when you’re done.

Now when you want to delete a link from your links table, you’ll first need to obtain the link’s ID with artisan link:list, as demonstrated earlier on. Once you know the ID of a link, you can run the artisan link:delete command with:

  • docker-compose exec app php artisan link:delete LINK_ID
Output
Are you sure you want to delete this link? https://laravel.com (yes/no) [no]: > yes Link deleted.

You’re now able to insert, list, and delete links in the application’s database, using Artisan commands executed from a command-line interface. In the next part of this series, you’ll set up the front end of your application using Blade templates and the Bulma CSS framework.

So far, you’ve seen how to set up the application’s MySQL database tables using migrations, how to create an Eloquent model to interact with the links table, and how to create Artisan commands to manage links in the database. You’ll now see how to create a custom Blade template to show your links in the application’s front-end. To facilitate styling this page while keeping it minimal, for this series we are going to use Bulma, a single-file CSS framework.

The default route set up within the Laravel web routes file points to an example template that you can find at resources/views/welcome.blade.php. You’ll create a new index.blade.php file within that same directory, and edit the main routes file so that the / route points to this template instead. In the route definition, you’ll also need to obtain a list of all links that you want to show in the new index template.

Start by updating the routes file of your Laravel application. Open the routes/web.php file using your text or code editor of choice:

  • nano routes/web.php

Your current / route points to the example page that comes with Laravel by default:

Route::get('/', function () {
    return view('welcome');
});

To make the proposed changes, first you’ll use the Link Eloquent model to fetch all links from the database, and sort them in decreasing order to make sure any new links you create are listed first, and thus will be shown at the top of the page.

$links = Link::all()->sortDesc();

The view helper function will look for a template file named welcome.blade.php, in the root of the resources/views directory, and return the rendered result to the browser. You’ll change this to point to a new index.blade.php template. Additionally, you’ll pass the $links variable along as template data.

    return view('index', [
        'links' => $links
    ]);

The following code implements the discussed changes for the / route. Replace the contents in your routes/web.php file with:

routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Models\Link;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('index', [
        'links' => Link::all()->sortDesc()
    ]);
});

Save and close the file when you’re done.

The routes file is all set, but if you try to access your main application’s page right now you will get an error message because the index.blade.php template doesn’t exist yet. You’ll create it now.

You can base your template on the Bulma starter template, which provides a minimal HTML page structure with a title, a subtitle, and a main content area. Later on, you’ll include some CSS styling to customize the appearance of this page.

To get started, create a new index.blade.php template using your text or code editor of choice:

  • nano resources/views/index.blade.php

Apart from the HTML boilerplate code, which creates the page structure and the static elements that you may want to use (such as headers and other information), you’ll need to show the list of links that was passed along as template data —a collection of Link objects.

You can use Blade’s foreach loop to loop through the links in the collection, and output them to the page:

            @foreach ($links as $link)
                <li>
                    <a href="{{ $link->url }}" target="_blank" title="Visit Link: {{ $link->url }}">{{ $link->description }}</a>
                </li>
            @endforeach

Include the following content in your index.blade.php file. Feel free to customize the title and other information in the page as you wish:

resources/views/index.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Sammy's Awesome Links</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
</head>
<body>
<section class="section">
    <div class="container">
        <h1 class="title">
            Check out my awesome links
        </h1>
        <p class="subtitle">
            You can include a little description here.
        </p>

        <ul>
            @foreach ($links as $link)
                <li>
                    <a href="{{ $link->url }}" target="_blank" title="Visit Link: {{ $link->url }}">{{ $link->description }}</a>
                </li>
            @endforeach
        </ul>
    </div>
</section>
</body>
</html>

Save the file when you’re done.

Now go to your browser to check the results. You should be able to access your application at port 8000 of either localhost or your remote server’s IP address, in case you are using a remote server as a development platform.

http://localhost:8000

You’ll see a page like this, showing all links present in your database from latest to first:

Landing Laravel Demo Application - Initial Version

Your application is now fully-functional, but you can still improve the appearance of this starter page to make it more appealing to your audience.

Styling and Customizing the Template (Optional)

Now that the base template is ready, you can include a few optional CSS customizations to style the page using some of the features available in Bulma, in addition to custom styles.

To give this page a new look, you can start by setting up a full page background. In this guide, we’ll use a DigitalOcean Wallpaper, but as an alternative you can also use a personal image or an image from a free stock photo website such as unsplash. You’ll need to obtain the image URL and use it to set up the background CSS property for the html element. A few other properties can be adjusted to make sure the image is centralized.

html {
            background: url("https://i.imgur.com/BWIdYTM.jpeg") no-repeat center center fixed;
            -webkit-background-size: cover;
            -moz-background-size: cover;
            -o-background-size: cover;
            background-size: cover;
        }

To style the list of links, you might want to replace the <li> elements for each link with box components, and include the link URL as a paragraph under the link description.

            @foreach ($links as $link)
                <div class="box link">
                    <h3><a href="{{ $link->url }}" target="_blank" title="Visit Link: {{ $link->url }}">{{ $link->description }}</a></h3>
                    <p>{{$link->url}}</p>
                </div>
            @endforeach

Finally, you can create a couple additional CSS styles to customize the appearance of the link text.

        div.link h3 {
            font-size: large;
        }

        div.link p {
            font-size: small;
            color: #718096;
        }

The following Blade template contains all suggested implementations. Replace your current index.blade.php file contents with:

resources/views/index.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Sammy's Awesome Links</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">

    <style>
        html {
            background: url("https://i.imgur.com/BWIdYTM.jpeg") no-repeat center center fixed;
            -webkit-background-size: cover;
            -moz-background-size: cover;
            -o-background-size: cover;
            background-size: cover;
        }

        div.link h3 {
            font-size: large;
        }

        div.link p {
            font-size: small;
            color: #718096;
        }
    </style>
</head>
<body>
<section class="section">
    <div class="container">
        <h1 class="title">
            Check out my awesome links
        </h1>
        <p class="subtitle">
            You can include a little description here.
        </p>

        <section class="links">
            @foreach ($links as $link)
                <div class="box link">
                    <h3><a href="{{ $link->url }}" target="_blank" title="Visit Link: {{ $link->url }}">{{ $link->description }}</a></h3>
                    <p>{{$link->url}}</p>
                </div>
            @endforeach
        </section>
    </div>
</section>
</body>
</html>

Save the file when you’re done.

Now reload your browser and you’ll see the updated page:

Landing Laravel Demo Application - Final Version

Conclusion

Congratulations, you have built a fully functional Laravel application. You have seen how to bootstrap a containerized PHP development environment and how to create a Laravel application from scratch, how to create database migrations and Eloquent models, how to create Artisan commands, and how to style a custom blade template.

The goal of this series was to provide a project-oriented path to get started in Laravel, using a links landing page as example application. You are strongly encouraged to review and experiment with each individual topic to get more familiar with the framework capabilities.

If you’d like to make this application accessible to the general public, you have several options for deploying it to a remote server. Our guide on How To Install and Configure Laravel with Nginx on Ubuntu 20.04 goes over how to deploy a Laravel application to a remote web server, but you might also check out our guide on How to Deploy a Laravel application to Kubernetes with Helm if you would like to use Kubernetes and a containerized environment to serve your application to the public.

For more information about the subjects we discussed in this series, check the following resources: