Query Builder & Scopes

Every Eloquent model gives you access to the same fluent, chainable Query Builder that powers the rest of Laravel's database layer. Instead of writing raw SQL, you build a query up piece by piece, method by method, and Eloquent translates the whole chain into a single SQL statement only when you finally ask for results:

<?php
$articles = Article::where('published', true)
    ->orderBy('created_at', 'desc')
    ->get();

Nothing hits the database until get() is called — where() and orderBy() just record what the eventual query should look like, which is exactly what makes the builder so easy to compose conditionally (adding a where() only if a filter was supplied by the user, for example).

Common Query Builder Methods

MethodWhat it does
where('col', $val)Filters rows where the column equals a value (also accepts an operator: where('views', '>', 100))
orWhere(...)Adds an alternative condition, joined with OR instead of AND
whereIn('col', [...])Filters rows where the column matches any value in the given array
orderBy('col', 'asc'|'desc')Sorts the results by a column
limit($n)Caps the number of rows returned
first()Runs the query and returns just the first matching model, or null if none match
get()Runs the query and returns every matching row as a Collection of models

first() vs. get(): A Common Point of Confusion

These two are both query-executing methods, but they return fundamentally different shapes of data, and mixing them up is a frequent source of bugs for developers new to Eloquent:

<?php
$article = Article::where('slug', 'hello-world')->first(); // one Article model, or null

$articles = Article::where('published', true)->get(); // a Collection, possibly empty

first() always gives you either a single model instance or null — never a list, so you never loop over it. get() always gives you a Collection, even if only one row matched, or none did — so it's always safe to loop over, count, or otherwise treat as a list. Calling ->title on the result of a get() call directly is a common mistake that fails, because a Collection has no title property; only an individual model inside it does.

Local Scopes: Reusable Query Logic on the Model

Filters like where('published', true) often get reused across many different places in an application — a controller, a scheduled command, an API endpoint. Repeating the same where() call everywhere is easy to get subtly wrong in one spot and forget to update elsewhere. Local scopes solve this by letting the model itself define named, reusable pieces of query logic as ordinary methods:

<?php
// app/Models/Article.php
public function scopePublished($query)
{
    return $query->where('published', true);
}

The scope prefix is a naming convention Eloquent looks for — it's how the framework recognizes a method as a scope in the first place. When you actually call it, you drop the scope prefix and lowercase the first letter, and Eloquent automatically passes the underlying query builder in as the $query argument for you:

<?php
$articles = Article::published()->get();

Full Worked Example: Combining Two Scopes

Scopes are designed to be chained together, since each one just returns the query builder for the next method in the chain to continue building on. Here's a second scope, recent(), defined alongside published():

<?php
// app/Models/Article.php
class Article extends Model
{
    public function scopePublished($query)
    {
        return $query->where('published', true);
    }

    public function scopeRecent($query, $days = 30)
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }
}

Note that scopeRecent() also accepts its own argument, $days, with a default value — a scope can take extra parameters beyond the automatically supplied $query, and callers pass them exactly as if calling any other method. Chained together, both scopes read almost like a plain-English description of the query:

<?php
$articles = Article::published()->recent(7)->get(); // published articles from the last 7 days

Global Scopes: A Brief Mention

Local scopes must be called explicitly. A global scope instead applies to every query made against a model automatically, without needing to be called at all — it's registered once, usually in the model's booted() method, and Eloquent silently adds its constraint to every query from then on. Global scopes are a more advanced tool with fewer everyday uses; the best-known example is actually built into Laravel itself — soft deletes (excluding "trashed" rows from every query by default) is implemented internally as a global scope.

Bonus: The Collection Returned by get()

Since get() returns a Laravel Collection rather than a plain PHP array, you get a large set of convenient, chainable methods for free on top of the results:

<?php
$articles = Article::published()->get();

$titles = $articles->pluck('title'); // a Collection of just the titles
$total = $articles->count(); // how many articles matched
$upper = $articles->map(fn($article) => strtoupper($article->title));

pluck() pulls a single column out of every model into a fresh collection, count() reports how many models the collection holds, and map() runs a closure over every model and collects the return values — none of these trigger any additional database queries, since by this point the results are already sitting in PHP memory.

Next up: Scopes and the query builder work against tables that already exist — the next lesson covers how those tables get created and populated in the first place, with Migrations & Seeders.