PHP Acentos e mbstring

Na aula de Funções de String, algumas respostas pareceram estranhas: strlen("maçã") deu 6, e strtoupper("joão") deu JOãO. Não é defeito do seu computador: as funções clássicas do PHP contam bytes, não letras. Para quem escreve em português, essa aula é obrigatória.

Você vai aprender
  • Os quatro sintomas: tamanho errado, maiúscula pela metade, corte no meio da letra e texto invertido virando lixo.
  • A ideia do UTF-8: por que uma letra pode ocupar 1, 2, 3 ou 4 bytes.
  • As funções mb_: mb_strlen, mb_strtoupper, mb_strtolower, mb_substr, mb_str_split, mb_strpos, mb_convert_case.
  • As novas: mb_str_pad (PHP 8.3), mb_trim, mb_ucfirst e mb_lcfirst (PHP 8.4).
  • A regra prática: com texto em português, use mb_.

O problema, ao vivo

A Ana tem uma lojinha de frutas e começou a usar as funções de string nos nomes dos produtos:

<?php
$fruta = "maçã";
echo strlen($fruta), "\n";
echo strtoupper("Olá"), "\n";
echo substr($fruta, 0, 3), "\n";
echo strrev($fruta), "\n";
$ php problema.php
6           « "maçã" tem 4 letras, não 6
OLá         « o "á" ficou minúsculo
ma�         « pediu 3 letras e veio "ma" + um símbolo estranho
�ç�am       « invertido, virou lixo

Quatro respostas erradas, e nenhuma mensagem de erro. O PHP fez exatamente o que essas funções prometem; o problema é que elas foram feitas para o inglês, que não tem acentos. Para entender, precisamos olhar como o texto fica guardado.


A ideia: letras viram bytes (UTF-8)

O computador só guarda números. Cada número pequeno, de 0 a 255, é um byte. Para guardar texto, existe uma tabela que diz qual número representa cada letra. A tabela usada hoje em quase todo lugar (sites, Linux, PHP) se chama UTF-8.

Pense numa caixa de correio com escaninhos. As letras simples do inglês (a–z, números, pontuação) cabem num escaninho só. Letras acentuadas são "encomendas maiores" e ocupam dois escaninhos. Símbolos como € ocupam três, e emojis, quatro.

<?php
echo bin2hex("maçã"), "\n";
echo strlen("a"), " ", strlen("ç"), " ", strlen("€"), " ", strlen("😀"), "\n";
$ php bytes.php
6d61c3a7c3a3      « os bytes de "maçã", em hexadecimal (2 dígitos = 1 byte)
1 2 3 4           « a = 1 byte, ç = 2, € = 3, 😀 = 4

bin2hex mostra os bytes "crus" de um texto. Separando de dois em dois dígitos:

Letramaçã
Bytes6d61c3a7c3a3
Posição do byte012345

Agora os quatro sintomas fazem sentido:

  • strlen deu 6 porque contou os 6 bytes.
  • substr($fruta, 0, 3) pegou os bytes 0, 1 e 2: m, a e só a primeira metade do ç (c3). Meia letra não é nada; o terminal (ou o navegador) mostra no lugar o símbolo de "caractere inválido", �.
  • strrev inverteu os bytes um a um, separando as metades de cada letra. Por sorte, c3 a7 ficou junto de novo no meio (o ç que aparece), mas o resto virou �.
  • strtoupper só conhece as 26 letras sem acento. Os dois bytes do á não são nenhuma delas, então ficaram como estavam.
Você não precisa decorar bytes

Basta lembrar da ideia: no UTF-8, letras acentuadas ocupam mais de um byte, e as funções antigas do PHP contam bytes. O PHP não tem um "tipo texto com acentos": uma string é só uma sequência de bytes. Quem decide se conta bytes ou letras é a função que você escolhe.


A solução: as funções mb_

O PHP tem uma extensão chamada mbstring (de multibyte string, "texto de vários bytes"). Ela traz versões das funções de string que entendem UTF-8 e contam letras (o nome técnico é "caracteres"). Quase todas têm o mesmo nome da original com mb_ na frente e recebem os mesmos argumentos.

<?php
$fruta = "maçã";
echo mb_strlen($fruta), "\n";
echo mb_strtoupper("Olá, João"), "\n";
echo mb_strtolower("ÁRVORE"), "\n";
echo mb_substr($fruta, 0, 3), "\n";
echo mb_substr("Conceição", -4), "\n";
$ php solucao.php
4             « 4 letras
OLÁ, JOÃO     « agora o Á e o à também
árvore
maç           « 3 letras inteiras
ição          « negativos contam do fim, como no substr
Sem mb_ (bytes)ResultadoCom mb_ (letras)Resultado
strlen("maçã")6mb_strlen("maçã")4
strtoupper("Olá")OLámb_strtoupper("Olá")OLÁ
strtolower("ÁRVORE")Árvoremb_strtolower("ÁRVORE")árvore
substr("maçã", 0, 3)ma�mb_substr("maçã", 0, 3)maç

Um exemplo do dia a dia: o cadastro da escola da Ana aceita nomes de até 10 letras. Com strlen, a aluna Conceição seria recusada:

<?php
$nome = "Conceição";
echo strlen($nome), "\n";
echo mb_strlen($nome), "\n";
$ php limite.php
11       « strlen: 11 bytes, "passou do limite"
9        « mb_strlen: 9 letras, a resposta certa

Missão: o programa abaixo diz que "Conceição" tem 11 letras. Rode, veja o erro e corrija para mostrar 9 letras.

<?php
$nome = "Conceição";
echo strlen($nome), " letras\n";
<?php
$nome = "Conceição";
echo mb_strlen($nome), " letras\n";

Letra por letra: mb_str_split

str_split quebra um texto em pedaços de 1 byte, o que parte as letras acentuadas. mb_str_split quebra em letras:

<?php
print_r(str_split("pé"));
print_r(mb_str_split("maçã"));
echo implode("", array_reverse(mb_str_split("maçã"))), "\n";
$ php letras.php
Array
(
    [0] => p
    [1] => �        « metade do é
    [2] => �        « a outra metade
)
Array
(
    [0] => m
    [1] => a
    [2] => ç
    [3] => ã
)
ãçam               « maçã ao contrário, do jeito certo

Não existe mb_strrev. A última linha mostra como inverter com acentos: mb_str_split separa as letras, array_reverse inverte a lista (veja Funções de Array) e implode("", …) junta tudo de novo, sem nada entre as letras. Leia de dentro para fora.


Maiúsculas no começo: mb_convert_case, mb_ucfirst e mb_lcfirst

<?php
echo ucwords("joão da silva"), "\n";
echo mb_convert_case("JOÃO DA SILVA", MB_CASE_TITLE), "\n";
echo ucfirst("érica"), "\n";
echo mb_ucfirst("érica"), "\n";
echo mb_lcfirst("Érica"), "\n";
$ php capitalizar.php
João Da Silva       « ucwords funcionou porque o j não tem acento
João Da Silva       « mb_convert_case: minúsculas e depois a 1ª de cada palavra
érica               « ucfirst não sabe fazer É
Érica               « mb_ucfirst sabe
érica
mb_convert_case(texto, modo)
"Converter maiúsculas/minúsculas". O modo é uma constante: MB_CASE_UPPER (tudo maiúsculo), MB_CASE_LOWER (tudo minúsculo) ou MB_CASE_TITLE ("de título": primeira de cada palavra maiúscula, o resto minúsculo).
mb_ucfirst / mb_lcfirst PHP 8.4+
Primeira letra maiúscula / minúscula, com acento. Antes do 8.4: mb_strtoupper(mb_substr($s, 0, 1)) . mb_substr($s, 1).

O MB_CASE_TITLE também resolve a ordem: ele já passa o resto da palavra para minúsculas, então "JOÃO" vira "João" sem precisar de um mb_strtolower antes.


Alinhar colunas: mb_str_pad PHP 8.3+

A Ana quer imprimir a lista de clientes e frutas favoritas em colunas. Com str_pad, que conta bytes, as linhas com acento ficam tortas:

<?php
echo str_pad("Ana", 10), str_pad("maçã", 8), "|\n";
echo str_pad("Bruno", 10), str_pad("uva", 8), "|\n";
echo str_pad("Conceição", 10), str_pad("limão", 8), "|\n";
echo "--- com mb_str_pad ---\n";
echo mb_str_pad("Ana", 10), mb_str_pad("maçã", 8), "|\n";
echo mb_str_pad("Bruno", 10), mb_str_pad("uva", 8), "|\n";
echo mb_str_pad("Conceição", 10), mb_str_pad("limão", 8), "|\n";
$ php colunas.php
Ana       maçã  |
Bruno     uva     |
Conceiçãolimão  |       « "Conceição" já tem 11 bytes: nenhum espaço foi posto
--- com mb_str_pad ---
Ana       maçã    |
Bruno     uva     |
Conceição limão   |     « tudo alinhado

str_pad achou que "maçã" (6 bytes) só precisava de 2 espaços para chegar a 8, e que "Conceição" (11 bytes) já passava de 10. mb_str_pad conta letras e alinha certo. Os argumentos são os mesmos do str_pad: texto, tamanho, com o que completar e o lado (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).

Antes do PHP 8.3

mb_str_pad não existe (dá Call to undefined function mb_str_pad()). A alternativa é somar a diferença de bytes: str_pad($s, 10 + strlen($s) - mb_strlen($s)).


Limpar espaços "especiais": mb_trim PHP 8.4+

O trim remove espaços, tabs e quebras de linha comuns. Mas existem espaços "especiais" do UTF-8. O mais comum é o espaço não separável (código U+00A0), que aparece quando você copia texto de um site, do Word ou de um PDF. Ele parece um espaço, mas o trim não o reconhece:

<?php
$colado = "\u{00A0}Ana Souza\u{00A0}";
echo "[", trim($colado), "]\n";
echo "[", mb_trim($colado), "]\n";
$ php aparar.php
[ Ana Souza ]       « os espaços especiais ficaram
[Ana Souza]

O "\u{00A0}" é a forma de escrever um caractere pelo seu código dentro de aspas duplas (veja Strings). Também existem mb_ltrim e mb_rtrim PHP 8.4+. Antes do 8.4, use uma expressão regular: preg_replace('/^\s+|\s+$/u', '', $s).


Procurar: mb_strpos e companhia

As funções de busca também têm versão mb_. A diferença está no número devolvido: posição em letras ou em bytes.

<?php
echo strpos("maçã verde", "verde"), "\n";
echo mb_strpos("maçã verde", "verde"), "\n";
$ php posicao.php
7          « em bytes
5          « em letras: m-a-ç-ã-espaço, e o "verde" começa na 5ª posição

Se você vai usar a posição com substr, use a mesma "família": strpos com substr, ou mb_strpos com mb_substr. Misturar as duas é um bug comum. Para só saber se contém, str_contains funciona bem com acentos (ele compara os bytes e não precisa contar nada).


Quais funções precisam do mb_?

A regra: precisa quando a função conta, corta ou muda letras. Quando ela só procura ou troca um texto inteiro por outro, a versão comum funciona.

Função comumCom acentosUse
strlenconta bytesmb_strlen
strtoupper / strtolowerignora acentosmb_strtoupper / mb_strtolower
ucfirst / lcfirstignora acentosmb_ucfirst / mb_lcfirst 8.4+
ucwordsignora acentosmb_convert_case($s, MB_CASE_TITLE)
substrcorta no meio da letramb_substr
strpos / stripos / strrposposição em bytesmb_strpos / mb_stripos / mb_strrpos
str_splitparte as letrasmb_str_split
strrevvira lixoimplode("", array_reverse(mb_str_split($s)))
str_padalinha erradomb_str_pad 8.3+
trimnão vê espaços especiaismb_trim 8.4+
substr_countfunciona(ou mb_substr_count)
str_replace, str_contains, str_starts_with, str_ends_with, explode, implodefuncionama versão comum
wordwrapconta bytes; com corte, parte letrasnão há mb_wordwrap; cuidado
str_word_counttrata acentos como separadorcount(explode(" ", $s))
A regra desta aula

Com texto em português (nomes, endereços, produtos, qualquer coisa que um usuário digita), use mb_ para contar, cortar e mudar maiúsculas. Não custa nada, e o programa nunca vai quebrar com a "Conceição" ou o "João".


Quando dá errado

Aparece � no meio do texto
<?php
$nome = "Érica";
$sobrenome = "Souza";
echo substr($nome, 0, 1) . substr($sobrenome, 0, 1), "\n";
echo mb_substr($nome, 0, 1) . mb_substr($sobrenome, 0, 1), "\n";
$ php iniciais.php
�S       « substr pegou só o 1º byte do É
ÉS

O que significa: o � é como o terminal e o navegador mostram um byte que sozinho não forma letra nenhuma. Alguma função de bytes (substr, str_split, strrev, wordwrap com corte) partiu uma letra ao meio.

Como consertar: troque pela versão mb_ (mb_substr, mb_str_split).

Aparece Olá em vez de Olá
$ php -r 'echo mb_convert_encoding("Olá, João", "UTF-8", "ISO-8859-1"), "\n";'
Olá, João

O que significa: esse "embaralhado" (chamado mojibake) acontece quando um texto em UTF-8 é lido como se estivesse em outra tabela, a antiga ISO-8859-1 (Latin-1). Cada byte do á (c3 a1) virou uma letra separada: Ã e ¡. O comando acima provoca o problema de propósito, para você reconhecê-lo.

Como consertar: use UTF-8 em todo lugar: salve os arquivos .php em UTF-8 no editor (veja Editores e IDEs), coloque <meta charset="utf-8"> no HTML e, no banco de dados, use utf8mb4 (veja MySQL e MariaDB). O PHP 8 já usa UTF-8 como padrão.

must be a valid encoding
<?php
echo mb_strlen("maçã", "utf-9"), "\n";
$ php enc.php

Fatal error: Uncaught ValueError: mb_strlen(): Argument #2 ($encoding) must be a valid encoding, "utf-9" given in /home/ana/php/enc.php:2
Stack trace:
#0 /home/ana/php/enc.php(2): mb_strlen('ma\xC3\xA7\xC3\xA3', 'utf-9')
#1 {main}
  thrown in /home/ana/php/enc.php on line 2

O que significa: as funções mb_ aceitam um último argumento opcional com o nome da codificação. "utf-9" não existe. Repare na linha #0: o PHP mostra os argumentos, e o "maçã" aparece como 'ma\xC3\xA7\xC3\xA3', os mesmos bytes que você viu no começo da aula.

Como consertar: não passe a codificação (o padrão já é UTF-8) ou escreva "UTF-8".

Call to undefined function mb_strlen()

O que significa: a extensão mbstring não está instalada. No laboratório e no editor do guia ela está, mas em algumas instalações do Linux ela vem num pacote separado. A mensagem é a mesma de qualquer função inexistente: Fatal error: Uncaught Error: Call to undefined function mb_strlen().

Como consertar: no Ubuntu/Debian, sudo apt install php-mbstring (veja Instalar no Linux). Confira com php -m, que lista as extensões: mbstring deve aparecer. Se a mensagem for sobre mb_str_pad, mb_trim ou mb_ucfirst, o problema é a versão do PHP (8.3/8.4+), não a extensão.


Referência

FunçãoO que fazExemplo → resultado
mb_strlen($s)Número de letrasmb_strlen("maçã") → 4
mb_strtoupper($s)Tudo maiúsculomb_strtoupper("ação") → AÇÃO
mb_strtolower($s)Tudo minúsculomb_strtolower("ÁRVORE") → árvore
mb_convert_case($s, MB_CASE_TITLE)1ª de cada palavra"JOÃO DA SILVA" → João Da Silva
mb_ucfirst / mb_lcfirst 8.4+1ª letra maiúscula / minúsculamb_ucfirst("érica") → Érica
mb_substr($s, $ini, $qtd)Recorta letrasmb_substr("maçã", 0, 3) → maç
mb_strpos($s, $p)Posição em letras, ou falsemb_strpos("maçã verde", "verde") → 5
mb_str_split($s)Lista de letrasmb_str_split("pé") → ["p", "é"]
mb_str_pad($s, $tam, $com, $lado) 8.3+Completa contando letrasmb_str_pad("maçã", 6, ".") → maçã..
mb_trim / mb_ltrim / mb_rtrim 8.4+Tira espaços (inclusive os especiais)mb_trim("\u{00A0}Ana") → Ana
mb_substr_count($s, $p)Quantas vezes aparecemb_substr_count("maçã com maçã", "maçã") → 2
mb_strimwidth($s, 0, $larg, "...")Encurta com reticênciasmb_strimwidth("Amanhã não haverá aula", 0, 12, "...") → Amanhã nã...
bin2hex($s)Mostra os bytes (para investigar)bin2hex("ç") → c3a7

Exercícios

1. Contar letras

Complete para mostrar 4:

echo ("maçã");

2. Gritar em português

Complete para mostrar CORAÇÃO:

echo ("coração");

3. A primeira letra

Complete para pegar só a primeira letra de $nome (que pode ser "Érica"):

$inicial = mb_substr($nome, );

4. Quanto vale strlen("pão")?

5. Qual destas funções não precisa da versão mb_ para funcionar com acentos?

6. Um nome apareceu na tela como Jo�o. Qual a causa mais provável?

Missão final: o formulário da escola mandou o nome " joão da conceição ", com espaços sobrando. Faça o programa mostrar o nome sem os espaços das pontas, todo em maiúsculas e, na linha de baixo, quantas letras ele tem:
JOÃO DA CONCEIÇÃO
17 letras
Rode primeiro para ver os erros do código atual.

<?php
$digitado = "  joão da conceição  ";
echo strtoupper($digitado), "\n";
echo strlen($digitado), " letras\n";
<?php
$digitado = "  joão da conceição  ";
$nome = trim($digitado);
echo mb_strtoupper($nome), "\n";
echo mb_strlen($nome), " letras\n";

Resumo

  • No UTF-8, letras sem acento ocupam 1 byte; acentuadas, 2; símbolos como €, 3; emojis, 4.
  • As funções clássicas (strlen, substr, strtoupper, strrev, str_pad…) trabalham com bytes: com acentos, contam errado, cortam letras ao meio (�) e não mudam maiúsculas.
  • As funções mb_ (extensão mbstring) trabalham com letras: mb_strlen, mb_strtoupper, mb_strtolower, mb_substr, mb_strpos, mb_str_split, mb_convert_case.
  • Novidades: mb_str_pad (8.3), mb_trim, mb_ucfirst, mb_lcfirst (8.4).
  • str_replace, str_contains, explode e implode funcionam normalmente com acentos.
  • Regra: texto em português → mb_. E UTF-8 em todo lugar (arquivos, HTML, banco).