- 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
Unicode and UTF-8: Why Your Text Turns Into Question Marks
About Post
A user signs up with their name in Arabic. The form accepts it, the success message appears, everyone's happy. Then you open the admin panel and their name is ????. Not garbled. Not weird symbols. Just four polite question marks where a name used to be.
If you build for users in the Gulf, or anywhere with more than one language, you will meet this bug. Usually in production, usually with someone's name, which is the one thing people really notice.
The good news: text encoding bugs look mysterious, but there are only a handful of them, and each one leaves a recognisable fingerprint.
Characters, code points and bytes
Three ideas, and most confusion comes from mixing them up.
- Unicode is a giant catalogue. It gives every character a number called a code point.
Ais U+0041, the Arabic letter meemمis U+0645, and the "face with tears of joy" emoji is U+1F602. - An encoding decides how those numbers are stored as bytes. UTF-8 is the one that won the web.
- UTF-8 is variable-length. Plain English letters take 1 byte, Arabic letters take 2, most Chinese and Japanese characters take 3, and emoji take 4.
That last point is the root of almost everything below. Text is not "one byte per character", and any code, column or connection that assumes it is will eventually damage someone's data.
Reading the fingerprints
The shape of the damage tells you where it happened.
| You see | What happened | Recoverable? |
|---|---|---|
???? | Text was converted to a character set that has no way to represent it, like a latin1 column or connection. Each unknown character became ?. | No. The original bytes are gone. |
Ù…ØÙ…د | Correct UTF-8 bytes were read as Latin-1 or Windows-1252. Classic "mojibake", often from a connection charset mismatch. | Usually, by reinterpreting the bytes. |
� | Invalid UTF-8 bytes. Often a string cut in the middle of a character, or a file saved in another encoding. | Depends on where it was cut. |
Incorrect string value: '\xF0\x9F...' | A 4-byte character (an emoji) hit a MySQL utf8 column in strict mode. | Nothing saved yet; fix the column. |
The question marks are the scary one, because nothing will bring that data back. Fix the cause before more names disappear.
MySQL's utf8 is not UTF-8
This is the most famous trap in the stack. In MySQL, the character set called utf8 is really utf8mb3: it stores at most 3 bytes per character. Arabic fits, so everything looks fine during testing. Then the first user types an emoji, which needs 4 bytes, and the insert fails or the character is mangled.
The real UTF-8 is called utf8mb4. Laravel uses it by default in config/database.php, with utf8mb4_unicode_ci as the collation. Problems come from older tables, databases created by hand, or imports from legacy systems. Check with SHOW CREATE TABLE users; and convert what's wrong:
-- Back up first. This rewrites the whole table.
ALTER TABLE users
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
On old MySQL versions you might hit an index length error after converting, because indexed VARCHAR(255) in 4-byte characters exceeds the old 767-byte key limit. That's why some older Laravel apps have Schema::defaultStringLength(191) in a service provider.
The connection matters as much as the column
Your columns can be perfect utf8mb4 and you can still produce mojibake if the connection speaks a different charset. The client sends UTF-8 bytes, MySQL thinks they're Latin-1, "converts" them, and stores double-encoded nonsense.
Laravel sets the connection charset from the same config, so you're covered there. In plain PDO code, put it in the DSN: mysql:host=localhost;dbname=app;charset=utf8mb4. Legacy scripts that skip this are a common source of mojibake that only appears for data written by one part of the system.
PHP strings are bytes
PHP's classic string functions count and cut bytes, not characters:
strlen('محمد'); // 8 (bytes)
mb_strlen('محمد'); // 4 (characters)
substr('محمد', 0, 3); // "م" plus half a letter: shows �
mb_substr('محمد', 0, 3); // "محم"
Truncating a name for a notification, an SMS or a PDF with substr is a reliable way to produce �. Use the mb_* functions, or Laravel's Str::limit(), which is multibyte-safe.
Two more PHP gotchas:
- Regexes need the
uflag.preg_match('/^[\p{L} ]+$/u', $name)accepts letters in any script. Withoutu, the pattern works on bytes and an Arabic name fails a "letters only" check. - Know your validation rule. Laravel's
alpharule accepts Unicode letters by default. If you only want A to Z, usealpha:ascii, and think twice about whether you really do.
Emoji add one more twist. A single visible emoji can be several code points glued together (skin tones, family emoji, flags). mb_strlen counts those code points separately. If you need "what a human sees as one character", the intl extension's grapheme_strlen() is the right tool. JavaScript has its own version of the problem: .length counts UTF-16 units, so most emoji have a length of 2.
Collations: when different text counts as equal
The _ci in utf8mb4_unicode_ci means case-insensitive, and Unicode collations also treat many accented letters as equal to their plain versions. So a unique index on a name or username may reject "José" because "Jose" already exists. That's often what you want for searching and rarely what you expect for uniqueness. If a column needs exact matching, give it a binary collation like utf8mb4_bin.
The rule: UTF-8 everywhere, with no exceptions. Source files, HTML <meta charset="utf-8">, HTTP headers, database columns as utf8mb4, the database connection, and multibyte-safe string functions. Encoding bugs live in the one layer you forgot.
Bonus: the Excel CSV problem
You export a perfect UTF-8 CSV with Arabic names. A colleague opens it in Excel and sees mojibake. Excel often guesses the encoding wrong unless the file starts with a byte order mark (BOM). Writing "\xEF\xBB\xBF" at the start of the file usually fixes it. Ugly, but it saves a lot of confused emails.
The quick checklist
- Tables and columns are
utf8mb4, never MySQL'sutf8. - The connection charset is
utf8mb4too. mb_*functions for length and truncation;uflag on regexes.- Test forms with Arabic text and emoji, not just "John Smith".
- Collations chosen on purpose, especially on unique columns.
What's the worst thing you've seen an encoding bug do to real data? I suspect everyone has a story involving a customer's name.

Be first to comment it...