A Practical Introduction to Laravel Eloquent ORM

31.6k views

Introduction

Eloquent is an object relational mapper (ORM) that is included by default within the Laravel framework. An ORM is software that facilitates handling database records by representing data as objects, working as a layer of abstraction on top of the database engine used to store an application’s data.

Eloquent 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 complex SQL queries.

In this project-based series, you’ll learn how to make database queries and how to work with relationships in Laravel Eloquent. To practice the examples explained throughout the series, you’ll download a demo Laravel application and use the included Docker Compose setup to run a PHP development environment on containers. The demo consists of a single page application that shows a list of links or bookmarks. You’ll improve the application by adding new models and defining new relationships between them that will extend the current database structure.

If you’d like an introduction to the Laravel framework, you can follow the guide on How To Build a Links Landing Page in Laravel. That tutorial explains how you can build the demo application that this series uses as a foundation from scratch.

Prerequisites

Although the code shared throughout this series should work seamlessly across multiple environments and systems, the instructions explained here were tested within an Ubuntu 20.04 local system running Docker and Docker Compose. Regardless of your base operating system, here’s what you’ll need to set up in order to get started:

  • Docker installed on your local machine or remote development server. If you’re running Ubuntu 20.04, you can follow Steps 1 and 2 of How To Install and Use Docker on Ubuntu 20.04 to set it up. Windows and MacOS users need to install Docker Desktop instead.
  • Docker Compose installed on your local machine or development server. Docker Compose comes included by default with Docker Desktop for both Windows and MacOS systems, but Linux users need to install the Compose executable, following Step 1 of How To Install and Use Docker Compose on Ubuntu 20.04.
  • A code editor for PHP (optional). A code editor helps making code easier to read and to format, and can improve your productivity by pointing out issues before you execute your code. You can follow our guide on How To Set Up Visual Studio Code for PHP Projects to set up VSCode, a free code editor, within your local development environment.

Setting Up the Demo Project

To get started, you’ll need to set up the Landing Laravel demo project. There are two ways in which you can get the demo application code ready to use with this series:

  1. The first way is to follow the guide on How To Build a Links Landing Page in Laravel. That series explains how to build the demo application that you’ll use as base for the current series from scratch. If you choose this option, you can move on to the first tutorial in this series How To Create a One-To-Many Relationship in Laravel Eloquent.

  2. The second option is to download the complete demo application code and use it as the base that you will build on in this series. On the application releases page, you’ll find separate application versions for each tutorial in the series. You can choose to start from the first tutorial by downloading version 0.1.1, or you can choose to download one of the elo-tutorial releases that are paired with each individual tutorial in the series.

To download the code from GitHub, first make sure that you have the curl and unzip tools available on your system:

  • sudo apt update
  • sudo apt install curl unzip

Next, change to your home directory using the cd ~ command, and then download your preferred project release using curl. To start from the first guide, you can download the 0.1.1 release:

  • cd ~
  • curl -L https://github.com/do-community/landing-laravel/archive/refs/tags/0.1.1.zip -o landing-laravel.zip

The demo code will be saved in the landing-laravel.zip file. Unzip the contents of the zip file, rename the directory to landing-laravel, and cd into the application folder using the following commands:

  • unzip landing-laravel.zip
  • mv landing-laravel-0.1.1 landing-laravel
  • cd landing-laravel

The MySQL image that you will use with Docker Compose uses environment variables to set up the database user and password. Before bringing the environment up for the first time, you’ll need to set up an .env file that contains database credentials. The demo application includes an example .env.example file that you can use. Copy it in place by running the following command:

  • cp .env.example .env

You can now bring your environment up with docker-compose:

  • docker-compose up -d

Install the Laravel application dependencies with:

  • docker-compose exec app composer install

This will install the PHP dependencies that Laravel needs via Composer. Next, create a unique application key using artisan:

  • docker-compose exec app php artisan key:generate

You should receive output like the following, indicating that artisan added a unique application key to your .env file:

Output
Application key set successfully.

Finally, run the database migrations and seeders to set up the database:

  • docker-compose exec app php artisan migrate --seed

You will receive output like the following, which indicates the database tables, and seed data are configured correctly:

Output
Migration table created successfully. . . . Database seeding completed successfully.

The application is now configured and running in your development environment. Point your browser to http://localhost:8000 to access it. If you’re starting at the first guide of the series, you’ll receive a page similar to this:

Landing Laravel demo application

With this base structure in place, you can proceed with the rest of the tutorials in this series.

The demo Landing Laravel application that you set up as a prerequisite for this series contains a single database table to store links. In this tutorial you’ll modify this initial database structure to include a second table, which you will use to organize links into lists.

For the links and lists example we’re going to use in this series, each link is part of only one list, but each list can have multiple links. This kind of relationship is also known as a one-to-many relationship.

A one-to-many relationship happens when one item, which we’ll call type A, can be linked to several items of type B, but the opposite doesn’t hold true: an item of type B can only be linked to one item of type A. Transposing this scenario to the current demo application models, A is the list type, and B is the link type.

To get started, you’ll need to create a model and a database table to represent a List of links. Then, you’ll update the existing Link model and table to include the relationship between both models. Because the term List is reserved for PHP internals, you won’t be able to name your new model with that term. You can call this new model LinkList instead.

First, make sure you’re in the application directory:

  • cd ~/landing-laravel

Create a new model using artisan:

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

This will generate a new model class in the app/Model directory:

app/Model/LinkList.php

If you look at your app/Console/Commands directory, you’ll notice that there’s already a class file named LinkList.php. This is not to be confused with the Eloquent model you just created. This class contains a CLI command that lists all the links in the database via artisan.

To avoid confusion in the future, now is a good moment to rename that class and its command signature to a different name. In this case use the class name LinkShow since that name also describes what the class does. To rename the app/Console/Commands/LinkList.php file to another valid name, run this command in your terminal:

  • mv app/Console/Commands/LinkList.php app/Console/Commands/LinkShow.php

Then, open the file app/Console/Commands/LinkShow.php in your code editor to change the class name from LinkList to LinkShow, and the command signature from link:list to link:show, like the highlighted lines in the following code listing. This is how the file should look like once you’re finished:

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

namespace App\Console\Commands;

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

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

    /**
     * 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’re done. To check that everything worked as expected, run your newly renamed link:show artisan command:

  • docker-compose exec app php artisan link:show

You’ll receive output like this:

Output
+----+-------------------------------------------------+----------------------------------+ | id | url | description | +----+-------------------------------------------------+----------------------------------+ | 1 | https://digitalocean.com/community | DigitalOcean Community | | 2 | https://digitalocean.com/community/tags/laravel | Laravel Tutorias at DigitalOcean | | 3 | https://digitalocean.com/community/tags/php | PHP Tutorials at DigitalOcean | +----+-------------------------------------------------+----------------------------------+

The new app/Model/LinkList.php class that you generated with the previous artisan make:model command contains generic code for a new Eloquent class. Unlike other ORMs such as Doctrine, Eloquent doesn’t alter database structures, handling only data itself. Eloquent models are usually lean, with class properties automatically inferred from the model’s table structure.

This approach to handling data-only with Eloquent means that you don’t need to set up any properties for the LinkList class because they will be inferred from the database table structure for that model.

Structural database operations are typically handled in Laravel via database migrations. Migrations allow developers to programmatically define structural changes to the database, such as creating, modifying, and deleting tables.

You’ll now create a new migration to set up the lists table in the database.

The artisan command line tool included by default with Laravel contains several helper methods to bootstrap new components such as controllers, models, migrations, among others. To create a new migration using artisan, run:

  • docker-compose exec app php artisan make:migration create_link_lists_table
Output
Created Migration: 2021_07_07_152554_create_link_lists_table

This command will generate a new file under the database/migrations directory in your Laravel application, using an auto-generated name based on the current date and time, and the migration name. That file contains generic code that you’ll modify to set up the lists table.

Using your code editor, open the generated migration file. The file currently looks like this:

database/migrations/2021_07_07_152554_create_link_lists_table.php
<?php

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

class CreateLinkListsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('link_lists', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

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

The migration runs the up() method when executed with the artisan migrate command. This is where your table definition goes, and by default it creates an id primary key field and two timestamp fields (created_at and updated_at), defined with the timestamps() Schema method. Those fields are automatically filled by Eloquent when a model is created and updated, respectively. The down() method is called when the migration is rolled back with the artisan rollback command, and typically executes code to drop the table or revert structural changes.

You’ll change the up method to include the following fields:

  • title: a string representing the title of this List
  • description: a string representing the description of a List
  • slug: a unique, short string based on the title, typically used to create user-friendly URLs

In a one-to-many relationship, the many side (which in this scenario corresponds to the links table) is the one to hold the column reference (or foreign key) to the other element (corresponding to the lists table). That means you’ll have to modify the links table later on, in order to include a reference field that will link that table to the lists table.

The lists table, on the other hand, doesn’t need any special field to reference its links.

Replace the current content in your migration file with the following code:

database/migrations/2021_07_07_152554_create_link_lists_table.php
<?php

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

class CreateLinkListsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('link_lists', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('title', 60);
            $table->string('slug', 60)->unique();
            $table->text('description')->nullable();
        });
    }

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

Save the file when you’re done.

Next, open the existing links migration file in your code editor. In the demo project, you’ll find the migration at the following path:

2020_11_18_165241_create_links_table.php

First, include a use directive pointing to the fully qualified class name for the LinkList class, at the beginning of the file and right after the last use line:

…
use Illuminate\Support\Facades\Schema;
use App\Models\LinkList;
...

Next, include the following line in the table definition, within the up method and right after the line that sets up the description field:

$table->text('description');
$table->foreignIdFor(LinkList::class);

The foreignIdFor() method creates a foreign key column to the referenced Eloquent model. It uses default nomenclature to set up a field that is linked to the primary key field of the referenced table.

This is how the full migration class should look like when you’re done:

database/migrations/2020_11_18_165241_create_links_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\LinkList;

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->foreignIdFor(LinkList::class);
            $table->timestamps();
        });
    }

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

Save the file when you’re finished editing it. Next, wipe the database and then run the migration command again to recreate your database structure with the updated migrations files:

  • docker-compose exec app php artisan db:wipe
  • docker-compose exec app php artisan migrate

Configuring Eloquent Model Relationships

The database tables are now set up, but you still need to configure the Eloquent models to define the relationship between them.

On the List model, which is the one side of the relationship, you’ll set up a new method named links. This method will work as a proxy to access the links that are related to each list, using the hasMany method from the parent Illuminate\Database\Eloquent\Model class.

In your code editor, open the file app/Model/LinkList.php. Replace the current generic code with the following content:

app/Model/LinkList.php
<?php

namespace App\Models;

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

class LinkList extends Model
{
    use HasFactory;

    public function links()
    {
        return $this->hasMany(Link::class);
    }
}

Save the file when you’re done.

Next, you’ll edit the many side of the relationship to include a reference back to the List model, so that links can access their respective list. This is done with the belongsTo method from the parent Model class. This method is used to define the inverse side of the one-to-many relationship.

Open the Link model in your code editor:

app/Model/Link.php

Replace the current content in your Link.php file with the following code:

app/Model/Link.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Link extends Model
{
    public function link_list()
    {
        return $this->belongsTo(LinkList::class);
    }
}

Save the file when you’re done.

With both models updated, your database is now configured completely, but it is currently empty. In the next section of this series, you’ll learn how to insert new records in the database using Eloquent models.

In a previous section of this series, you set up two models for a one-to-many relationship between the LinkList and Link models. In this section, you’ll learn how to insert links and lists in the database using Eloquent models. To limit the scope of this work, you’ll use custom Artisan commands to manage links and lists from the command line, which won’t require a web form.

One of the biggest advantages of using an ORM system is the ability to manipulate rows in a database table as objects within your codebase. With Eloquent, as with other ORMs, the object itself provides methods that can be used to persist it to the database, saving you the work of writing SQL statements and manually managing data within tables.

When working with one-to-many relationships in Laravel Eloquent, you have a few different options to save related models. In most cases, you’ll need to first set up the model representing the one side of the relationship, which in this demo is the LinkList model, and save that to the database. After doing that, you’ll be able to reference this model (which, once saved, represents a database record) when setting up the many side of the relationship (the Link model). That also means you’ll need to first have one or more lists in order to be able to create links.

Before creating a new command to insert lists, however, you should update the existing link:new command to support the list feature.

Open the following file in your code editor:

app/Console/Commands/LinkNew.php

You’ll see code like this:

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;
    }
}

The handle() method is where the command executes its procedures. This is what it does:

  1. The ask() method, made available through the parent Illuminate\Console\Command class, is a method used to obtain input from a user in the command line. This will prompt a user for a link, and validate the input to make sure it’s a valid URL.
  2. The script then asks for an optional description.
  3. Once values for url and description are obtained, the script will prompt for a confirmation using the confirm() method, available through the parent Illuminate\Console\Command.
  4. When a confirmation is submitted with y or yes, the script will set up a new link object and save it to the database using the save() method, available through the model’s parent Illuminate\Database\Eloquent\Model class.
  5. The script outputs a message to inform the user that the link was saved to the database, using the info output method.

Note about return values: in the context of command line applications running on bash, non-zero return values are used to signal that the application exited in error, while 0 means it exited with success.

If you run the link:new command now, it will break before it is finished because the database expects every link to be connected to a list. You’ll need to let the user choose which list a link should be included in, using a default list if none is provided by the user.

The following code will ask the user to specify a list or leave it blank to use the default list. Then, it will try to locate the list or create a new list using the specified slug in case the list doesn’t exist yet. To retrieve a list provided by the user, this code uses the firstWhere method to find a list based on its slug field. Finally, it saves the new link using the links() relationship that can be accessed from the LinkList object.

Substitute the current content in your LinkNew command class with:

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

namespace App\Console\Commands;

use App\Models\Link;
use App\Models\LinkList;
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');
        $list_name = $this->ask('Link List (leave blank to use default)') ?? "default";

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

        if ($this->confirm('Is this information correct?')) {
            $list = LinkList::firstWhere('slug', $list_name);
            if (!$list) {
                $list = new LinkList();
                $list->title = $list_name;
                $list->slug = $list_name;
                $list->save();
            }

            $link = new Link();
            $link->url = $url;
            $link->description = $description;
            $list->links()->save($link);

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

        return 0;
    }
}

Save and close the file when you’re done. Then, run the command with:

  • docker-compose exec app php artisan link:new

You’ll be prompted to provide a URL, a description, and a list name, in case you don’t want to save this link to the default list.

Once you save your new link, if you run the link:show command, you should see the new link added to the results. However, there is no information included in the output about lists yet. You’ll need to update the LinkShow command to include a column that displays this information.

Open the app/Console/Commands/LinkShow.php file in your code editor:

app/Console/Commands/LinkShow.php

This is how the class should look like now:

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

namespace App\Console\Commands;

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

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

    /**
     * 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;
    }
}

You’ll see that the current handle() method is fetching a certain number of fields and converting the result to an array. By default, results come from Eloquent as an Eloquent Collection, so this function converts them to an array in order to use that data within the table() method. The problem is that when the array conversion is made, you lose the relationship between class models (Link and LinkList), which makes it more difficult to access information about the list that a link is connected to.

You’ll need to change this code so that it fetches the full Link object, including the related objects from the database. To create an array that is suitable for using with the table() method, you can iterate through the collection of results returned by Link::all().

Replace the current contents in the app/Console/Commands/LinkShow.php file with the following code:

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

namespace App\Console\Commands;

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

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

    /**
     * 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', 'list', 'description' ];
        $links = Link::all();

        $table_rows = [];
        foreach ($links as $link) {
            $table_rows[] = [ $link->id, $link->url, $link->link_list->slug, $link->description ];
        }

        $this->table($headers, $table_rows);

        return 0;
    }
}

Now, if you run the link:show method, you’ll see an additional column showing the list slug:

Output
+----+-----------------------------------------------------------------------------------------+-----------+--------------------------------------+ | id | url | list | description | +----+-----------------------------------------------------------------------------------------+-----------+--------------------------------------+ | 1 | https://digitalocean.com | default | DigitalOcean Website | | 2 | https://digitalocean.com/community/tutorials | tutorials | DO Tutorials | | 3 | https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-20-04 | tutorials | Initial server setup on ubuntu 20.04 | +----+-----------------------------------------------------------------------------------------+-----------+--------------------------------------+

Later in this tutorial series, you’ll update the front end and main route code to show links organized into lists. For now, you’ll use the command line to add, migrate, and validate your changes to the database and models.

The next tutorial in this series will demonstrate another way of inserting new records in the database using Eloquent models, this time through the use of database seeders.

Laravel’s Seeders are special classes that live in the database/seeders directory in a Laravel project that allow you to programmatically insert a collection of default or sample records in the database. The demo application has a seeder class that imports links from a links.yml file in the root of the application folder.

In your code editor, open the following file:

database/seeders/LinkSeeder.php

It will contain the following code:

database/seeders/LinkSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Link;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Yaml\Yaml;

class LinkSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //only import seeds if DB is empty.
        if (!Link::count()) {
            $this->importLinks();
        }
    }

    /**
     * Imports Links from the default links.yml file at the root of the app.
     * Change that file to import a set of personal basic links you want to show
     * as soon as the application is deployed.
     */
    public function importLinks()
    {
        $links_import_path = __DIR__ . '/../../links.yml';

        $yaml = new Yaml();
        if (is_file($links_import_path)) {
            $links = $yaml->parsefile($links_import_path);

            foreach ($links as $link) {
                DB::table('links')->insert([
                    'url' => $link['url'],
                    'description' => $link['description']
                ]);
            }
        }
    }
}

Notice that this code does not use the Link model and instead uses the query builder to insert new links in the database. This is a different way of working with database records in Laravel that doesn’t depend on Eloquent models. Even though this works well, by using Eloquent models you’ll have access to a series of helpful methods and shortcuts to make your code more concise and easier to read.

To improve this code, you’ll change the foreach loop to use Eloquent models instead of querying the database directly with the query builder. You’ll also have to create a default list of links (called $default_list in the following code) before the loop is started, so that you can reference this list in each new link created.

Replace the current content in your seeder class with the following code:

database/seeders/LinkSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Link;
use App\Models\LinkList;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Yaml\Yaml;

class LinkSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //only import seeds if DB is empty.
        if (!Link::count()) {
            $this->importLinks();
        }
    }

    /**
     * Imports Links from the default links.yml file at the root of the app.
     * Change that file to import a set of personal basic links you want to show
     * as soon as the application is deployed.
     */
    public function importLinks()
    {
        $links_import_path = __DIR__ . '/../../links.yml';

        $yaml = new Yaml();
        if (is_file($links_import_path)) {
            $links = $yaml->parsefile($links_import_path);

            $default_list = new LinkList();
            $default_list->title = "Default";
            $default_list->description = "Default List";
            $default_list->slug = "default";
            $default_list->save();

            foreach ($links as $link) {
                $seed_link = new Link();
                $seed_link->url = $link['url'];
                $seed_link->description = $link['description'];

                $default_list->links()->save($seed_link);
            }
        }
    }
}


The updated code uses an object-oriented approach for setting up the properties for the LinkList and List models that are translated into table columns by Eloquent. The final line of the for loop uses the $default_list reference to the links table, which is accessed via the method links(), to save new links within that list.

Save the file when you’re done. Laravel seeders will only run when the database is empty, so as to not conflict with actual data that was inserted in the database by other means. Thus in order to run the modified seeder, you’ll need to wipe the database once again with the artisan db:wipe command..

Run the following command to wipe the development database:

  • docker-compose exec app php artisan db:wipe
Output
Dropped all tables successfully.

Now to recreate the tables and run the updated seeders, you can use the following artisan migrate --seed command:

  • docker-compose exec app php artisan migrate --seed

You should receive output that is similar to the following:

Output
Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (124.20ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (121.75ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (112.43ms) Migrating: 2020_11_18_165241_create_links_table Migrated: 2020_11_18_165241_create_links_table (61.04ms) Migrating: 2021_07_09_122027_create_link_lists_table Migrated: 2021_07_09_122027_create_link_lists_table (112.18ms) Seeding: Database\Seeders\LinkSeeder Seeded: Database\Seeders\LinkSeeder (84.57ms) Database seeding completed successfully.

In the next chapter of this series, you’ll learn in more detail how to query database records with Eloquent.

If you’ve been following along with all parts of this series so far, you should have the application’s database and commands upgraded to include lists of links.

The application main route currently shows all links in the database, with no information about lists. In this section, you’ll update the main front end view in order to reflect the new architecture.

One of the biggest advantages of using an ORM system is the ability to manipulate rows in a database table as objects within your codebase. Eloquent provides several methods that can be accessed directly from models to query the database and filter results without having to write SQL statements. A typical SELECT query to obtain all rows from a table, which in pure SQL looks something like SELECT * FROM links, can be accomplished in Eloquent with code like this:

$links = Link::all();

The result set is returned as an Eloquent Collection, an iterable object that behaves similarly to an array, but provides extended functionality such as map / reduce methods and the ability to “hydrate” (pull in fresh data) referenced objects only when needed, which helps with overall performance while interacting with the database.

Updating the Index Route

If you examine the main application route file, where the index route is defined, you’ll notice that the current application code queries all links using a Link::all() call. Open the routes/web.php file in your code editor:

routes/web.php

This is how the / route is currently defined in this file:

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

As the name suggests, the sortDesc() method is used for ordering results in descending order, from last to first. This is not the same as an ORDER BY clause in an SQL query, since the sortDesc method works at code level, reordering the collection. You can ignore this method for now, as we’ll talk more about ordering results in an upcoming section of this series.

You’ll now edit this code to obtain a collection of all lists currently registered within the database, so that you can use it later on in the front end view to show all list names.

First, include a use declaration at the beginning of the file, referencing the LinkList model. This declaration is used to make sure that you don’t need to type the full class name each time you reference that class.

routes/web.php
<?php

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

Then, change the return of the main route definition to include a lists variable containing all registered lists:

routes/web.php
…
    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
…

This is how the finished file should look. Note the highlighted changes:

routes/web.php
<?php

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

/*
|--------------------------------------------------------------------------
| 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 () {
    $links = Link::all()->sortDesc();
    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

Don’t forget to save the file when you’re done updating it.

Updating the Index View

Once you get the main route updated to also provide information about lists, you can edit the referenced view file. Open the resources/views/index.blade.php file in your code editor:

resources/views/index.blade.php

This file contains the single front end view file that the application uses in its main index page. Within this file, locate the @foreach blade block that loops over the $links variable. It will look like this:

resources/views/index.blade.php
...
            @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
...

The code in the loop does not include any information about lists yet. You’ll now include a new line right after the link URL that contains the title of the list where that link is saved. You can use the tag CSS class from Bulma to style this information as a tag:

resources/views/index.blade.php
…
<p>{{$link->url}}</p>
<p class="mt-2"><a href="#" title="{{ $list->title }}" class="tag is-info">{{ $link->link_list->title }}</a></p>

Add the highlighted line to your file. The links will be adjusted later on, when you set up individual list pages in an upcoming part of this series.

Next, locate the paragraph that has the class subtitle, which comes right after the <h1> tag and before the section containing your links. You’ll replace the generic text used in that area with a menu based on your link lists, which you made available previously in a lists variable when you edited the default route in routes/web.php.

Replace the text in the subtitle section with the highlighted content:

resources/views/index.blade.php

        <p class="subtitle">
            @foreach ($lists as $list)<a href="#" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }}</a> @endforeach
        </p>

The list tags are styled just slightly differently than before, with an is-light additional CSS class to invert the colors of each tag.

This is how your index.blade.php file should look like after you’re done editing. The highlighted lines indicate the changes:

resources/views/index.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>My 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">
            @foreach ($lists as $list)<a href="#" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }}</a> @endforeach
        </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>
                    <p class="mt-2"><a href="#" title="{{ $list->title }}" class="tag is-info">{{ $link->link_list->title }}</a></p>
                </div>
            @endforeach
        </section>
    </div>
</section>
</body>
</html>

Save the file when you’re done. The main front end view is now ready to show updated information about link lists.

If you’d like, you can use the link:new Artisan command now to include new links and test your updated application:

  • docker-compose exec app php artisan link:new
[secondary_label Output
Link URL:
> https://laravel.com/docs/8.x/

Link Description:
> Laravel Docs

Link List (leave blank to use default):
> laravel

New Link:
https://laravel.com/docs/8.x/ - Laravel Docs
Listed in: laravel

Is this information correct? (yes/no) [no]:
> yes

Saved.

Then, reload the application page on your browser. If you’re using the included Docker Compose setup, the application should be available on the following local address:

http://localhost:8000

You’ll obtain a page similar to this:

Screenshot of Landing Laravel app, updated to support link lists

In the next part of this series, you’ll set up individual pages for link lists and learn how to make queries to the database using the where() method, for more fine-grained results.

In a previous part of this series, you updated the demo application to organize links into lists in your database. The main application view now shows a menu with all lists that are currently registered within the database, but the menu has no active links yet.

In this section, you’ll create a new route within the application to show links by list. You’ll also learn how to use the where() method in Eloquent to better filter results in a database query.

To get started, open the routes/web.php file in your code editor:

routes/web.php

The file currently has the following content:

routes/web.php
<?php

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

/*
|--------------------------------------------------------------------------
| 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 () {
    $links = Link::all()->sortDesc();
    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

The Route::get call defines an HTTP GET route for the application entry page. When a request to / is made to the application, this callback function will be triggered to return the index view.

You’ll now create a second route to show lists of links based on a list slug. A slug is a short string that is typically used to build user-friendly URLs. The new route must query the database’s link_lists table for a list that has the provided URL parameter as its slug field. If a list with that slug cannot be found, the application should inform the user with an HTTP 404 or not found error.

The following code creates a GET route using a dynamic parameter (defined by {slug}), named link-list. This will:

  • Use the LinkList Eloquent model to query the database with the where() method, using slug as search criteria. The first() method will make sure only one object is returned.
  • If a list with the specified slug can’t be found, a 404 error is thrown with the abort method.
  • If a valid list is found, the index view is then rendered, and the list is provided as a template parameter.

The $lists parameter is provided to build the list menu, and the $links parameter is provided for compatibility with the current version of the index view, since it loops through a variable with that name.

Include the following code at the bottom of your routes/web.php file:

routes/web.php
Route::get('/{slug}', function ($slug) {
    $list = LinkList::where('slug', $slug)->first();
    if (!$list) {
        abort(404);
    }

    return view('index', [
        'list' => $list,
        'links' => $list->links,
        'lists' => LinkList::all()
    ]);
})->name('link-list');

Save the file when you’re done.

Although there are shortcuts to implement routes that reference Eloquent models, in this tutorial we focus on using the where() method for learning purposes.

To test that your new route works as expected, you can go to your browser and access the link for the default list page. If you’ve followed all steps in this series so far and your database is not empty, the default list page should be available at the following local address:

http://localhost:8000/default

You’ll see the same page as before, but links are now limited to those from the default list. If you have additional lists, you can access their page by replacing the highlighted default slug in the URL with the slug of your list.

With your new route configured, you can now use the route Blade method to dynamically generate URLs for your link lists, from your template view. You can also customize the page header to show information about a list in case one is available.

Open the resources/views/index.blade.php file in your code editor:

resources/views/index.blade.php

This file has 2 lines that need updating. First, locate the subtitle paragraph containing the menu you created in another part of this series. This is how it looks like now:

resources/views/index.blade.php
        <p class="subtitle">
            @foreach ($lists as $list)<a href="#" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }}</a> @endforeach
        </p>

You’ll update the href hyperlink to include the current URL for the list page, using the route blade method. This method expects the name of the route as the first argument, with URL parameters provided as additional arguments to the method call. Replace the # character with the following highlighted content:

resources/views/index.blade.php
        <p class="subtitle">
            @foreach ($lists as $list)<a href="{{ route('link-list', $list->slug) }}" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }}</a> @endforeach
        </p>

Next, locate the links section and the foreach loop within. You’ll need to include another call to the route() method where the list name is printed for each link. This will be similar to the previous example, however, the list object is accessed differently, through the $link variable:

resources/views/index.blade.php
            <p>{{$link->url}}</p>
            <p class="mt-2"><a href="{{ route('link-list', $link->link_list->slug) }}" title="{{ $link->link_list->title }}" class="tag is-info">{{ $link->link_list->title }}</a></p>

Next, you may want to include information about a list, when additional information is provided. You can check for the existence of a $list variable, and print the list title only when that variable is available.

Replace your title section with the following highlighted code:

        <h1 class="title">
            @if (isset($list))
                {{ $list->title }}
            @else
                Check out my awesome links
            @endif
        </h1>

This is how the index.blade.php file will look once you’re finished. The changes are highlighted for your convenience:

resources/views/index.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>My 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">
            @if (isset($list))
                {{ $list->title }}
            @else
                Check out my awesome links
            @endif
        </h1>
        <p class="subtitle">
            @foreach ($lists as $list)<a href="{{ route('link-list', $list->slug) }}" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }}</a> @endforeach
        </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>
                    <p class="mt-2"><a href="{{ route('link-list', $link->link_list->slug) }}" title="{{ $link->link_list->title }}" class="tag is-info">{{ $link->link_list->title }}</a></p>
                </div>
            @endforeach
        </section>
    </div>
</section>
</body>
</html>

Save the file when you’re done updating its contents.

You can now access the application main page through your browser. If you are using the included Docker Compose setup, the application should be available through the following local address:

http://localhost:8000

You’ll get a page similar to the following screenshot:

The screenshot shows the updated Landing Laravel application with working links to individual list pages

In the next part of this series, you’ll learn how to order query results in Laravel Eloquent.

In a previous part of this series, you learned how to obtain database records using the all() method from within an Eloquent model. You may recall using a method called sortDesc(), which was used to sort the records in descending order.

The sortDesc() method is part of the Collection class, a powerful Laravel utility class that works as an improved version of native PHP arrays. Instead of ordering results within the database query itself, this method will invert the order of a collection, so that the last item appears first in the collection. While that works well for smaller result sets, it doesn’t offer the same flexibility as sorting the results in the database query itself.

To sort results in the database query, you’ll need to use the orderBy() method, and provide the table field you want to use as criteria for ordering. This will give you more flexibility to build a query that will obtain only the results you need from the database.

You’ll now change the code in your routes/web.php file to show results ordered from newest to oldest, based on the created_at table field.

Both the created_at and the updated_at fields are managed by Eloquent when you include a timestamps() definition in your table migration. You should not update these fields manually, but you can use them to sort and filter your queries.

Open this file in your code editor:

routes/web.php

This is how the code looks like now:

routes/web.php
<?php

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

/*
|--------------------------------------------------------------------------
| 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 () {
    $links = Link::all()->sortDesc();
    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

Route::get('/{slug}', function ($slug) {
    $list = LinkList::where('slug', $slug)->first();
    if (!$list) {
        abort(404);
    }

    return view('index', [
        'list' => $list,
        'links' => $list->links,
        'lists' => LinkList::all()
    ]);
})->name('link-list');

Notice that the /{slug} route, which is responsible for listing the links by slug, currently doesn’t use any sorting method. Links are obtained through the list variable, highlighted in the code, using the relationship defined in the LinkList model.

If you add multiple links to a list now, the query will return results ordered from oldest to newest by default. Although you could use the sortDesc() method to reorder the collection within the $list->links call, using the orderBy() method provides more flexibility and allows you to include additional filtering conditions later. You can chain this method with a where() call for even more fine-grained results.

Replace the highlighted line in the previous code sample with the following line:

routes/web.php
'links' => $list->links()->orderBy('created_at', 'desc')->get(),

Notice that this time we’re invoking the built-in query builder by calling the $list->links() method, which refers to the relationship method defined in the LinkList class. This is different from calling $list->links as a class property (without the parenthesis), which will invoke a magic method in the model to fetch all links related to that list.

This is how the full routes/web.php file should look like once you’re finished:

routes/web.php
<?php

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

/*
|--------------------------------------------------------------------------
| 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 () {
    $links = Link::all()->sortDesc();
    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

Route::get('/{slug}', function ($slug) {
    $list = LinkList::where('slug', $slug)->first();
    if (!$list) {
        abort(404);
    }

    return view('index', [
        'list' => $list,
        'links' => $list->links()->orderBy('created_at', 'desc')->get(),
        'lists' => LinkList::all()
    ]);
})->name('link-list');

Save and close the file. Now, add a couple new links using the link:new Artisan command. You can use the default list:

  • docker-compose exec app php artisan link:new
Output
Link URL: > https://laravel.com/docs/8.x/eloquent Link Description: > Laravel Eloquent Docs Link List (leave blank to use default): > New Link: https://laravel.com/docs/8.x/eloquent - Laravel Eloquent Docs Listed in: default Is this information correct? (yes/no) [no]: > yes Saved.

If you reload the default link list page, you should now obtain links from newest to oldest:

http://localhost:8000/default

Default Link List showing links ordered by creation date, in descending order - from newest to oldest

Likewise, if you would prefer to order links alphabetically by the link description, you would have to change the line to use that table field in the method call like this:

'links' => $list->links()->orderBy('description', 'asc')->get(),

This is how the links would be ordered after such change:

Default Link List showing links ordered alphabetically by link description

In the next part of this series, you’ll learn how to obtain the total result count from a Laravel Eloquent query.

When working with database query results, it is often useful to obtain only the total number of rows in a result set, instead of pulling the full dataset content with a regular query. Eloquent offers a few different aggregate methods that return a scalar number instead of an Eloquent object, including count(), max(), and sum(), among others. These are all made available through the inherent query builder that is built into every Eloquent model.

In this part of the series, you’ll update the main application view to show the total number of links in each list.

Open the file resources/views/index.php in your code editor:

resources/views/index.php

Locate the paragraph styled with the subtitle class, which contains the foreach loop that renders the application menu:

...
<p class="subtitle">
     @foreach ($lists as $list)<a href="{{ route('link-list', $list->slug) }}" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }}</a> @endforeach
</p>
...

To obtain the total number of links in each list, you can access the query builder from within the $list->links() relationship method defined on the LinkList class, and call the count() method that is available through the query builder:

{{ $list->links()->count() }}

Update the code inside the foreach loop to include the count() method call, which will display the number of links in a list. Make sure to place it inside the <a> tag and right after the list title. You can wrap this information inside a parenthesis for more readability in the rendered HTML output.

This is how the code should look like once you are finished:

 <p class="subtitle">
      @foreach ($lists as $list)<a href="{{ route('link-list', $list->slug) }}" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }} ({{ $list->links()->count() }})</a> @endforeach
 </p>

Save the file when you’re finished. Then, reload the main application page on your browser:

http://localhost:8000

You’ll obtain a page like the following, showing the total number of links included in each list, on the top menu:

Updated application showing number of links in each link on the top menu

In the next part of this series, you’ll learn how to limit the number of results in a query, and how to paginate results in Laravel Eloquent.

Throughout this series, you have been adding new links to your demo application to test out several features from Laravel Eloquent. You may have noticed that the main index page is getting longer each time you add a new link, since there is no limit to the number of links shown in the application. Although that won’t be an issue when you have a small number of database entries, in the long term that might result in longer loading times for your page, and a layout that is more difficult to read due to the amount of content spread in a single page.

In this part of the series, you’ll learn how to limit the number of results in a Laravel Eloquent query with the limit() method, and how to paginate results with the simplePaginate() method.

Limiting Query Results

To get started, you’ll update your main application route (/) to limit the number of links that are listed on your index page.

Start by opening your web routes file in your code editor:

routes/web.php

Then, locate the main route definition:

routes/web.php
Route::get('/', function () {
    $links = Link::all()->sortDesc();
    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

The highlighted line shows the query that obtains all links currently in the database, through the Link model all() method. As explained in a previous part of this series, this method is inherited from the parent Model class, and returns a collection with all database records associated with that model. The sortDesc() method is used to sort the resulting collection in descending order.

You’ll now change the highlighted line to use the database query sorting method orderBy(), which orders query results at the database level, instead of simply reordering the full set of rows that is returned as an Eloquent Collection via the all() method. You’ll also include a chained call to the limit() method in order to limit the query results. Finally, you’ll use the get() method to obtain the filtered resultset as an Eloquent Collection.

Replace your main route with the following code. The change is highlighted for your convenience:

routes/web.php
Route::get('/', function () {
    $links = Link::orderBy('created_at', 'desc')->limit(4)->get();

    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

The updated code will now pull the latest 4 links added to the database, no matter at which list. Because all links are added to lists, visitors can still go to specific lists to see the full list of links.

Next, you’ll learn how to paginate results to make sure all links are still accessible, even though they don’t load all at once on a single page.

Paginating Query Results

Your index page now limits the number of links that are listed, so that your page isn’t overloaded with content, and gets rendered in a shorter amount of time. While this solution works well in many cases, you’ll need to make sure visitors can still access older links that aren’t visible by default. The most effective way to do so is by implementing a pagination where users can navigate between multiple pages of results.

Laravel Eloquent has native methods to facilitate implementing pagination on database query results. The paginate() and simplePaginate() methods take care of generating pagination links, handling HTTP parameters to identify which page is currently being requested, and querying the database with the correct limit and offset in order to obtain the expected set of results, depending on the number of records per page you want to list.

You’ll now update the Eloquent queries in routes/web.php to use the simplePaginate() method, which generates a basic navigation with previous and next links. Unlike the paginate() method, simplePaginate() doesn’t show information about the total number of pages in a query result.

Open the routes/web.php file in your code editor. Start by updating the / route, replacing the limit(4)->get() method call with the simplePaginate() method:

routes/web.php
...
Route::get('/', function () {
    $links = Link::orderBy('created_at', 'desc')->simplePaginate(4);

    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});
...

Next, locate the /{slug} route definition in the same file, and replace the get() method with the simplePaginate() method. This is how the code should look once you’re done:

routes/web.php
...
Route::get('/{slug}', function ($slug) {
    $list = LinkList::where('slug', $slug)->first();
    if (!$list) {
        abort(404);
    }

    return view('index', [
        'list' => $list,
        'links' => $list->links()->orderBy('created_at', 'desc')->simplePaginate(4),
        'lists' => LinkList::all()
    ]);
})->name('link-list');
...

This is how the finished routes/web.php file will look once you’re finished. The changes are highlighted for your convenience:

routes/web.php
<?php

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

/*
|--------------------------------------------------------------------------
| 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 () {
    $links = Link::orderBy('created_at', 'desc')->simplePaginate(4);

    return view('index', [
        'links' => $links,
        'lists' => LinkList::all()
    ]);
});

Route::get('/{slug}', function ($slug) {
    $list = LinkList::where('slug', $slug)->first();
    if (!$list) {
        abort(404);
    }

    return view('index', [
        'list' => $list,
        'links' => $list->links()->orderBy('created_at', 'desc')->simplePaginate(4),
        'lists' => LinkList::all()
    ]);
})->name('link-list');

Save the file when you’re done.

The database queries are now updated, but you still need to update your front end view to include code that will render the navigation bar. The resulting Eloquent collection obtained with simplePaginate() contains a method called links(), which can be called from the front end view to output the necessary HTML code that will render a navigation section based on a paginated Eloquent query.

You can also use the links() method in a paginated Eloquent collection to access the inherent paginator object, which provides several helpful methods to obtain information about the content such as the current page and whether there are multiple pages of content or not.

Open the resources/views/index.blade.php application view in your code editor:

resources/views/index.blade.php

Locate the end of the section tagged with the links class, which contains a foreach loop where links are rendered. Place the following piece of code after that section and before the final </div> tag on that page:

resources/views/index.blade.php
        @if ($links->links()->paginator->hasPages())
            <div class="mt-4 p-4 box has-text-centered">
                {{ $links->links() }}
            </div>
        @endif

This code checks for the existence of multiple pages of results by accessing the paginator object and calling the hasPages() method. When that method returns true, the page renders a new div element and calls the links() method to print the navigation links for the related Eloquent query.

This is how the updated index.blade.php page will look like once you’re finished:

resources/views/index.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>My 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">
            @if (isset($list))
                {{ $list->title }}
            @else
                Check out my awesome links
            @endif
        </h1>
        <p class="subtitle">
            @foreach ($lists as $list)<a href="{{ route('link-list', $list->slug) }}" title="{{ $list->title }}" class="tag is-info is-light">{{ $list->title }} ({{ $list->links()->count() }})</a> @endforeach
        </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>
                    <p class="mt-2"><a href="{{ route('link-list', $link->link_list->slug) }}" title="{{ $link->link_list->title }}" class="tag is-info">{{ $link->link_list->title }}</a></p>
                </div>
            @endforeach
        </section>

        @if ($links->links()->paginator->hasPages())
            <div class="mt-4 p-4 box has-text-centered">
                {{ $links->links() }}
            </div>
        @endif
    </div>
</section>
</body>
</html>

Save the file when you’re done updating it. If you go back to your browser window and reload the application page now, you’ll notice a new navigation bar whenever you have more than 4 links in the general listing or in any individual link list page.

Updated application Landing Laravel with content pagination

With a functional pagination in place, you can grow your content while making sure that older items are still accessible to users and search engines. In cases where you need only a fixed amount of results based on a certain criteria, where pagination is not necessary, you can use the limit() method to simplify your query and guarantee a limited result set.

In a previous section of this series, you updated an existing Artisan command in order to support the new lists feature. Although there are commands to insert and delete links, the demo application currently doesn’t have a command to edit existing links. This can be useful to move links between lists, for instance, or update a link description.

In this guide, you’ll create a new Artisan command to update existing links in the database.

From your terminal, first make sure you’re in your project’s root directory, then run the following to bootstrap a new Artisan command:

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

This will create a new LinkUpdate.php file located at app/Console/Commands. Open the file in your code editor of choice:

app/Console/Commands/LinkUpdate.php

This file contains boilerplate code for a new Artisan command. You’ll update it to handle editing a link provided its unique id. This is what your handle() method needs to do:

  • Obtain an id provided by the user and check for the existence of a link with a matching id in the database.
  • If a valid link cannot be found, show an error message and exit.
  • If a valid link is found, prompt the user to provide updated values for the link description and link list.
  • Ask the user to confirm changes.
  • When confirmed, update the item in the database.

Start by including a couple use definitions at the top of the file, to facilitate referencing to the Link and LinkList classes later on:

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

namespace App\Console\Commands;

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

...

To obtain the link id, you should set up a mandatory argument in the new link:update command, so that users are required to provide that parameter at run time. Locate the command signature definition at the top of the file and replace it with the highlighted line:

app/Console/Commands/LinkUpdate.php
...

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

If you save the file and try to run the command now without an additional argument, you’ll get an error:

  • docker-compose exec app php artisan link:update
Output
Not enough arguments (missing: "link_id").

In the handle() method, you need to obtain the link id provided by the user and locate it in the database. This can be done with the argument() method that is provided through the parent Command class. Then, you can use the find() Eloquent method to query the database for a link with that id. If the find() method returns null, it means no link with that id was found, so the program should exit in error.

app/Console/Commands/LinkUpdate.php
...
   /**
     * 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;
        }

        // obtain updated information from user
    }
...

When a valid link is found, you need to prompt the user for the updated link information.You can do so using the ask method, highlighted in the next example:

app/Console/Commands/LinkUpdate.php: function handle()
...
        if ($link === null) {
            $this->error("Invalid or non-existent link ID.");
            return 1;
        }

        $link->description = $this->ask('Link Description (ENTER to keep current)') ?? $link->description;
        $list_name = $this->ask('Link List (ENTER to keep current)') ?? $link->link_list->title;
...

This code will prompt the user for an updated description and list, while keeping the current values as default in case a user doesn’t provide new ones, pressing ENTER to skip the prompt.

Once you have all this information, you can proceed to the update. It’s a good idea to use the confirm() method to have the user confirm the changes before you run the database update. This is how such code would look:

app/Console/Commands/LinkUpdate.php: function handle()
...
        $link->description = $this->ask('Link Description (ENTER to keep current)') ?? $link->description;
        $list_name = $this->ask('Link List (ENTER to keep current)') ?? $link->link_list->title;

        $this->info("Description: $link->description");
        $this->info("Listed in: " . $list_name);

        if ($this->confirm('Is this information correct?')) {
            //code that updates the link
        }
...

Inside the if block, you have to start by checking if the requested list exists, otherwise create a new list with the provided name. Then, you’ll use the associate() method to update the relationship between this link and its “parent” list. The save() method, finally, will persist the changes to the database:

app/Console/Commands/LinkUpdate.php: function handle()
...
        if ($this->confirm('Is this information correct?')) {
            $list = LinkList::firstWhere('slug', $list_name);
            if (!$list) {
                $list = new LinkList();
                $list->title = $list_name;
                $list->slug = $list_name;
                $list->save();
            }
            $link->link_list()->associate($list)->save();
            $this->info("Updated.");
        }
...

This is the complete LinkUpdate.php file for your reference:

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

namespace App\Console\Commands;

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

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Update a link in 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;
        }

        $link->description = $this->ask('Link Description (ENTER to keep current)') ?? $link->description;
        $list_name = $this->ask('Link List (ENTER to keep current)') ?? $link->link_list->title;

        $this->info("Description: $link->description");
        $this->info("Listed in: " . $list_name);

        if ($this->confirm('Is this information correct?')) {
            $list = LinkList::firstWhere('slug', $list_name);
            if (!$list) {
                $list = new LinkList();
                $list->title = $list_name;
                $list->slug = $list_name;
                $list->save();
            }
            $link->link_list()->associate($list)->save();
            $this->info("Updated.");
        }

        return 0;
    }
}

Note: For more detailed information on Artisan commands, check our guide on How To Create Artisan Commands to Manage Database Records in Laravel, which is part of our introductory Laravel series.

Save the file when you’re finished. Then, use the link:show command to obtain all links and its respective IDs:

  • docker-compose exec app php artisan link:show
Output
+----+-------------------------------------------------+--------------+----------------------------------+ | id | url | list | description | +----+-------------------------------------------------+--------------+----------------------------------+ | 1 | https://digitalocean.com/community | default | DO Community | | 2 | https://digitalocean.com/community/tags/laravel | default | Laravel Tutorias at DigitalOcean | | 3 | https://digitalocean.com/community/tags/php | default | PHP Tutorials at DigitalOcean | | 4 | https://twitter.com/digitalocean | social | Twitter | | 5 | https://dev.to/digitalocean | social | DEV.to | | 6 | https://laravel.com/docs/8.x/eloquent | default | Laravel Eloquent Docs | +----+-------------------------------------------------+--------------+----------------------------------+

Then, choose an item to edit. For instance, you may want to create a digitalocean list for the links that point to the DigitalOcean website (that would correspond to items with IDs 1, 2, and 3 in the previous example output).

To update the link with ID 1, run:

  • docker-compose exec app php artisan link:update 1
Output
Link Description (ENTER to keep current): > DO Community Link List (ENTER to keep current): > digitalocean Description: DO Community Listed in: digitalocean Is this information correct? (yes/no) [no]: > y Updated.

Then, run the link:show command again to see the updated information:

Output
+----+-------------------------------------------------+--------------+----------------------------------+ | id | url | list | description | +----+-------------------------------------------------+--------------+----------------------------------+ | 1 | https://digitalocean.com/community | digitalocean | DO Community | | 2 | https://digitalocean.com/community/tags/laravel | digitalocean | Laravel Tutorias at DigitalOcean | | 3 | https://digitalocean.com/community/tags/php | digitalocean | PHP Tutorials at DigitalOcean | | 4 | https://twitter.com/digitalocean | social | Twitter | | 5 | https://dev.to/digitalocean | social | DEV.to | | 6 | https://laravel.com/docs/8.x/eloquent | default | Laravel Eloquent Docs | +----+-------------------------------------------------+--------------+----------------------------------+

In this guide, you learned how to update database records with Laravel Eloquent. You have upgraded the demo application to include a new command that allows users to edit existing links in the database.

In the next and final part of this series, you’ll create a new command to delete a list of links.

In Eloquent, you can delete database records conveniently with the delete method from the parent Model class. The link:delete command, already implemented within the base version of the demo application, deletes links based on a valid link id. The application is still missing a command to delete lists.

In the last part of this series, you’ll create a new command to delete lists. For simplicity, any links associated with the list to be deleted will be reassigned to the default link list.

From your terminal, run the following to bootstrap a new Artisan command:

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

This will create a new ListDelete.php file located at app/Console/Commands. Open the file in your code editor of choice:

app/Console/Commands/ListDelete.php

You’ll update this code to handle deleting a link list provided its unique slug, which is a URL-friendly name used to identify each list.

This is what your handle() method needs to do:

  • Obtain a slug provided by the user and check for the existence of a list with a matching slug in the database.
  • If a valid list cannot be found, show an error message and exit.
  • If a valid list is found, prompt the user to confirm.
  • Reassign to the default list any links associated with the list that will be deleted.
  • Delete the list from the database.

If you’ve been following along with all parts of the series so far, you have implemented similar code before when creating the LinkUpdate command. The main difference now is that you won’t need to prompt the user for additional info, and before running the delete() method you’ll need to run a mass update to change associated links to a different list.

Replace the boilerplate code in your ListDelete.php file with the following:

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

namespace App\Console\Commands;

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

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Delete Lists';

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

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $list_slug = $this->argument('list_slug');
        $list = LinkList::firstWhere('slug', $list_slug);

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

        if ($this->confirm("Confirm deleting the list '$list->title'? Links will be reassigned to the default list.")) {
            $default_list = LinkList::firstWhere('slug', 'default');
            if (!$default_list) {
                $default_list = new LinkList();
                $default_list->title = 'default';
                $default_list->slug = 'default';
                $default_list->save();
            }

            $this->info("Reassigning links to default list...");

            Link::where('link_list_id', $list->id)->update(['link_list_id' => $default_list->id]);

            $list->delete();
            $this->info("List Deleted.");
        }

        return 0;
    }
}

Save the file.

In the previous code, the handle() method starts by trying to locate a link list based on the provided slug. If a valid list can’t be found, the application exits in error. When a valid list is found, the confirm() method is called to ask the user for confirmation.

When confirmed, the application will locate the default list or create a new one if necessary, assigning it to the $default_list variable.

Next, it will locate and update all links that are associated with the list that is about to be deleted. The chained call to update() will update the referenced list ID on all links that match the query, using the condition defined within the previous where() call. This line is highlighted for your reference.

Finally, the list is deleted with the delete() method, also highlighted. This method is available to all Eloquent models through the parent Model class.

To delete a list, first run link:show to obtain all links currently in the database:

  • docker-compose exec app php artisan link:show
Output
+----+-------------------------------------------------+--------------+----------------------------------+ | id | url | list | description | +----+-------------------------------------------------+--------------+----------------------------------+ | 1 | https://digitalocean.com/community | digitalocean | DO Community | | 2 | https://digitalocean.com/community/tags/laravel | digitalocean | Laravel Tutorias at DigitalOcean | | 3 | https://digitalocean.com/community/tags/php | digitalocean | PHP Tutorials at DigitalOcean | | 4 | https://twitter.com/digitalocean | social | Twitter | | 5 | https://dev.to/digitalocean | social | DEV.to | | 6 | https://laravel.com/docs/8.x/eloquent | default | Laravel Eloquent Docs | +----+-------------------------------------------------+--------------+----------------------------------+

To delete the digitalocean list and revert those links back to the default list, run:

  • docker-compose exec app php artisan list:delete digitalocean

Confirm the deletion by typing y and hitting ENTER.

Output
Confirm deleting the list 'digitalocean'? Links will be reassigned to the default list. (yes/no) [no]: > y Reassigning links to default list... List Deleted.

If you run the link:show() command again, you’ll see the updated information:

Output
+----+-------------------------------------------------+---------+----------------------------------+ | id | url | list | description | +----+-------------------------------------------------+---------+----------------------------------+ | 1 | https://digitalocean.com/community | default | DO Community | | 2 | https://digitalocean.com/community/tags/laravel | default | Laravel Tutorias at DigitalOcean | | 3 | https://digitalocean.com/community/tags/php | default | PHP Tutorials at DigitalOcean | | 4 | https://twitter.com/erikaheidi | social | Twitter | | 5 | https://dev.to/erikaheidi | social | DEV.to | | 6 | https://laravel.com/docs/8.x/eloquent | default | Laravel Eloquent Docs | +----+-------------------------------------------------+---------+----------------------------------+

The application now has a dedicated command to delete lists of links.

Series Conclusion

Eloquent is a powerful ORM that comes built in with the Laravel framework. It abstracts many of the complex queries that are necessary to deal with relationships, to insert, update, and delete database records, and to search, filter, and limit query results.

In this project-based series, you’ve learned how to work with one-to-many relationships in Laravel Eloquent, how to query the database, how to order and paginate results, and how to perform the basic operations of inserting, updating, and deleting records in the database.

For more detailed information about Laravel Eloquent and everything that you can do with this tool, check the official documentation.