Laravel Reference
Defining a model
Eloquent models sit on top of the same query builder covered elsewhere — every method shown below (where(), orderBy(), and so on) is available on a model just as it is on DB::table().
php artisan make:model Post -m // -m also creates a migration
See the database field types guide for the column types and modifiers available inside that migration.
class Post extends Model
{
protected $fillable = ['title', 'body', 'user_id'];
protected $hidden = ['internal_notes'];
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
'metadata' => 'array',
];
}
$fillable whitelists which attributes can be mass-assigned via create()/fill() — the counterpart $guarded blacklists instead. Setting protected $guarded = [] allows every attribute (rely on this only when input is already validated elsewhere, since it removes the whitelist entirely). $hidden excludes attributes when the model is converted to an array or JSON. $casts converts attributes to native types (or Carbon instances for dates) automatically — pasting a cast attribute's raw JSON into the JSON viewer is a quick way to check its structure before writing a cast. A native PHP enum also works directly as a cast ('status' => PostStatus::class), and for anything more involved than a type conversion, a custom cast class implementing CastsAttributes can encapsulate arbitrary get/set logic (e.g. an encrypted cast, built in for exactly that).
Basic CRUD
Post::create(['title' => 'Hello', 'body' => '...']);
Post::find(1);
Post::findOrFail(1);
Post::where('published', true)->first();
Post::all();
$post = Post::find(1);
$post->title = 'Updated title';
$post->save();
Post::where('id', 1)->update(['title' => 'Updated title']);
$post->delete();
Post::destroy([1, 2, 3]);
all() and other multi-row queries return a Collection, not a plain array, so the result is immediately chainable with map, filter, pluck, and the rest.
// Find by attributes, or create with the merged attributes if none exists
Post::firstOrCreate(['slug' => 'hello-world'], ['title' => 'Hello World']);
// Same lookup, but update the matched row (or create it) either way
Post::updateOrCreate(['slug' => 'hello-world'], ['title' => 'Hello World']);
// Like firstOrCreate but doesn't persist — useful when you want to set more before saving
$post = Post::firstOrNew(['slug' => 'hello-world']);
firstOrCreate only writes to the database when nothing matches; updateOrCreate always writes, updating the existing row's other attributes if one was found. Both are a single guarded operation, avoiding the race condition of a manual find() then create().
Relationships
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
| Relationship | Meaning |
|---|---|
hasOne | This model owns exactly one related row (e.g. User → Profile). |
hasMany | This model owns many related rows (e.g. User → Posts). |
belongsTo | The inverse — this model holds the foreign key (e.g. Post → User). |
belongsToMany | Many-to-many via a pivot table (e.g. User ↔ Role). |
hasManyThrough | Access a distant relation through an intermediate model. |
morphMany / morphTo | Polymorphic relationship — one relation type serving several model types. |
Pivot tables for belongsToMany can carry their own columns beyond the two foreign keys: withPivot('assigned_at') makes that column available on $user->roles->first()->pivot->assigned_at, and withTimestamps() maintains created_at/updated_at on the pivot row itself. wherePivot('assigned_at', '>', now()->subMonth()) filters the relation query by a pivot column. A $touches = ['post'] property on a child model (e.g. Comment) updates the parent's updated_at whenever the child is saved — handy for cache-busting a parent based on its children changing.
Eager loading (avoiding N+1)
Accessing $post->user in a loop over many posts fires one extra query per post. Eager load the relationship up front instead:
// N+1: fires a query per post inside the loop
foreach (Post::all() as $post) {
echo $post->user->name;
}
// One extra query total, not one per post
$posts = Post::with('user')->get();
$posts = Post::with(['user', 'comments'])->get();
// Load a relation on an already-fetched collection
$posts->load('comments');
N+1 problems are easy to introduce accidentally — a relation accessed inside a Blade @foreach, or inside an API resource's toArray(), looks identical to any other property access, so nothing warns you at the call site. Model::preventLazyLoading(), usually called in AppServiceProvider::boot() for the local environment, throws an exception the moment code lazy-loads a relationship instead of eager-loading it, which surfaces these during development rather than as a slow endpoint in production. For counting related rows without loading them at all, withCount('comments') adds a comments_count attribute using a single aggregate subquery.
Query scopes
class Post extends Model
{
public function scopePublished(Builder $query): void
{
$query->where('published', true);
}
}
Post::published()->latest()->get();
That's a local scope — opt-in, called explicitly. A global scope applies to every query against the model automatically, the same way soft deletes silently exclude trashed rows:
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('published', true);
}
}
class Post extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new PublishedScope);
}
}
Post::all(); // published rows only, no explicit filter needed
Post::withoutGlobalScope(PublishedScope::class)->get(); // opt out for this query
Because a global scope is invisible at the call site, reach for it sparingly — it's easy to forget it's filtering results when debugging "missing" rows later.
Chunking large result sets
get()/all() load every matching row into memory at once. For a table too large for that, process it in batches instead:
Post::chunk(200, function ($posts) {
foreach ($posts as $post) {
// ...
}
});
// Safer when the callback updates/deletes rows in the chunk being iterated —
// chunk() re-runs the same offset query each time and can skip/repeat rows otherwise
Post::chunkById(200, function ($posts) {
foreach ($posts as $post) {
$post->update(['processed' => true]);
}
});
// A generator over the whole table with one row in memory at a time —
// usable directly with foreach, no callback needed
foreach (Post::lazy() as $post) {
// ...
}
Accessors and mutators
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
set: fn ($value) => ['first_name' => explode(' ', $value)[0]],
);
}
}
$user->full_name; // uses the get callback
Soft deletes
Adding the SoftDeletes trait means delete() sets a deleted_at timestamp instead of removing the row, and every query automatically excludes soft-deleted records without you having to remember to filter them out. The model needs a nullable deleted_at column, typically added with $table->softDeletes() in a migration — see the database field types guide for that column type.
class Post extends Model
{
use SoftDeletes;
}
$post->delete(); // sets deleted_at, row stays in the table
Post::find(1); // null — soft-deleted rows are excluded by default
Post::withTrashed()->find(1); // include soft-deleted rows
Post::onlyTrashed()->get(); // only soft-deleted rows
$post->restore(); // clear deleted_at
$post->forceDelete(); // actually remove the row
Soft deletes are worth using whenever "deleted" data still has audit, undo, or reporting value — but every relationship touching that model needs to account for it, since a soft-deleted parent's children don't get soft-deleted automatically, and unique constraints in the database won't know to ignore trashed rows either.
Factories and seeders
Factories describe how to generate a fake but realistic instance of a model, which is invaluable for tests and for populating a local database with representative data instead of hand-writing rows.
php artisan make:factory PostFactory
php artisan make:seeder PostSeeder
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
'user_id' => User::factory(),
];
}
}
Post::factory()->count(20)->create();
Post::factory()->create(['title' => 'Fixed title']);
// A named state for a common variation
Post::factory()->count(5)->published()->create();
Seeders drive factories (or insert fixed reference data) and are run with php artisan db:seed, or automatically as part of php artisan migrate:fresh --seed when rebuilding a local database from scratch. See the Artisan commands guide for more on make: generators and running commands.