Querying
Basic Queries
// Get all records
$users = User::all();
// Get with conditions
$users = User::where('is_active', true)->get();
// Get single record
$user = User::find($id);
$user = User::findOrFail($id);
$user = User::where('email', $email)->first();
$user = User::where('email', $email)->firstOrFail();
// Get specific columns
$users = User::select(['id', 'name', 'email'])->get();
// Count
$count = User::where('is_active', true)->count();
// Check existence
if (User::where('email', $email)->exists()) {
// ...
}
Use Query Builder Method
Always start queries with query() for clarity and IDE support:
// Good
$tickets = Ticket::query()
->with(['user', 'category'])
->where('status', Status::Open)
->latest()
->paginate(15);
// Acceptable for simple queries
$ticket = Ticket::find($id);
$tickets = Ticket::all();
Avoid Raw Queries
// Good
User::where('votes', '>', 100)->get();
// Bad - Raw query when not needed
User::whereRaw('votes > 100')->get();
// Acceptable - Complex queries that require raw
User::whereRaw('LOWER(email) = ?', [strtolower($email)])->first();
Query Scopes
Using the Scope Attribute (Laravel 11+)
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
#[Scope]
protected function active(Builder $query): void
{
$query->where('is_active', true);
}
#[Scope]
protected function forUser(Builder $query, User $user): void
{
$query->where('user_id', $user->id);
}
// Usage
Ticket::active()->forUser($user)->get();
Traditional Scope Syntax
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function scopeStatus(Builder $query, Status $status): Builder
{
return $query->where('status', $status);
}
When to Use Scopes
- Reusable query conditions
- Complex multi-condition queries
- Queries used across multiple locations
// Good - Reusable scope
Ticket::active()->recent()->get();
// Bad - Duplicating conditions
Ticket::where('is_active', true)->where('created_at', '>', now()->subWeek())->get();
// ... same conditions elsewhere in codebase
Custom Query Builders
For models with 3+ scopes, use a custom Eloquent Builder:
<?php
declare(strict_types=1);
namespace App\Builders;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
/**
* @extends Builder<User>
*/
final class UserBuilder extends Builder
{
public function active(): self
{
return $this->where('is_active', true);
}
public function verified(): self
{
return $this->whereNotNull('email_verified_at');
}
public function withRole(string $role): self
{
return $this->where('role', $role);
}
public function createdAfter(Carbon $date): self
{
return $this->where('created_at', '>=', $date);
}
public function search(string $term): self
{
return $this->where(function (Builder $query) use ($term): void {
$query->where('name', 'like', "%{$term}%")
->orWhere('email', 'like', "%{$term}%");
});
}
}
// In Model
use App\Builders\UserBuilder;
final class User extends Authenticatable
{
/**
* @param \Illuminate\Database\Query\Builder $query
*/
public function newEloquentBuilder($query): UserBuilder
{
return new UserBuilder($query);
}
}
// Usage
$users = User::query()
->active()
->verified()
->withRole('admin')
->search($term)
->paginate(15);
Reusable Scope Traits
For scopes shared across multiple models:
<?php
declare(strict_types=1);
namespace App\Models\Traits;
use Illuminate\Database\Eloquent\Builder;
trait HasActiveScope
{
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function scopeInactive(Builder $query): Builder
{
return $query->where('is_active', false);
}
}
<?php
declare(strict_types=1);
namespace App\Models\Traits;
use Illuminate\Database\Eloquent\Builder;
trait HasDateScopes
{
public function scopeCreatedBetween(Builder $query, Carbon $start, Carbon $end): Builder
{
return $query->whereBetween('created_at', [$start, $end]);
}
public function scopeCreatedToday(Builder $query): Builder
{
return $query->whereDate('created_at', today());
}
public function scopeRecent(Builder $query, int $days = 7): Builder
{
return $query->where('created_at', '>=', now()->subDays($days));
}
}
Eager Loading
Prevent N+1 Queries
// Bad - N+1 problem
$tickets = Ticket::all();
foreach ($tickets as $ticket) {
echo $ticket->user->name; // Query for each ticket
}
// Good - Eager load
$tickets = Ticket::with('user')->get();
foreach ($tickets as $ticket) {
echo $ticket->user->name; // No additional queries
}
Multiple Relationships
$tickets = Ticket::with(['user', 'category', 'comments'])->get();
Nested Relationships
$tickets = Ticket::with(['user.profile', 'comments.author'])->get();
Constrained Eager Loading
$tickets = Ticket::with([
'comments' => fn ($query) => $query->latest()->limit(5),
'user:id,name,email',
])->get();
Lazy Eager Loading
$tickets = Ticket::all();
if ($needComments) {
$tickets->load('comments');
}
Prevent Lazy Loading
Enable in AppServiceProvider to catch N+1 issues:
public function boot(): void
{
Model::preventLazyLoading(!$this->app->isProduction());
}
Relationships
Query Through Relationships
// Get related records
$comments = $ticket->comments()->latest()->get();
// Query existence
$ticketsWithComments = Ticket::has('comments')->get();
$ticketsWithComments = Ticket::has('comments', '>=', 5)->get();
// Query with conditions
$tickets = Ticket::whereHas('comments', function ($query): void {
$query->where('is_approved', true);
})->get();
// Count relationships
$tickets = Ticket::withCount('comments')->get();
// Access via $ticket->comments_count
// Sum relationship column
$users = User::withSum('orders', 'total')->get();
// Access via $user->orders_sum_total
Saving Relationships
// Create through relationship
$ticket->comments()->create([
'body' => 'Comment text',
'user_id' => auth()->id(),
]);
// Associate belongs to
$comment->ticket()->associate($ticket);
$comment->save();
// Sync many-to-many
$ticket->tags()->sync([1, 2, 3]);
$ticket->tags()->syncWithoutDetaching([4, 5]);
// Attach/detach
$ticket->tags()->attach($tagId);
$ticket->tags()->detach($tagId);
Collections
Use Collection Methods
// Filter
$activeUsers = $users->filter(fn (User $user) => $user->is_active);
// Map
$names = $users->map(fn (User $user) => $user->name);
// Pluck
$emails = $users->pluck('email');
$emailsByName = $users->pluck('email', 'name');
// Group
$byRole = $users->groupBy('role');
// First matching
$admin = $users->firstWhere('role', 'admin');
// Check contents
if ($users->contains('email', 'admin@example.com')) {
// ...
}
// Aggregate
$total = $orders->sum('amount');
$average = $orders->avg('amount');
Avoid Multiple Queries
// Bad - Multiple queries
$activeCount = User::where('is_active', true)->count();
$inactiveCount = User::where('is_active', false)->count();
// Good - Single query, process in PHP
$users = User::all();
$activeCount = $users->where('is_active', true)->count();
$inactiveCount = $users->where('is_active', false)->count();
// Or use groupBy
$counts = User::query()
->selectRaw('is_active, COUNT(*) as count')
->groupBy('is_active')
->pluck('count', 'is_active');
Large Datasets
Chunking
// Process in chunks to avoid memory issues
User::chunk(100, function ($users): void {
foreach ($users as $user) {
$this->processUser($user);
}
});
// Chunk by ID (safer for updates)
User::chunkById(100, function ($users): void {
foreach ($users as $user) {
$user->update(['processed' => true]);
}
});
Lazy Collections
// Memory efficient iteration
User::lazy()->each(function (User $user): void {
$this->processUser($user);
});
// With eager loading
User::with('profile')->lazy()->each(function (User $user): void {
// ...
});
Cursor
// Most memory efficient for read-only
foreach (User::cursor() as $user) {
$this->processUser($user);
}
Transactions
use Illuminate\Support\Facades\DB;
// Simple transaction
DB::transaction(function (): void {
$order = Order::create([...]);
$order->items()->createMany([...]);
$order->user->decrement('balance', $order->total);
});
// With return value
$order = DB::transaction(function () use ($data): Order {
$order = Order::create($data);
$order->items()->createMany($data['items']);
return $order;
});
// Manual transaction control
DB::beginTransaction();
try {
// ... operations
DB::commit();
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
Mass Operations
Efficient Updates
// Good - Single query
Ticket::where('status', Status::Pending)
->where('created_at', '<', now()->subDays(30))
->update(['status' => Status::Expired]);
// Bad - Multiple queries
$tickets = Ticket::where('status', Status::Pending)->get();
foreach ($tickets as $ticket) {
$ticket->update(['status' => Status::Expired]);
}
Efficient Inserts
// Good - Batch insert
User::insert([
['name' => 'John', 'email' => 'john@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com'],
]);
// With timestamps
User::query()->insert(
collect($users)->map(fn ($user) => [
...$user,
'created_at' => now(),
'updated_at' => now(),
])->toArray()
);
// Upsert
User::upsert(
[
['email' => 'john@example.com', 'name' => 'John Updated'],
['email' => 'jane@example.com', 'name' => 'Jane Updated'],
],
uniqueBy: ['email'],
update: ['name']
);
Factories
Basic Structure
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Ticket>
*/
final class TicketFactory extends Factory
{
protected $model = Ticket::class;
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
'status' => Status::Open,
'priority' => fake()->randomElement(['low', 'medium', 'high']),
'user_id' => User::factory(),
];
}
}
States
public function closed(): static
{
return $this->state(fn (array $attributes) => [
'status' => Status::Closed,
'closed_at' => now(),
]);
}
public function highPriority(): static
{
return $this->state(fn (array $attributes) => [
'priority' => 'high',
]);
}
public function forUser(User $user): static
{
return $this->state(fn (array $attributes) => [
'user_id' => $user->id,
]);
}
// Usage
Ticket::factory()->closed()->create();
Ticket::factory()->highPriority()->forUser($user)->create();
Relationships in Factories
// Create with relationship
$ticket = Ticket::factory()
->has(Comment::factory()->count(3))
->create();
// Create for existing model
$ticket = Ticket::factory()
->for($user)
->create();
// Many-to-many
$ticket = Ticket::factory()
->hasAttached(Tag::factory()->count(2))
->create();
// With pivot data
$ticket = Ticket::factory()
->hasAttached(
User::factory()->count(2),
['role' => 'collaborator']
)
->create();
Sequences
$users = User::factory()
->count(10)
->sequence(
['role' => 'admin'],
['role' => 'editor'],
['role' => 'viewer'],
)
->create();
Soft Deletes
Querying
// Exclude soft deleted (default)
$tickets = Ticket::all();
// Include soft deleted
$tickets = Ticket::withTrashed()->get();
// Only soft deleted
$tickets = Ticket::onlyTrashed()->get();
// Check if soft deleted
if ($ticket->trashed()) {
// ...
}
Operations
// Soft delete
$ticket->delete();
// Restore
$ticket->restore();
// Force delete (permanent)
$ticket->forceDelete();
Best Practices
Use Explicit Column Selection
// Good - Select only needed columns
$users = User::select(['id', 'name', 'email'])->get();
// Bad - Select all columns when not needed
$users = User::all();
$names = $users->pluck('name');
Avoid Queries in Loops
// Bad
foreach ($userIds as $id) {
$user = User::find($id);
$this->process($user);
}
// Good
$users = User::whereIn('id', $userIds)->get();
foreach ($users as $user) {
$this->process($user);
}
Use Database Constraints
// Good - Let database handle uniqueness
try {
User::create(['email' => $email]);
} catch (UniqueConstraintViolationException $e) {
// Handle duplicate
}
// Bad - Race condition possible
if (!User::where('email', $email)->exists()) {
User::create(['email' => $email]);
}