- 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
PHP Enums in Laravel: Replacing Magic Strings for Good
About Post
Somewhere in your codebase there's a line like if ($contract->status === 'actve'). It has been there for months. It has never been true, nobody noticed, and no test, linter or type checker had any reason to complain. It's just a string.
That's the problem with magic strings. 'active', 'pending', 'terminated' are spread across controllers, Blade views, queries, validation rules and JavaScript, each one a typo away from a silent bug. PHP has had a proper fix since 8.1: enums. And Laravel supports them almost everywhere you'd want.
What magic strings cost you
Before the fix, the symptoms. If any of these sound familiar, enums will help:
- Searching the project for
'pending'returns 40 results, and you're not sure which ones are contract statuses and which are payment statuses. - The list of allowed values lives in three places: a validation rule, a dropdown and a comment in the migration. They disagree.
- The "nice" label for each status (Waiting for signature instead of
pending_signature) is built with anifchain in a Blade file. - Adding a new status means hunting through the code and hoping you found every
switch.
Step 1: a backed enum
A backed enum has a scalar value for each case, which is what you store in the database. Here's a contract status, with a label method attached:
enum ContractStatus: string
{
case Draft = 'draft';
case PendingSignature = 'pending_signature';
case Active = 'active';
case Terminated = 'terminated';
public function label(): string
{
return match ($this) {
self::Draft => 'Draft',
self::PendingSignature => 'Waiting for signature',
self::Active => 'Active',
self::Terminated => 'Terminated',
};
}
}
Notice the match has no default. That's deliberate. If someone adds a new case and forgets the label, PHP throws an UnhandledMatchError the first time it's used, instead of quietly showing an empty string. Static analysers like PHPStan can flag it before that.
Step 2: cast it on the model
Tell Eloquent that the column is an enum, and you never touch the raw string again:
class Contract extends Model
{
protected function casts(): array
{
return [
'status' => ContractStatus::class,
];
}
}
Now $contract->status is a ContractStatus object, not a string:
if ($contract->status === ContractStatus::Active) {
// a typo here is a fatal error, not a silent false
}
echo $contract->status->label(); // "Active"
$contract->update(['status' => ContractStatus::Terminated]);
Contract::where('status', ContractStatus::PendingSignature)->get();
Laravel converts the enum to its value when saving and querying, and back to an enum when reading. Your IDE autocompletes the cases, and "find usages" on ContractStatus::PendingSignature finds exactly the right places.
Step 3: validate against the enum
The allowed values now come from one place. Validation should use it too:
use Illuminate\Validation\Rule;
$request->validate([
'status' => ['required', Rule::enum(ContractStatus::class)],
]);
$status = $request->enum('status', ContractStatus::class); // ContractStatus or null
Add a case to the enum and the validation accepts it automatically. Remove one and the validation rejects it. No second list to forget.
Step 4: put behaviour where it belongs
This is the part people miss. Enums can have methods, constants and interfaces, which makes them a natural home for small rules about the value itself:
public function isEditable(): bool
{
return $this === self::Draft;
}
public static function options(): array
{
$options = [];
foreach (self::cases() as $case) {
$options[$case->value] = $case->label();
}
return $options;
}
Now the Blade template asks $contract->status->isEditable() instead of repeating === 'draft', and every dropdown in the app is built from ContractStatus::options(). You can add a color() method for badges in the same way. Keep it to rules about the status itself, though; anything that needs a database or a service belongs elsewhere.
The gotchas
from()vstryFrom().ContractStatus::from('nope')throws aValueError.tryFrom('nope')returnsnull. UsetryFrom()for anything that came from outside your code.- Store it as a string column, not a MySQL
ENUM. A plainstringcolumn with the PHP enum as the source of truth means adding a case is a code change, not a schema change. - The values are a contract with your data. Renaming a case (
ActivetoLive) is free. Changing a value ('active'to'live') means migrating every existing row. Pick values carefully and then leave them alone. - Enum cases can't be array keys. They're objects. Use
$status->valueas the key. - APIs and JavaScript still see strings.
json_encode()outputs the backing value, so your React or React Native app receives"active". If you use TypeScript, mirror the values in a union type so the front end gets some of the same safety.
When not to use an enum: if the list of values is something an admin should be able to edit (property types, maintenance categories, document types), it's data, not code. Put it in a table. Enums are for values your code makes decisions about.
A bonus: enums in routes
Laravel can bind a route parameter straight to a backed enum. Type-hint it, and an invalid value returns a 404 before your controller even runs:
Route::get('/contracts/status/{status}', function (ContractStatus $status) {
return Contract::where('status', $status)->paginate();
});
The migration path
You don't need to convert the whole app in one go. Pick the status column that causes the most confusion, create the enum with the exact values already in the database, add the cast, and fix what breaks. The type errors you get along the way are the bugs that were already hiding there.
Which magic string in your codebase would you turn into an enum first? Mine is always the status column.

Be first to comment it...