strlen não conta letra: limite de caracteres que bate na tela, no PHP e no banco
Entenda por que strlen, length do JavaScript e VARCHAR contam coisas diferentes, e alinhe validação, contador e coluna para o limite de 160 caracteres valer igual em todo lugar.
O campo de bio do perfil diz "máximo de 160 caracteres". A pessoa escreve uma frase com "ação", "não" e um emoji de polegar, o contador na tela mostra 150 e o servidor recusa dizendo que passou do limite. A resposta curta: strlen conta bytes, não letras. Em UTF-8, "ã" e "ç" ocupam 2 bytes, e um emoji ocupa 4. Para limite de texto humano, use mb_strlen($texto, 'UTF-8') no PHP, conte da mesma forma no JavaScript e garanta que a coluna do banco também mede caracteres.
O problema raramente é uma função só. São três contagens diferentes, uma em cada camada, que concordam para texto em inglês e brigam no primeiro acento. Este guia alinha as três usando o mesmo campo de bio.
Por que cada camada conta uma coisa
Texto em UTF-8 é uma sequência de bytes. Letras sem acento ocupam 1 byte, letras acentuadas do português ocupam 2, e emojis ocupam 4. A palavra "ação" tem 4 letras e 6 bytes. A frase "não não não" tem 11 caracteres e 14 bytes. Um polegar 👍 é 1 caractere e 4 bytes.
Cada ferramenta escolhe uma unidade:
- PHP
strlenconta bytes. Para "ação", devolve 6. - PHP
mb_strlencom UTF-8 conta pontos de código, que é o que quase todo mundo chama de caractere. Para "ação", devolve 4. - JavaScript
.lengthconta unidades de UTF-16. Letras acentuadas valem 1, mas o emoji vale 2. O atributomaxlengthdo HTML usa essa mesma contagem. - MySQL
VARCHAR(160)comutf8mb4limita em caracteres. A funçãoLENGTH()devolve bytes eCHAR_LENGTH()devolve caracteres.
Há ainda um degrau acima: o que o olho vê como uma letra pode ser mais de um ponto de código. O polegar com tom de pele 👍🏽 são dois pontos de código (o polegar e o modificador), 8 bytes e 4 unidades de UTF-16. E "é" pode chegar como um caractere só ou como "e" seguido de um acento combinante, dependendo de onde a pessoa copiou. Visualmente idênticos, com tamanhos diferentes.
Como alinhar o limite nas três camadas
- Decida a unidade do limite. Para campo de texto humano, a escolha prática é ponto de código: é o que
mb_strlene oVARCHARdo MySQL medem. - Normalize o texto para a forma composta (NFC) antes de contar e de gravar. Assim "é" decomposto vira um caractere só.
- Valide no servidor com
mb_strlen. Essa é a regra que vale. - Mostre o contador na tela com a mesma unidade, contando pontos de código em vez de
.length. - Confira que a coluna é
utf8mb4, para emoji caber, e que o tamanho em caracteres comporta o limite. - Teste com acento, emoji e texto colado de outro aplicativo.
No servidor, em PHP
<?php
const LIMITE_BIO = 160;
$bio = trim($_POST['bio'] ?? '');
if (class_exists('Normalizer')) {
$normalizada = Normalizer::normalize($bio, Normalizer::FORM_C);
if ($normalizada !== false) {
$bio = $normalizada;
}
}
if (!mb_check_encoding($bio, 'UTF-8')) {
$erros['bio'] = 'O texto tem caracteres inválidos.';
} elseif (mb_strlen($bio, 'UTF-8') > LIMITE_BIO) {
$erros['bio'] = 'A bio pode ter até ' . LIMITE_BIO . ' caracteres.';
}
A classe Normalizer vem da extensão intl; o class_exists evita erro fatal num servidor sem ela, mas o ideal é instalar a extensão. A checagem com mb_check_encoding barra bytes que não formam UTF-8 válido, que de outra forma estourariam no banco ou no json_encode.
Na tela, em JavaScript
const campo = document.getElementById('bio');
const contador = document.getElementById('bio-contador');
const LIMITE = 160;
function contar(texto) {
return Array.from(texto.normalize('NFC')).length;
}
campo.addEventListener('input', function () {
const restam = LIMITE - contar(campo.value);
contador.textContent = restam >= 0
? 'Restam ' + restam + ' caracteres'
: 'Passou ' + (-restam) + ' caracteres do limite';
});
Array.from percorre a string por ponto de código, então o polegar vale 1, igual ao mb_strlen. Repare que não há maxlength="160" no campo: como ele conta unidades de UTF-16, bloquearia a pessoa antes do limite real quando ela usa emoji. Mostre o contador e deixe o servidor decidir.
No banco
ALTER TABLE perfis
MODIFY bio VARCHAR(160) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SELECT id, CHAR_LENGTH(bio) AS caracteres, LENGTH(bio) AS bytes
FROM perfis
ORDER BY bytes DESC
LIMIT 10;
A segunda consulta mostra, lado a lado, as duas medidas das bios mais pesadas. Se a coluna ainda está no antigo utf8 do MySQL, que só aceita até 3 bytes por caractere, o emoji falha na gravação. Antes de alterar uma tabela grande em produção, faça backup e teste a conversão; o guia de encoding de ponta a ponta cobre as outras camadas onde o acento pode quebrar.
Quando contar byte é o certo
Byte não é a unidade errada, é a resposta de outra pergunta. Tamanho de arquivo, limite de payload de API, campo de protocolo com tamanho fixo e índice que tem teto em bytes pedem strlen. O erro é usar a mesma função para "quantas letras a pessoa escreveu". E quando precisar encurtar o texto, para um resumo ou uma prévia, corte por caractere com mb_substr; o detalhe de não partir a letra no meio está em cortar texto no limite sem partir o acento.
Erros comuns
- Validar com
strlenum campo de texto. Quem escreve em português ganha menos espaço que quem escreve sem acento, e a mensagem de erro parece mentira. - Cortar com
substr. O corte cai no meio de um caractere de 2 bytes e o texto termina num símbolo quebrado, que ojson_encoderecusa. - Usar
maxlengthcomo limite principal. Ele conta emoji como 2 e trava a pessoa antes do limite que o servidor aceitaria. - Contador da tela com
.lengthe servidor commb_strlen. A tela diz que cabe, o servidor diz que não, ou o contrário. As duas contagens precisam ser a mesma. - Coluna em
utf8antigo. O texto com emoji passa na validação e falha noINSERT, ou é cortado, dependendo do modo do MySQL. - Não normalizar. Duas bios visualmente iguais têm tamanhos diferentes, e o limite parece aleatório para quem copia texto de outro app.
Checklist
- A validação de texto no servidor usa
mb_strlencom UTF-8, nãostrlen. - O texto é normalizado para NFC antes de contar e gravar.
- O contador da tela conta pontos de código, não
.length. - Nenhum
maxlengthmenor que o limite real trava quem usa emoji. - A coluna usa
utf8mb4e comporta o limite em caracteres. - Cortes de texto usam
mb_substr. - Você testou com "não não não", com 👍 e com 👍🏽.
Digite "não não não" e alguns emojis no campo com limite do seu sistema até a tela dizer que chegou no máximo, e envie. O servidor aceitou exatamente o que a tela deixou passar? Se não, qual das três camadas está contando byte?
Referência: http://php.net/manual/pt_BR/function.strlen.php