Programação Por Equipe DevBras

Capa do artigo: strlen não conta letra: limite de caracteres que bate na tela, no PHP e no banco

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 strlen conta bytes. Para "ação", devolve 6.
  • PHP mb_strlen com UTF-8 conta pontos de código, que é o que quase todo mundo chama de caractere. Para "ação", devolve 4.
  • JavaScript .length conta unidades de UTF-16. Letras acentuadas valem 1, mas o emoji vale 2. O atributo maxlength do HTML usa essa mesma contagem.
  • MySQL VARCHAR(160) com utf8mb4 limita em caracteres. A função LENGTH() devolve bytes e CHAR_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

  1. Decida a unidade do limite. Para campo de texto humano, a escolha prática é ponto de código: é o que mb_strlen e o VARCHAR do MySQL medem.
  2. Normalize o texto para a forma composta (NFC) antes de contar e de gravar. Assim "é" decomposto vira um caractere só.
  3. Valide no servidor com mb_strlen. Essa é a regra que vale.
  4. Mostre o contador na tela com a mesma unidade, contando pontos de código em vez de .length.
  5. Confira que a coluna é utf8mb4, para emoji caber, e que o tamanho em caracteres comporta o limite.
  6. 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 strlen um 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 o json_encode recusa.
  • Usar maxlength como limite principal. Ele conta emoji como 2 e trava a pessoa antes do limite que o servidor aceitaria.
  • Contador da tela com .length e servidor com mb_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 utf8 antigo. O texto com emoji passa na validação e falha no INSERT, 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_strlen com UTF-8, não strlen.
  • O texto é normalizado para NFC antes de contar e gravar.
  • O contador da tela conta pontos de código, não .length.
  • Nenhum maxlength menor que o limite real trava quem usa emoji.
  • A coluna usa utf8mb4 e 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?

Avalie este artigo

Referência: http://php.net/manual/pt_BR/function.strlen.php