- 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
Laravel Events and Listeners: Decoupling Without the Chaos
About Post
The controller method started innocently. Sign the contract, save it, return a response. Then someone needed an email to the tenant. Then a notification to the property manager. Then a PDF generated and stored. Then the first invoice. Then an audit log entry.
Now the "sign" method is sixty lines long, takes several seconds because it's sending emails inline, and if the PDF service hiccups, the tenant sees an error even though the contract was signed.
Events and listeners are Laravel's answer to this. Used well, they turn that pile into one line and a set of small, independent reactions. Used badly, they turn your app into a haunted house where things happen and nobody knows why. Let's do the first one.
The idea: announce, don't orchestrate
An event is a fact about something that already happened: ContractSigned, PaymentReceived, MaintenanceRequestClosed. A listener is one reaction to that fact.
The code that signs the contract doesn't need to know who cares. It announces the fact and moves on. Billing, email and auditing each subscribe on their own. Add a sixth reaction next month without touching the signing code at all.
Step 1: the event
namespace App\Events;
use App\Models\Contract;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ContractSigned implements ShouldDispatchAfterCommit
{
use Dispatchable, SerializesModels;
public function __construct(public Contract $contract) {}
}
Name events in the past tense. They describe what happened, not what should happen next. SendWelcomeEmail is a command pretending to be an event.
That ShouldDispatchAfterCommit interface is the most underrated line here. If you dispatch inside a database transaction and the transaction rolls back, the event is simply dropped. Without it, your listeners could email a tenant about a contract that, as far as the database is concerned, was never signed.
Step 2: the listeners
namespace App\Listeners;
use App\Events\ContractSigned;
use App\Mail\WelcomePack;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
class SendWelcomePack implements ShouldQueue
{
public function handle(ContractSigned $event): void
{
Mail::to($event->contract->tenant)->send(new WelcomePack($event->contract));
}
}
Since Laravel 11, listeners in app/Listeners are discovered automatically from the type-hint on handle(). No manual registration. Run php artisan event:list to see what's wired to what.
ShouldQueue is where the real win is: the listener runs on a queue worker, not during the HTTP request. The user gets their response immediately, and if the mail server is slow or down, the job retries in the background instead of breaking the signing.
Step 3: dispatch
DB::transaction(function () use ($contract) {
$contract->markAsSigned();
ContractSigned::dispatch($contract);
});
The sixty-line method is now a few lines. Each reaction lives in its own small class that you can read, test and change on its own.
Events vs observers vs just calling the code
Laravel gives you three ways to "do something when something happens", and picking the wrong one is where the chaos starts.
| Use | When |
|---|---|
| A direct call | The step is part of the action and must succeed with it. Saving the signature itself is not a listener. |
| A domain event | Something meaningful happened in the business, and other parts of the app react to it: emails, invoices, audit, notifications. |
| A model observer | Technical reactions to any save of a model, wherever it happens: generating a UUID, clearing a cache, keeping a search index in sync. |
The trap with observers is that updated fires for every update, from every place, for every reason. "Email the tenant when the contract status changes" in an observer means a one-off script that loops over contracts to correct a data issue might email every tenant it touches. A domain event is dispatched on purpose, by the code that knows what actually happened.
The gotchas that bite in production
- Mass updates skip model events.
Contract::where('expires_at', '<', now())->update([...])runs one SQL query. No models are loaded, so no observers fire. If your logic depends on them, loop over the models, or dispatch your own event after the update. - Listeners must be safe to run twice. Queued jobs can be retried. A listener that creates an invoice should check whether the invoice already exists, or rely on a unique constraint.
- Order is not a contract. If listener B needs listener A's result, they aren't independent reactions. Make it one listener, or have A dispatch its own event.
- Don't build event chains you can't follow. An event that triggers a listener that dispatches an event that triggers... is how "decoupled" becomes "impossible to debug". One or two hops is fine. Five is a mystery novel.
Testing with Event::fake
Test the two halves separately. First, that the action announces the event:
public function test_signing_dispatches_contract_signed(): void
{
Event::fake([ContractSigned::class]);
$contract = Contract::factory()->create();
$this->actingAs(User::factory()->create())
->postJson("/api/contracts/{$contract->id}/sign")
->assertOk();
Event::assertDispatched(ContractSigned::class, fn ($e) => $e->contract->is($contract));
}
Then test each listener directly: call handle() with an event you construct yourself, with Mail::fake() in place. You can also assert the wiring with Event::assertListening(ContractSigned::class, SendWelcomePack::class).
Notice the array passed to Event::fake(). Calling it with no arguments fakes every event, including Eloquent's own model events. If an observer sets a UUID on creating, your factories will suddenly produce models without one, and you'll lose half an hour wondering why. Fake only the events you're asserting on.
The rule of thumb: if the step must succeed for the action to count, call it directly. If it's a reaction to the action, make it a queued listener on a past-tense event, dispatched after commit.
Wrap-up
- Events are facts, in the past tense. Listeners are independent reactions.
- Queue listeners that talk to the outside world.
- Use
ShouldDispatchAfterCommitwhen dispatching inside transactions. - Domain events for business moments, observers for technical housekeeping.
- Fake specific events in tests, never all of them by accident.
The Laravel events docs cover the rest, including event subscribers. Where do you draw the line between an observer and an event in your projects?

Be first to comment it...