Laravel Queries: Learn by Building a Data Table
Time to read:
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
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:
The results will be an array of objects, as in the example output below.
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.
Below are sample outputs when $value
does and does not have a value.
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.
You can also chain multiple where()
calls for more complex queries:
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:
The result below (formatted for readability), shows the number of users in a given age group.
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.
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.:
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:
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:
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:
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:
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
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:
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:
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:
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:
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.




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" .
Next, install the Livewire package using the below command:
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:
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:
Next, for the "votes" table, run the following command:
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:
Now, run the migrations to provision the database:
Set up the model
Create the model for the "contacts" table, by running the following command:
Once done, update the content of app/Models/Contacts.php with this:
Next, create a model for the "votes" table with the following command:
Then, update the content of app/Models/Votes.php with the following code:
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:
Update the content of the generated database/factories/ContactsFactory.php file with the following code:
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:
Now, update database/factories/VotesFactory.php with the following code:
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.
Then, update the database/seeders/ContactsSeeder.php file with the following code:
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:
This calls the ContactsSeeder class which gives us some data to work with.
Seed the database by running this command:
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:
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:
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:
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.
Start the application
To start up our development server, let’s open up the terminal and run:
We can then open up the browser at http://localhost:8000 to see our UI.
Implement search
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:
Then, add the following function to the bottom of the class.
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:
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.


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:
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:
And voila! Our data table is complete with pagination, searching, and filter features!


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:
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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.