By day 15, you've built routes, controllers, and middleware. Now it's time to master the layer that connects them to your database: Eloquent ORM basics: a PHP & Laravel guide for developers. Eloquent is Laravel's ActiveRecord ORM—it bridges your PHP objects and database tables, letting you write queries in a way that feels natural to the language. Used correctly, it eliminates boilerplate, reduces bugs, and keeps your application maintainable as it grows.
Models: One class per table with fillable fields
Every database table in a Laravel application gets a corresponding Eloquent model. This is the foundation of Eloquent ORM basics. A model is a PHP class that represents a single table and encapsulates the logic for interacting with it.
When you generate a model using php artisan make:model Product, Laravel creates a class that inherits from Model. By convention, the model name is singular and capitalized, while the table name is plural and lowercase. So the Product model maps to the products table.
The $fillable array is critical. It explicitly lists which columns can be mass-assigned—set directly from user input. Without it, all mass assignment attempts fail by default, protecting you from accidentally exposing sensitive fields.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'name',
'description',
'price',
'sku',
'active',
];
}In a real scenario, imagine you're building an e-commerce platform. A product has a name, description, price, SKU, active status, and internal cost. You only want your API endpoints to accept name, description, price, and SKU from the client—never cost or a hidden discount field. The $fillable array ensures this constraint at the model level. This is a practical safety measure that scales across your entire application.
If you need to explicitly deny certain columns instead, use $guarded. But $fillable is the safer default because it's whitelist-based.
Querying: Fluent queries return collections
Eloquent's query builder is designed to be fluent and chainable. You build queries by calling methods in sequence, and they return a collection of model instances (or a single instance, depending on the terminal method).
Here's a common pattern from a real e-commerce codebase: fetch active products, ordered by newest first, with pagination.
$products = Product::where('active', true)
->orderByDesc('created_at')
->paginate(20);Let's break this down:
Product::starts the query on the Product modelwhere('active', true)filters to active productsorderByDesc('created_at')sorts by creation date, newest firstpaginate(20)returns paginated results with 20 items per page
The result is a LengthAwarePaginator instance, which you can pass directly to your view. Each item in the collection is a Product model instance, so you can access properties like $product->name or $product->price.
You can chain many methods:
$results = Product::where('price', '>', 100)
->where('category_id', 5)
->whereIn('status', ['active', 'featured'])
->with('category') // eager load the category relationship
->orderBy('name')
->limit(50)
->get();The get() method executes the query and returns a collection. Other terminal methods include first(), find($id), count(), and exists(). Each has its purpose: use first() when you need one record, find() when you're retrieving by primary key, and count() when you only need the number of matching rows.
Mass assignment: Use $fillable to control which fields can be set
Mass assignment in Laravel means setting multiple model attributes at once, usually from request data. The most common place you'll use it is in create and update operations.
// In your controller
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'sku' => 'required|string|unique:products',
]);
$product = Product::create($validated);
return response()->json($product, 201);
}When you call Product::create($validated), Laravel attempts to set all columns in the $validated array on the model. But it will only succeed for columns listed in the model's $fillable array. Any column not in $fillable is silently ignored.
This is a security mechanism. Without it, a malicious user could craft a request that includes a field like admin: true or discount_percentage: 90, and if your model doesn't protect against it, those values would be inserted into the database.
For updates, the pattern is identical:
public function update(Request $request, Product $product)
{
$validated = $request->validate([
'name' => 'string|max:255',
'price' => 'numeric|min:0',
]);
$product->update($validated);
return response()->json($product);
}The update() method works just like create()—it respects $fillable and only sets the columns you've explicitly allowed. This is how Eloquent ORM basics protect your data layer from accidental or intentional misuse.
Casts: Cast JSON, dates, enums and decimals automatically
Database columns store raw data—strings and numbers. But your PHP code often needs richer types: DateTime objects, collections, or enums. Casts handle the conversion automatically.
The $casts array on your model tells Eloquent how to convert each column when you retrieve or store it:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'name',
'description',
'price',
'sku',
'active',
'attributes',
'published_at',
];
protected $casts = [
'price' => 'decimal:2',
'active' => 'boolean',
'attributes' => 'json',
'published_at' => 'datetime',
];
}Now when you retrieve a product:
$product = Product::find(1);
// $product->price is a Decimal instance, not a string
echo $product->price; // "19.99"
// $product->active is a boolean, even though the database stores 0 or 1
if ($product->active) {
// ...
}
// $product->attributes is automatically decoded from JSON into an array
echo $product->attributes['color']; // "blue"
// $product->published_at is a Carbon instance, not a string
echo $product->published_at->format('Y-m-d'); // "2024-01-15"This is tremendously useful in real applications. Imagine a ride-hailing platform where each booking stores a JSON object with pickup coordinates, destination, passenger preferences, and driver notes. Without casts, you'd manually decode the JSON every time. With 'metadata' => 'json', it's automatically accessible as an array.
Similarly, storing prices as decimals is crucial for financial accuracy. Using 'price' => 'decimal:2' ensures precision and prevents floating-point errors that plague money calculations.
Laravel also supports enum casts (PHP 8.1+), allowing you to cast string columns to native enums:
protected $casts = [
'status' => OrderStatus::class,
];
// Now $order->status is an OrderStatus enum instance
if ($order->status === OrderStatus::SHIPPED) {
// ...
}Common mistakes
Forgetting to add new fields to $fillable. You add a migration for a new column, but requests to create or update records with that column silently fail because the column isn't in $fillable. Always update both the migration and the model.
Using the wrong query terminal method. Calling get() when you expect a single record, then accessing ->name directly without checking if the collection is empty. Always use first() or find() for single records, and handle the null case.
Neglecting N+1 queries. This is subtle but critical. If you loop through products and access their category name, you'll run one query per product. Always use with('category') to eager load relationships. We'll cover relationships in detail in a future post, but remember: if you access a relationship inside a loop, you have an N+1 problem.
Casting dates incorrectly. If your database column is a string but you need a DateTime, the cast will fail silently or throw an error. Ensure your database schema matches your cast definition. For timestamps, Laravel's timestamps() migration helper and the default datetime cast work seamlessly.
Key takeaways
- Every table gets a model. Define
$fillableto whitelist which columns can be mass-assigned, protecting your database from accidental or malicious writes. - Build queries fluently by chaining methods. The result is readable, testable, and maintainable code that senior developers recognize immediately.
- Use
$caststo automatically convert raw database values into PHP types: decimals for money, booleans for flags, JSON for nested data, and DateTime for timestamps. - Know your terminal methods:
get()for a collection,first()orfind()for a single record,count()orexists()for checks, andpaginate()for paginated results. - Validate and sanitize input before passing it to
create()orupdate(). Use Laravel's validation to ensure data integrity at the application layer, not just the database layer.






Comments
Loading comments…