← Voltar aos artigos

strings · utf-8 · mbstring · acentuação · validação

strlen vs mb_strlen no PHP: por que contar caracteres com acento dá errado

Publicado em 03/08/2026 · 10 min de leitura

strlen('ação') devolve 6, não 4. A função não está errada — ela só não conta o que você imagina: strlen conta bytes, mb_strlen conta caracteres. Em UTF-8, letras acentuadas ocupam mais de um byte cada, então qualquer texto em português faz as duas contagens divergirem. E o problema vai além do número: cortar com substr pode partir uma letra ao meio e produzir o famoso . Este artigo mostra o mecanismo, quais funções têm par mb_*, como diagnosticar, e — importante — quando strlen continua sendo a escolha certa, porque trocar tudo cegamente também é erro.

O mecanismo: UTF-8 tem letras de tamanhos diferentes

Em UTF-8, o número de bytes por caractere é variável. Letras do ASCII (a–z, 0–9) ocupam 1 byte; letras acentuadas do português ocupam 2; símbolos e emojis podem ocupar 3 ou 4. Dá para ver isso com bin2hex, que mostra os bytes crus da string:

$s = 'ação';
echo strlen($s);      // 6  ← bytes
echo mb_strlen($s);   // 4  ← caracteres
echo bin2hex($s);     // 61 c3a7 c3a3 6f
//                       a    ç    ã   o
//                       1B   2B   2B  1B  = 6 bytes, 4 letras

strlen foi criada quando 1 byte = 1 caractere era verdade (era o mundo ASCII). Ela continua fazendo exatamente aquilo: medir o tamanho da string em bytes. Não é uma função "quebrada" — é uma função que responde outra pergunta. A família mb_* (de multibyte string) sabe interpretar UTF-8 e responde a pergunta que você geralmente quer: quantas letras.

O corte: onde nasce o

Contar errado é chato; cortar errado é visível para o usuário. O substr também trabalha em bytes, então ele corta na posição N de bytes — e essa posição pode cair no meio de uma letra. Sobra um pedaço de byte que não forma caractere válido, e o navegador desenha :

Corte em 1 byte

Pega o a inteiro (1 byte). Coincide com a letra, então parece tudo bem.

substr('ação', 0, 1);   // "a"     bytes: 61
mb_substr('ação', 0, 1);// "a"

Corte em 2 bytes — quebra

Pega o a e metade do ç (que precisa de 2 bytes: c3 a7). O meio byte c3 sozinho não é caractere.

substr('ação', 0, 2);   // "a�"    bytes: 61 c3   ← quebrou
mb_substr('ação', 0, 2);// "aç"

Corte em 3 bytes — coincide de novo

Aqui a conta bate por acaso: a + ç completo. É isso que torna o bug traiçoeiro — ele aparece e some conforme o texto.

substr('ação', 0, 3);   // "aç"    bytes: 61 c3a7
mb_substr('ação', 0, 3);// "açã"

Corte em 4 bytes — quebra outra vez

a + ç + metade do ã. De novo o .

substr('ação', 0, 4);   // "aç�"   bytes: 61 c3a7 c3
mb_substr('ação', 0, 4);// "ação"

A conclusão

substr conta posições de byte; mb_substr conta posições de caractere e nunca parte uma letra ao meio. Em texto com acento, só o segundo é previsível.

mb_substr($texto, 0, 100);   // sempre 100 letras, sempre íntegro

Num caso real — gerar o resumo de um texto — a diferença fica óbvia:

evite
$frase = 'Descrição do produto';

echo strlen($frase);            // 22 (bytes) — mas são 20 letras
echo substr($frase, 0, 12);     // "Descrição "  ← só 10 letras
// os acentos "comeram" 2 posições do corte
prefira
$frase = 'Descrição do produto';

echo mb_strlen($frase);         // 20 — letras de verdade
echo mb_substr($frase, 0, 12);  // "Descrição do" ← 12 letras

Não é só contar e cortar: a família inteira

O mesmo raciocínio vale para todas as funções de string "clássicas" — elas operam byte a byte. Três exemplos que costumam passar despercebidos:

$s = 'ação';

echo strtoupper($s);      // "AçãO"  ← o ç e o ã ficam de fora
echo mb_strtoupper($s);   // "AÇÃO"

echo str_pad($s, 6, '.'); // "ação"  ← acha que já tem 6 (bytes!) e não completa
echo mb_str_pad($s, 6, '.'); // "ação.." (PHP 8.3+)

echo strrev($s);          // "o?ç?a" ← inverte BYTES e destrói os acentos

O strtoupper é especialmente enganoso porque o resultado parece funcionar: metade das letras sobe, os acentos não. E o str_pad mostra que o problema não é só "contar" — a contagem errada contamina qualquer função que dependa de tamanho.

Se você usa… Troque por… Para quê
strlen mb_strlen Contar letras
substr mb_substr Cortar sem quebrar
strtoupper / strtolower mb_strtoupper / mb_strtolower Caixa com acento
strpos / stripos mb_strpos / mb_stripos Posição em letras
str_split mb_str_split Separar em letras
str_pad mb_str_pad (PHP 8.3+) Alinhar/preencher
strrev (não tem par — use mb_str_split + array_reverse + implode) Inverter

Diagnóstico: descobrir se o problema é esse

Duas checagens rápidas resolvem a dúvida "será que é isso que está acontecendo comigo?":

// 1. Se os dois números divergem, a string tem caracteres multibyte
var_dump(strlen($texto), mb_strlen($texto));

// 2. Ver os bytes crus e onde estão os multibyte
echo wordwrap(bin2hex($texto), 2, ' ', true);

Se strlen > mb_strlen, qualquer lógica sua baseada em strlen/substr está operando com números diferentes do que o usuário vê. Vale checar especialmente validações de formulário: um limite de "máximo 20 caracteres" implementado com strlen rejeita nomes acentuados antes da conta fechar.

Um campo valida if (strlen($nome) > 20) { erro(); }. O usuário digita "João Conceição Sá" (18 letras). O que acontece?

Ver resposta

Resposta certa: É rejeitado — para o strlen a string tem 22

Os quatro acentos (ã, ç, ã, á) ocupam 2 bytes cada, somando 4 bytes extras: 18 letras + 4 = 22 bytes. A validação por bytes reprova um nome perfeitamente válido, e o usuário não entende por quê — a mensagem diz "máximo 20 caracteres" e ele contou 18. Valide com mb_strlen. É por isso que este bug costuma chegar como reclamação de usuário com nome acentuado, e não como erro no log.

Quando strlen continua certo

Aqui está a parte que quase nenhum tutorial diz: não saia trocando todo strlen por mb_strlen. Os dois respondem perguntas diferentes, e às vezes a pergunta certa é mesmo sobre bytes:

A regra prática que resume tudo: texto que veio de gente (ou vai para gente) → mb_*; bytes que vão para disco, rede ou memória → strlen.

A camada seguinte: caractere ≠ o que você vê

Já que você chegou até aqui, vale saber que mb_strlen também tem um limite — ela conta code points, que nem sempre correspondem ao que aparece na tela. Duas situações reais:

$pre  = "é";            // 1 code point (U+00E9)      → mb_strlen = 1
$comp = "e\u{0301}";    // "e" + acento combinante     → mb_strlen = 2
var_dump($pre === $comp);  // false — e os dois aparecem como "é" na tela!

As duas formas são visualmente idênticas, mas são strings diferentes — é a explicação para o clássico "o usuário jura que digitou o mesmo nome, mas a busca não acha". Quando o texto vem de fontes variadas (macOS, colagem de PDF, APIs), normalize antes de comparar ou salvar, com a extensão intl:

$normalizado = Normalizer::normalize($texto, Normalizer::FORM_C);

O mesmo vale para emojis compostos: uma família 👨‍👩‍👧 é um símbolo na tela, mas mb_strlen conta 5 (são vários code points unidos por junções invisíveis). Quando o que importa é o que o olho enxerga, a medida correta é o grapheme: grapheme_strlen(), também da intl.

Resumo de decisão

Situação Use Por quê
Contar letras de texto do usuário mb_strlen strlen conta bytes e infla com acento
Cortar resumo/preview mb_substr substr parte a letra e gera
Validar tamanho de campo mb_strlen Senão rejeita nome acentuado válido
Maiúscula/minúscula com acento mb_strtoupper / mb_strtolower As versões byte ignoram acentos
Medir arquivo, hash, Content-Length strlen A pergunta é mesmo sobre bytes
Comparar textos de fontes diferentes Normalizer::normalize antes "é" pode ser 1 ou 2 code points
Contar o que o usuário (emoji) grapheme_strlen (intl) Um símbolo pode ter vários code points
no manualMultibyte String Functions

A seção de funções multibyte lista a família mb_* inteira — contagem, corte, busca, caixa, conversão e divisão. Conhecer os pares completos é o que blinda seu código contra texto acentuado.

estudar na árvore →
no manualStrings

Entender que uma string em PHP é uma sequência de bytes (e não de caracteres) é a raiz de tudo o que este artigo explica — do strlen inflado ao corte que gera .

estudar na árvore →

Leve o estudo além do artigo

O manual inteiro do PHP como uma jornada: progresso por tópico, anotações e leitura embutida.