- 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
Six Mistakes With Laravel Eloquent Events and Observers (and the Fixes)
About Post
Observers feel like magic the first time you use one. A contract is created, and somewhere, without a single extra line in the controller, a welcome email goes out, an audit log is written and a cache is cleared.
Six months later, that magic is why a data import sent hundreds of emails, why a bulk update skipped the audit log entirely, and why nobody on the team can say exactly what happens when you call save().
Eloquent model events (creating, updated, deleted and friends) and the observers that group them are genuinely useful. They're also one of the easiest ways to hide behaviour in a Laravel app. Here are the mistakes I see most, why they happen, and what to do instead.
Mistake 1: hiding business side effects in observers
The mistake. Important actions live in an observer: send the tenant a welcome email in created, notify finance in updated.
Why it hurts. Every Contract::create() anywhere now sends an email: in seeders, in tests, in a one-off import script, in an admin "duplicate contract" feature someone adds next year. The controller reads like it only saves a record, and the most important thing it does is invisible.
The fix. Keep observers for things that should always happen with the data, whoever touches it: setting a UUID, normalising a field, clearing a cache, writing an audit row. Put business actions where the business action happens, in an action class or service that's called explicitly:
class SignContract
{
public function handle(Contract $contract, User $signedBy): void
{
$contract->markSigned($signedBy);
$contract->tenant->notify(new ContractSigned($contract));
ContractSignedEvent::dispatch($contract);
}
}
Now "signing a contract sends a notification" is written down in the place someone will look for it.
Mistake 2: expecting events on mass updates
The mistake.
Contract::where('ends_at', '<', now())->update(['status' => 'expired']);
Why it hurts. This runs a single SQL UPDATE. No models are loaded, so no model events fire: no updating, no updated, no saved. Your audit observer never hears about it. The same goes for query-builder delete(), insert(), upsert() and increment() on a query.
The fix. If the events matter, load the models and save them one by one, in chunks:
Contract::where('ends_at', '<', now())
->where('status', '!=', 'expired')
->lazyById()
->each(fn (Contract $contract) => $contract->update(['status' => 'expired']));
Slower, but correct. And if speed matters more than the events, keep the mass update and do the side effect explicitly afterwards, with a comment saying why. The bug is never the choice itself; it's not knowing you made one.
Mistake 3: the observer that saves the model it's observing
The mistake.
public function saved(Contract $contract): void
{
$contract->search_text = Str::lower("{$contract->reference} {$contract->tenant_name}");
$contract->save(); // fires saved again...
}
Why it hurts. save() fires saved, which calls save(), which fires saved. Depending on whether anything is dirty the second time round, you get an extra query on every save or an infinite loop. Either way, it's a trap.
The fix. Set derived values in saving (or creating/updating), before the write happens, so no second save is needed. If you genuinely must save inside an event, use saveQuietly(), which skips events.
public function saving(Contract $contract): void
{
$contract->search_text = Str::lower("{$contract->reference} {$contract->tenant_name}");
}
Mistake 4: side effects that escape a rolled-back transaction
The mistake. A contract and its payment schedule are created in a database transaction. The observer on Contract dispatches a job or sends an email in created. Then creating the schedule fails and the transaction rolls back.
Why it hurts. The contract no longer exists. The email about it has already been sent, or a queued job is about to look for a record that isn't there.
The fix. Tell the observer to wait for the commit:
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
class ContractObserver implements ShouldHandleEventsAfterCommit
{
// handlers now run only after the surrounding transaction commits
}
Queued jobs and listeners have the same option with afterCommit. In anything involving money or contracts, I'd treat "after commit" as the default.
Mistake 5: reacting to changes that didn't happen
The mistake. A saved handler that sends "your contract was updated" whenever it runs.
Why it hurts. saved fires even when nothing changed, for example when a form is submitted with the same values. updated only fires when there were dirty attributes, but it doesn't care which ones, so a timestamp touch counts too.
The fix. Check exactly what changed:
public function updated(Contract $contract): void
{
if ($contract->wasChanged('status')) {
AuditLog::record($contract, 'status', $contract->getOriginal('status'), $contract->status);
}
}
Inside updated, getOriginal() still returns the old value, which is exactly what an audit log needs.
Mistake 6: tests that silently switch observers off
The mistake. A test calls Event::fake() to assert that an event was dispatched. Suddenly an unrelated assertion fails because the contract has no UUID.
Why it hurts. Event::fake() fakes all events, and Eloquent model events go through the same dispatcher. Your observers stop running. Factories that relied on a creating hook now produce incomplete models.
The fix. Fake only what you're asserting on:
Event::fake([ContractSignedEvent::class]);
Or create the models you need before calling Event::fake(). And do write at least one test that exercises the observer for real, so you know the hidden behaviour actually happens.
A simple test for any observer: if a new developer called save() on this model in a seeder, would they be surprised by what happens? If yes, that logic belongs somewhere explicit.
Quick reference
- Observers for data rules; explicit actions for business side effects.
- Mass updates and deletes fire no model events. Decide consciously.
- Derive values in
saving; neversave()insidesaved. ShouldHandleEventsAfterCommitfor anything that leaves the database.wasChanged()before reacting to an update.Event::fake([...])with a list, not without one.
Register observers where people will see them, too: the #[ObservedBy] attribute on the model class makes the hidden behaviour a little less hidden. The Eloquent events docs have the full list of events.
Which of these has caught you out? I'd bet on the mass update, but I want to hear about the stranger ones.

Be first to comment it...