Laravel Queries: Learn by Building a Data Table

September 02, 2025
Written by
Kenneth Ekandem
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Laravel Queries: Learn by Building a Data Table

Laravel is an open-source PHP framework that enables developers to build robust web applications and APIs with ease and efficiency.

In recent years, Laravel has become the go-to framework for PHP developers. Its initial MVC structure made it relatively easy to build web applications. And by introducing new components with each new release, Laravel has become ever-more feature-rich in areas such as Pub/Sub, validation, database management, testing, and today's topic: queries.

This tutorial will show how to write queries with Laravel's Query Builder, showing a range of great examples, and finish off by building a data table; a great way to summarize the tutorial.

Whether you're a seasoned developer looking to enhance your Laravel skills, or a beginner eager to explore the world of database interaction, you should be able to learn something new about Laravel queries through today’s hands-on learning.

Prerequisites

  • PHP 8.3 or 8.4
  • Composer installed globally
  • Moderate knowledge of PHP & Laravel

What is a (SQL) query?

A query is an action to retrieve information from a database. A database query can either be a DDL (Data Definition Language) query or a DML (Data Manipulation Language) query.

A DML query requires data input to perform operations that modify the database such as creating, updating, deleting records. In contrast, a DDL query fetches information without making any changes from the database and serves to the client.

Laravel's Query Builder

A web application often needs to interact with a database to store data. Laravel makes this task hassle-free with the Laravel Query Builder, a tool in Laravel’s ecosystem that simplifies database interactions, and empowers developers to write cleaner, more maintainable code.

Queries fetch data from a database and also update existing data in the database. CRUD (Create, Read, Update, and Delete) operations are a great way to use queries. Options like readcan be expanded into aggregating data, and retrieving data using table joins, etc.

The Query Builder provides an easy interface to create and execute database queries, and offers a concise and readable syntax — a boon for those new to web development. It can be used to perform most database operations in your application and is compatible with database systems like MySQL, PostgreSQL, and SQLite. Creating queries is straightforward. You start with a table, then chain functions to specify conditions, column selection, ordering, and more.

The basics of the Query Builder

Now, let’s explore some fundamental aspects of query building in Laravel. Below are a couple of query builder functions, like select()which fetches data, supports filtering capabilities, and aggregationsthat can group retrieved information based on data sent from the client.

Select clauses

Select all rows

Using the combination of the select() and get() functions, clauses can be built like a chain, with more additional options to further filter the results required for that query. For example, an admin could look for the names and votes of all users:

$users = DB::table('users')->select('name', 'votes')->get();

The results will be an array of objects, as in the example output below.

[{"name":"kenneth ekandem","votes":"100"}]%
Add select conditions

Say the admin intends to filter further and add more options like age. The addSelect() can be used as a conditional statement. If the value is null, the "name" and "votes" columns will be the only ones returned. Otherwise, the column name set in $value is added to the select query, and the result is sent back.

$query = DB::table('users')->select("name", "votes");
if ($value) {
    $query->addSelect($value /* can be age */); 
}
$result = $query->get();
return response()->json($result, 200);

Below are sample outputs when $value does and does not have a value.

[{"name":"kenneth ekandem","votes":"100","age":"20"}]
[{"name":"kenneth ekandem","votes":"100"}]

Where clauses

The where clause is a conditional clause that takes in one or more parameters and queries one or multiple tables for matches. The clause can also maintain a chain of where clauses to handle multiple conditional statements.

Just as the select() clause specifies the columns to be selected, the where() clause filters columns to be fetched by passing boolean operations as argument, and retrieving the rows that pass the condition.

The code below adds awhere() clause to our query.

$users = DB::table('users')->where('id', 2)->get();
$users = DB::table('users')->where('votes', '=', 100)->get();

You can also chain multiple where() calls for more complex queries:

$users = DB::table('users')
           ->where('votes', '>=', 100)
           ->where('age', '>=', 20)
           ->get();
$matchThese = [
    ['votes', '>=', 100],
    ['age', '>=', 20]
]
$results = User::where($matchThese)->get()

In the above code:

  • There is more than one where() clause. As seen in the first line, we filtered rows to be fetched based on two conditions; the number of votes and the user's age.
  • Further down, the where clauses were reduced to one, using an array. This approach prevents the query from checking the conditions multiple times in the database, which is more efficient.

Group By clauses

When writing queries, the Group By clause allows us to group records from a database table based on a specific column or columns. In the example below, the column or columns of interest are passed as arguments to the groupBy() function, and then the column, in this case "age", will be sent back as the response payload:

$users = DB::table('users')
    ->select('age', DB::raw('count(*) as total, age'))
    ->where('age', '>=', 1)
    ->groupBy('age')
    ->get();

The result below (formatted for readability), shows the number of users in a given age group.

[
    {"age":90,"total":2},
    {"age":76,"total":1},
    {"age":88,"total":1},
    {"age":45,"total":1},
    {"age":39,"total":1},
    {"age":81,"total":2},
    {"age":69,"total":1}
]

Distinct clauses

The Distinct clause is used to fetch unique records from the database. It is very useful to find, for example, how many users made comments on a post, since users can make more than one comment. It can also be helpful to get the distinct number of users that commented. As part of Laravel Query Builder, the distinct clause can be chained to other Query Builder functions.

By default, the distinct() function doesn't take any arguments, such as when fetching distinct records from the 'users' table in the example below.

$users = DB::table('users')->distinct()->get();

Join clauses

Join clauses are used to make self-referential database queries or combine rows from two or more tables based on a related foreign key that connects the tables.

The first argument passed to the join() function is the name of the table you need to join to, while the remaining arguments specify the column constraints for the join. This can be done to modify and make new compound tables, or done as an initial step before performing more complex querying on them. Laravel supports four primary types of joins: inner, left, right, and full outer joins.

Inner joins

These return only the matching rows from both tables. Suppose you have two tables,"users"and"orders", and you want to fetch a list of orders for the users present. You can use the join() function for this.

Here's how to do it, joining the "users" and "orders" tables on the "id" and "user_id" columns, respectively.:

$users = DB::table('users')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.name', 'orders.order_date')
    ->get();
Left join

These return all rows from the left table and the matching rows from the right table. NULL values are returned for the right table if there are no matches. This statement is implemented with the leftJoin() function, and its arguments are similar to the innerJoin() function.

Let's say you want to retrieve a list of all users and their orders, including those who haven't placed any orders. You can use a left join like in the example below:

$users = DB::table('users')
    ->leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.name', 'orders.order_date')
    ->get();
Right join

Similar to a left join, the right join includes all rows from the right table and the matching rows from the left table. For example, to get a list of all orders and their associated users:

$users = DB::table('orders')
    ->rightJoin('users', 'users.id', '=', 'orders.user_id')
    ->select('users.name', 'orders.order_date')
    ->get();
Full outer join

These return all rows when there is a match in either the left or right table, and NULL values where there are no matches. Laravel's Query Builder does not provide a specific function for a full outer join. To perform one, you combine a left join and a union, as in the following example:

$users = DB::table('users')
    ->leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.name', 'orders.order_date')
    ->union(
        DB::table('users')
            ->rightJoin('orders', 'users.id', '=', 'orders.user_id')
            ->select('users.name', 'orders.order_date')
    )
    ->get();

In this example, we first perform a left join and then a right join, combining the results using the union() function.

Subquery joins

A subquery or inner query is a nested query used to filter the results of an outer query. Subqueries can be used as an alternative to joins and are usually nested inside the where clause. However, it's worth keeping in mind that subqueries can be slower than joins, and harder to read.

You can use subqueries in your joins like in these examples:

$latestPosts = DB::table('posts')
                 ->select('user_id', DB::raw('MAX(created_at) as last_post_created_at'))
                 ->where('is_published', true)
                 ->groupBy('user_id');
$users = DB::table('users')
           ->joinSub($latestPosts, 'latest_posts', function ($join) {
               $join->on('users.id', '=', 'latest_posts.user_id');
           })->get();

In the code above, the users table was queried with a subquery on the "latest_posts" table to fetch the user's latest posts.

Delete clauses

The Query Builder's delete() function is used to delete records from a table, and returns the number of affected rows. You may constrain deleteclauses by adding where clauses before calling the delete() function. For example

$deleted = DB::table('users')->delete();
 $deleted = DB::table('users')->where('votes', '>', 100)->delete();

If you wish to truncate an entire table, which remove all records from the table and resets any auto-incrementing columns to zero, use the truncate() function:

$truncated = DB::table('users')->truncate();

Aggregate clauses

The Query Builder also provides a variety of aggregate functions, such as count(), max(), min(), avg() and sum() which can be used as follows:

$usersCount = DB::table('users')->count();
$priceMax = DB::table('orders')->max('price');
$priceAvg = DB::table('orders')->avg('price');

Error handling in Queries

Error handling is essential to ensure application stability and maintain data integrity. Laravel simplifies this process by providing a built-in exception handler located in the App\Exceptions\Handler class. This class allows developers to log, report, and render exceptions efficiently.

By default, Laravel converts exceptions into HTTP responses, but you can customize this behavior to include specific status codes or detailed error messages.

For example, you can wrap database queries in a try-catch block to handle errors gracefully:

try {
    $users = DB::table('users')->get();
} catch (\Exception $e) {
    // Handle the exception
}

When building APIs, it’s particularly important to return meaningful error responses. A good practice is to include an HTTP status code and a JSON payload with error details, like so:

{
    "error": "Resource not found"
}

For more advanced error handling techniques, including customizing exception rendering and returning machine-readable API errors, refer to the Laravel documentation on error handling.

Implementing a data table

A data table is a common component in web applications, displaying rows of data in a structured format. Let's start implementing one in Laravel which will return the result to a view that can be interacted with on the front end.

Below is an example of it. The assets files can be found in the GitHub repository for this tutorial.

List of contacts showing name, email, phone, address, age, and occupation details.
A contact list displayed with a filter options panel open on the right side.

When completed, we'll be able to:

  • Fetch all contacts
  • Filter entries by selected parameters, for example: name, email, votes, etc
  • Sort entries by date, counts, status, etc

Project set up

First, let’s set up a Laravel project using Composer and change into the new project directory, using the commands below. We’ll call the project "laravel_datatable" .

composer create-project laravel/laravel laravel_datatable
cd laravel_datatable

Next, install the Livewire package using the below command:

composer require livewire/livewire

Create the database migrations

We’ll create two tables, a "votes" table and a "contacts" table. This means we’ll be creating the migrations, models, and factories for both tables.

Let’s begin by creating a migration class for the "contacts" table:

php artisan make:migration create_contacts_table

Add the below columns to the contacts migration file in database/migrations/create_contacts_table.php by replacing the public up() function with the one below:

public function up(): void
{
    Schema::create('contacts', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->text('address');
        $table->integer('age');
        $table->string('phone');
        $table->string('occupation');
        $table->timestamps();
    });
}

Next, for the "votes" table, run the following command:

php artisan make:migration create_votes_table

Adding the relevant columns to the "votes" migration class ( database/migrations/2025_02_27_224310_create_votes_table.php) by updating the up() function to match the following:

public function up(): void
{
    Schema::create('votes', function (Blueprint $table) {
        $table->id();
        $table->foreignId('contact_id')->constrained('contacts');
        $table->integer('votes');
        $table->timestamps();
    });
}

Now, run the migrations to provision the database:

php artisan migrate

Set up the model

Create the model for the "contacts" table, by running the following command:

php artisan make:model Contacts

Once done, update the content of app/Models/Contacts.php with this:

<?php

namespace App\Models;

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

class Contacts extends Model
{
    use HasFactory;

    protected $table = "contacts";
    protected $primaryKey = "id";
    protected $fillable = [
        'name',
        'email',
        'phone',
        'address',
        'age',
        'occupation'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

The user() function defines a one-to-one relationship between the contacts and users table.

Next, create a model for the "votes" table with the following command:

php artisan make:model Votes

Then, update the content of app/Models/Votes.php with the following code:

<?php

namespace App\Models;

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

class Votes extends Model
{
    use HasFactory;

    protected $fillable = ['contact_id', 'vote_count'];

    public function contact()
    {
        return $this->belongsTo(Contacts::class);
    }
}

Set up a factory to automatically generate contacts

Factories are used to generate dummy data for developer testing purposes. These records are generated in a factory file and uploaded to a table in the database. To create a contact factory file, run the command below:

php artisan make:factory ContactsFactory

Update the content of the generated database/factories/ContactsFactory.php file with the following code:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ContactsFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'address' => $this->faker->address(),
            'phone' => $this->faker->phoneNumber(),
            'age' => fake()->randomNumber(2),
            'occupation' => $this->faker->jobTitle()
        ];
    }
}

In the above code, the contact factory generates dummy data like name, email, etc.

Applying similar changes, we’ll create the VotesFactory class with the following command:

php artisan make:factory VotesFactory

Now, update database/factories/VotesFactory.php with the following code:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class VotesFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'votes' => fake()->numberBetween(0, 1000),
        ];
    }
}

Set up database seeding

We will seed the database with a few contacts before we begin querying it. First up, create a seeder file by running the command below.

php artisan make:seeder ContactsSeeder

Then, update the database/seeders/ContactsSeeder.php file with the following code:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class ContactsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        \App\Models\Contacts::factory(20)->create()->each(function ($contact) {
            \App\Models\Votes::factory()->create([
                'contact_id' => $contact->id
            ]);
        });
    }
}

In the above code, the contacts factory is imported and used to generate random 20 records that are then uploaded to the "contacts" table.

Next, update the database/seeders/DatabaseSeeder.php file with the code block below:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

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

This calls the ContactsSeeder class which gives us some data to work with. 

Seed the database by running this command:

php artisan db:seed

We should now see a database/database.sqlite file in our directory. Now that we have some data in our database, we can go ahead to create the data table. 

Build the data table

Now, let's switch to building the view resources for our project. We will build some templates that use our data table containing all the contact data and the option to filter them.

Create the table component

To create the view components like the contacts page, we will generate a Livewire component. Livewire components usually come in pairs, the "resource/view" component and the "controller" component for the resource.

Create the "Contacts" component with Livewire using the following command:

php artisan make:livewire Contacts

This creates two files: resources/views/livewire/contacts.blade.php and app/Livewire/Contacts.php.

Next, let’s navigate to the app/Livewire/Contacts.php file and replace its content with the following code:

<?php

namespace App\Livewire;

use Livewire\Component;
use Illuminate\Support\Facades\DB;
use Livewire\WithPagination;

class Contacts extends Component
{
    use WithPagination;

    public $search = "";
    protected $queryString = ['search'];

    public function render()
    {
        $query = DB::table("contacts")
                    ->leftJoin('votes', 'contacts.id', '=', 'votes.contact_id')
                    ->select('contacts.*', 'votes.votes');

        $contacts = $query->simplePaginate(10);
        return view('livewire.contacts', ['contacts' => $contacts]);
    }
}

The code above does the following:

  • The $search property is initialized as an empty string and the $queryString property is set to track the "search" parameter in the URL.

  • Inside of the render() function:

    • $query = DB::table("contacts") creates a query builder instance.

    • leftJoin() combines data from two separate tables. It uses the "contacts" table as a base and then uses a left join operation to connect it with the "votes" table. The join is created by matching the "id" column from the "contacts" table with the "contact_id" column in the "votes" table.

    • select() retrieves all columns from the "contacts" table ("contacts.*") along with just the "votes" column from the "votes" table.

    • $query->simplePaginate(10) is used to execute the query with paginations limiting the output to fetch 10 contacts per page.

    • The "contacts" data is then returned and can be accessed from the contacts page in resources/views/livewire/contacts.blade.php.

Next, let’s navigate to the contacts.blade.php file and replace its content with the following code:

<div>
  <div class="container">
    <div class="row">
      <div class="col-12">
        <div class="p-6 rounded-lg shadow-lg">
          <h2 class="text-2xl font-bold mb-2">Contact List</h2>
          <div class="row">
            <div class="col-8">
              <div class="flex items -center">
                <input
                  type="text"
                  class="border p-2 w-80"
                  placeholder="Search Contacts"
                  wire:model="search"
                />
                <button class="btn btn-primary text-white p-2">
                  Search <i class="fa-solid fa-magnifying-glass"></i>
                </button>
              </div>
            </div>
            <div class="col-4 d-flex justify-content-end align-content-end">
              <button
                class="btn btn-primary align-self-end"
                type="button"
                data-bs-toggle="offcanvas"
                data-bs-target="#offcanvasRight"
                aria-controls="offcanvasRight"
              >
                <i class="fa-solid fa-filter"></i>
                Filter By:
              </button>
            </div>
          </div>
        </div>
      </div>
      <div class="col-12">
        <div
          class="relative overflow-x-auto w-70"
          style="background: #e8e3e3; border: 1px solid #e8e3e3"
        >
          <table
            class="w-full text-sm text-left rtl:text-right
                    text-gray-500 dark:text-gray-400"
          >
            <thead
              class="text-xs text-gray-700 uppercase bg-gray-50
                   dark:bg-gray-700 dark:text-gray-400"
            >
              <tr>
                <th scope="col" class="px-6 py-3">Name</th>
                <th scope="col" class="px-6 py-3">Email</th>
                <th scope="col" class="px-6 py-3">Phone</th>
                <th scope="col" class="px-6 py-3">Address</th>
                <th scope="col" class="px-6 py-3">Age</th>
                <th scope="col" class="px-6 py-3">Votes</th>
                <th scope="col" class="px-6 py-3">Occupation</th>
              </tr>
            </thead>
            <tbody>
              @foreach($contacts as $contact)
              <tr
                class="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
              >
                <th scope="row" class="px-6 py-4 font-medium whitespace-nowrap">
                  {{ $contact->name}}
                </th>
                <td class="px-6 py-4">{{$contact->email}}</td>
                <td class="px-6 py-4">{{$contact->phone}}</td>
                <td class="px-6 py-4">{{$contact->address}}</td>
                <td class="px-6 py-4">{{$contact->age}}</td>
                <td class="px-6 py-4">{{$contact->votes}}</td>
                <td class="px-6 py-4">{{$contact->occupation}}</td>
              </tr>
              @endforeach
            </tbody>
          </table>
          <div class="d-flex">{!! $contacts->links() !!}</div>
        </div>
      </div>
    </div>
  </div>
</div>

The code block above renders a basic HTML table, with all the rows being populated using data returned from the Contacts.php file via a foreach loop. It also contains an input for searching data and a button for the filter, which we’ll get to soon.

Next, in resources/views/welcome.blade.php, include the contacts livewire page.

<div class="bg-color">
<livewire:contacts />
</div>
@livewireScripts

This demo app uses Bootstrap for styling; we’ll be using the Bootstrap CDN. Head over to the Git repo for this demo, copy the content of the welcome.blade.php file and paste it in your resources/views/welcome.blade.php.

Start the application

To start up our development server, let’s open up the terminal and run:

php artisan serve

We can then open up the browser at http://localhost:8000 to see our UI.

We added a search input and button in the contacts.blade.php file earlier. Let’s now get the search functionality working. Navigate to the render() function in app/Livewire/Contacts.php file and update it to match the following code:

public function render()
{
    $query = DB::table("contacts")
            ->leftJoin('votes', 'contacts.id', '=', 'votes.contact_id')
            ->select('contacts.*', 'votes.votes');

    if ($this->search) {
        $query->where('name', 'like', '%' . $this->search . '%')
            ->orWhere('email', 'like', '%' . $this->search . '%');
    }

    $contacts = $query->simplePaginate(10);
    return view('livewire.contacts', ['contacts' => $contacts]);
}

Then, add the following function to the bottom of the class.

public function applyFilters()
{
    $this->resetPage();
}

In the code above:

  • The applyFilters() function comes after the above render() function

  • There’s an if statement that checks if there’s a search term, and adds where clauses if true.

    • We use a slightly more complex where clause to search for queries that match with data from either the "name" or "email" columns.

    • The like operator is used for pattern matching with wildcards.

    • "%" is a wildcard that matches any character. For example, if the search query is %john% matches: "John Doe", "Johnny", "Elton John".

  • The applyFilters() function, in turn, invokes Livewire’s resetPage() pagination function. It resets the pagination to the first page in order to:

    • Show results from page one

    • Prevent staying on a page number that might not exist with new filters

Next, In the contacts.blade.php file, where we have the search input field and button (lines 10 - 18), update them to match the following:

<input type="text" class="border p-2 w-80" 
    placeholder="Search Contacts"
    wire:model="search"
    wire:keydown.enter="applyFilters">

<button class="btn btn-primary text-white p-2"
    wire:click="applyFilters">Search
    <i class="fa-solid fa-magnifying-glass"></i>
</button>
  • The wire:model="search" directive is used to create a two-way binding of the search property in the Contacts.php file to the input

  • In order to trigger search when the enter key is pressed, we add the wire:keydown.enter="applyFilters" directive to the input field

  • To trigger search when the button is clicked, we add the wire:click="applyFilters" directive to the search button

So far, our app should look like the demo below. We can search and move through the different pages of our table.

Contact list with names, emails, phone numbers, addresses, ages, and occupations, including search and filter options.

Filtering Data

To complete our app, we’ll implement filters that let us filter data based on certain factors, such as age, occupation, and votes. Building on our knowledge of the different clauses, we’ll implement the filter logic for these three data properties.

Let’s navigate to the app/Livewire/Contacts.php file where:

  • Just under the public $search = "" declaration, we’ll store the filter criteria in an associative array.

  • We'll set up the $queryString property to keep track of the filters parameter in the URL, in addition to the existing search parameter.

  • Inside the render() function, we’ll add the various logic for all of the filter criteria. Each of the conditional statements will check whether the corresponding filter criteria is null or an empty string, and set the corresponding database query.

  • The resetFilters() function will set the values of each of the criteria in the $filters array to null.

Update it to match the following code:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\DB;
use Livewire\Component;
use Livewire\WithPagination;

class Contacts extends Component
{
    use WithPagination;

    public $filters = [
        'age_min' => null,
        'age_max' => null,
        'occupation' => null,
        'min_votes' => null,
        'max_votes' => null,
    ];

    public $search = "";

    protected $queryString = ['search', 'filters'];

    public function render()
    {
        $query = DB::table("contacts")
            ->leftJoin('votes', 'contacts.id', '=', 'votes.contact_id')
            ->select('contacts.*', 'votes.votes');

        if ($this->search) {
            $query->where('name', 'like', '%' . $this->search . '%')
                ->orWhere('email', 'like', '%' . $this->search . '%');
        }

        if ($this->filters['age_min']) {
            $query->where('age', '>=', $this->filters['age_min']);
        }
        if ($this->filters['age_max']) {
            $query->where('age', '<=', $this->filters['age_max']);
        }
        if ($this->filters['occupation']) {
            $query->where('occupation', $this->filters['occupation']);
        }
        if ($this->filters['min_votes']) {
            $query->where('votes', '>=', $this->filters['min_votes']);
        }
        if ($this->filters['max_votes']) {
            $query->where('votes', '<=', $this->filters['max_votes']);
        }

        $contacts = $query->simplePaginate(10);
        return view('livewire.contacts', [
            'contacts' => $contacts,
            'occupations' => \App\Models\Contacts::distinct('occupation')->pluck('occupation')
        ]);
    }

    public function applyFilters()
    {
        $this->resetPage();
    }

    public function resetFilters()
    {
        $this->filters = [
            'age_min' => null,
            'age_max' => null,
            'occupation' => null,
            'min_votes' => null,
            'max_votes' => null
        ];
        $this->resetPage();
    }
}

We’ve added the filter logic, let’s wrap it up with the filters view. Navigate to the contacts.blade.php file and add the following code after the DIV with the "container" class:

<div class="offcanvas offcanvas-end"
     tabindex="-1"
     id="offcanvasRight"
     aria-labelledby="offcanvasRightLabel"
>
    <div class="offcanvas-header">
        <h5 class="offcanvas-title" id="offcanvasRightLabel">Filter Contacts</h5>
        <button type="button"
                class="btn-close"
                data-bs-dismiss="offcanvas"
                aria-label="Close"
        ></button>
    </div>
    <div class="offcanvas-body">
        <div class="mb-4">
            <label class="form-label">Age Range</label>
            <div class="d-flex gap-2">
                <input type="number"
                       class="form-control"
                       placeholder="Min Age"
                       wire:model="filters.age_min"
                />
                <input type="number"
                       class="form-control"
                       placeholder="Max Age"
                       wire:model="filters.age_max"
                />
            </div>
        </div>
        <div class="mb-4">
            <label class="form-label">Occupation</label>
            <select class="form-select" wire:model="filters.occupation">
                <option value="">All Occupations</option>
                @foreach($occupations as $occupation)
                    <option value="{{ $occupation }}">{{ $occupation }}</option>
                @endforeach
            </select>
        </div>
        <div class="mb-4">
            <label class="form-label">Minimum Votes</label>
            <input type="number"
                   class="form-control"
                   placeholder="Minimum Votes"
                   wire:model="filters.min_votes"
            />
        </div>
        <div class="mb-4">
            <label class="form-label">Maximum Votes</label>
            <input type="number"
                   class="form-control"
                   placeholder="Maximum Votes"
                   wire:model="filters.max_votes"
            />
        </div>
        <div class="d-grid gap-2">
            <button class="btn btn-primary" wire:click="applyFilters">
                Apply Filters
            </button>
            <button class="btn btn-outline-secondary" wire:click="resetFilters">
                Reset Filters
            </button>
        </div>
    </div>
</div>

And voila! Our data table is complete with pagination, searching, and filter features!

Contact list interface with a filter panel on the right for specifying age range, occupation, and minimum votes.

Test that the application works as expected

Now that you've built the application, let's briefly walk through using it. Start the application if you stopped it, by running the following command:

php artisan serve

Then open http://127.0.0.1:8000 to access the Laravel app.

  • DataTable: The contacts page displays the interactive data table populated with database records

  • Filter and Search: The search and filter options are used to refine your results by making queries with dynamic filtering

That's how to use Laravel's Query Builder

In this tutorial, we learned how to use Laravel's Query Builder, the basics of query building, and some of the important query clauses. We also built a data table in Laravel to better explain how to build queries and create dynamic and interactive components for web applications.

With the implementation of search, filtering, and pagination, we now have a robust foundation to manage and display data effectively.

If you'd like to further improve the data table, here are some simple additions you can make:

  • Enable sorting: Implement column-based sorting to let users organize data by clicking on headers.

  • Add advanced filters: Introduce range sliders or multi-select dropdowns for more granular filtering.

  • Add custom styling: Enhance the user interface with additional CSS frameworks or custom themes for better aesthetics.

These enhancements can make your data table even more functional and user-friendly.

All the code for the data table can be found in the GitHub repository.

Kenneth Ekandem is a software engineer from Nigeria currently in the blockchain space, but interested in learning everything computer science has to offer. He'd love to go to space one day and own his own vlogging channel to teach the next generation of programmers. You can reach out to him on Twitter, LinkedIn, and GitHub.

Sql icons created by Smashicons on Flaticon.