- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Eloquent Relationships You Might Be Underusing (and the Queries They Replace)
About Post
Open almost any Laravel codebase and you'll find hasMany and belongsTo everywhere. Then, right next to them, you'll find code like this:
$unitIds = $building->units()->pluck('id');
$contracts = Contract::whereIn('unit_id', $unitIds)->get();
foreach ($contracts as $contract) {
$contract->last_payment = $contract->payments()->latest()->first();
}
It works. It also runs one query per contract, hides a relationship that deserves a name, and will be copy-pasted into three more controllers by the end of the year.
Eloquent has had answers to these patterns for years. Here are the relationships and helpers I see underused most, each with the hand-written code it replaces. The examples use a simple domain: buildings have units, units have contracts, contracts have payments.
1. hasManyThrough: skip the middle table
You wrote: pluck the unit IDs, then query contracts with whereIn.
There's a relationship for that:
class Building extends Model
{
public function contracts(): HasManyThrough
{
return $this->hasManyThrough(Contract::class, Unit::class);
}
}
$building->contracts()->where('status', 'active')->count();
Building::withCount('contracts')->get();
It's one join instead of two round trips, and because it's a real relationship, you get eager loading, withCount and whereHas for free. Laravel also offers a fluent style, $this->through('units')->has('contracts'), which some people find easier to read.
The default keys assume units.building_id and contracts.unit_id. If yours differ, pass them as extra arguments.
2. latestOfMany and ofMany: "the one that matters" as a relationship
You wrote: $contract->payments()->latest()->first() inside a loop. One query per row, the classic N+1.
There's a relationship for that:
public function latestPayment(): HasOne
{
return $this->hasOne(Payment::class)->latestOfMany('paid_at');
}
public function largestPayment(): HasOne
{
return $this->hasOne(Payment::class)->ofMany('amount', 'max');
}
$contracts = Contract::with('latestPayment')->get(); // no N+1
This is one of my favourite Eloquent features, and one of the least known. A "has one of many" relationship turns "the latest", "the oldest" or "the biggest" into something you can eager load for a whole list in a single query. There's also oldestOfMany(), and ofMany() accepts a closure for extra conditions, such as only counting payments that cleared.
3. withSum, withMax and withExists: aggregates without loading rows
You wrote: $contract->payments->sum('amount'), which loads every payment model into memory just to add up one column.
There's a helper for that:
$contracts = Contract::query()
->withSum(['payments as paid_total' => fn ($q) => $q->where('status', 'cleared')], 'amount')
->withMax('payments', 'paid_at') // payments_max_paid_at
->withExists('maintenanceRequests') // maintenance_requests_exists
->get();
Each one adds a subquery to the SELECT, so the database does the maths and you get a plain attribute on each model. You can even sort by it: ->orderByDesc('paid_total'). There's also withMin, withAvg, and withAggregate as the general form.
Two small gotchas: a contract with no payments gets null, not 0, and sums may come back as strings depending on your database driver. Cast before doing maths.
4. whereRelation: whereHas without the closure
You wrote:
Contract::whereHas('tenant', function ($query) {
$query->where('is_verified', true);
})->get();
There's a shortcut for that:
Contract::whereRelation('tenant', 'is_verified', true)->get();
Contract::whereRelation('payments', 'amount', '>', 5000)->get();
Contract::whereRelation('unit.building', 'city', 'Doha')->get();
It's the same query underneath, but for a single condition it reads like a sentence. It supports operators and nested relations with dot notation, and there's orWhereRelation too. Its cousin whereBelongsTo($tenant) replaces where('tenant_id', $tenant->id) and works out the foreign key for you.
The mindset shift: a relationship is just a query with a name. Whenever you write the same join or lookup twice, ask whether it should be a relationship method instead. Named queries can be eager loaded, counted, filtered and tested; scattered ones can't.
5. Polymorphic relations: one table, many owners
You wrote: a contract_documents table, then a tenant_documents table, then a maintenance_documents table, all with the same columns.
There's a relationship for that:
// Migration: adds documentable_type and documentable_id
$table->morphs('documentable');
// On Contract, Tenant, MaintenanceRequest...
public function documents(): MorphMany
{
return $this->morphMany(Document::class, 'documentable');
}
// On Document
public function documentable(): MorphTo
{
return $this->morphTo();
}
One documents table, one upload component, one set of policies. Comments, attachments, activity logs and notes are all classic fits.
The gotcha: by default, Eloquent stores the full class name, like App\Models\Contract, in the _type column. Rename or move the class and existing rows point to nothing. Fix it on day one with a morph map in a service provider's boot():
Relation::enforceMorphMap([
'contract' => Contract::class,
'tenant' => Tenant::class,
'maintenance_request' => MaintenanceRequest::class,
]);
The honest trade-off: the database can't enforce a foreign key on a polymorphic column. If referential integrity is critical for a relationship, separate tables with real foreign keys may still be the better choice.
Bonus: chaperone() for the reverse N+1
When you loop over $contract->payments and each payment accesses $payment->contract, Eloquent may query the contract again for every payment, even though you already have it. Adding ->chaperone() to the hasMany definition tells Laravel to set the parent on each child automatically.
Cheat sheet
| If you're writing... | Try |
|---|---|
pluck('id') then whereIn | hasManyThrough |
->latest()->first() in a loop | latestOfMany / ofMany |
->relation->sum('col') | withSum, withMax, withExists |
whereHas with one condition | whereRelation |
| Identical tables for different owners | morphMany with a morph map |
The Eloquent relationships docs are long, but worth one slow read. Most developers skim them once early on and never return, which is exactly how these gems get missed.
Which Eloquent feature did you discover embarrassingly late, and what code did it let you delete?

Be first to comment it...