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.
- 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_ucfirstemb_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:
| Letra | m | a | ç | ã | ||
|---|---|---|---|---|---|---|
| Bytes | 6d | 61 | c3 | a7 | c3 | a3 |
| Posição do byte | 0 | 1 | 2 | 3 | 4 | 5 |
Agora os quatro sintomas fazem sentido:
strlendeu 6 porque contou os 6 bytes.substr($fruta, 0, 3)pegou os bytes 0, 1 e 2:m,ae 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",�.strrevinverteu os bytes um a um, separando as metades de cada letra. Por sorte,c3 a7ficou junto de novo no meio (oçque aparece), mas o resto virou�.strtouppersó conhece as 26 letras sem acento. Os dois bytes doánão são nenhuma delas, então ficaram como estavam.
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) | Resultado | Com mb_ (letras) | Resultado |
|---|---|---|---|
strlen("maçã") | 6 | mb_strlen("maçã") | 4 |
strtoupper("Olá") | OLá | mb_strtoupper("Olá") | OLÁ |
strtolower("ÁRVORE") | Árvore | mb_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) ouMB_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).
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 comum | Com acentos | Use |
|---|---|---|
strlen | conta bytes | mb_strlen |
strtoupper / strtolower | ignora acentos | mb_strtoupper / mb_strtolower |
ucfirst / lcfirst | ignora acentos | mb_ucfirst / mb_lcfirst 8.4+ |
ucwords | ignora acentos | mb_convert_case($s, MB_CASE_TITLE) |
substr | corta no meio da letra | mb_substr |
strpos / stripos / strrpos | posição em bytes | mb_strpos / mb_stripos / mb_strrpos |
str_split | parte as letras | mb_str_split |
strrev | vira lixo | implode("", array_reverse(mb_str_split($s))) |
str_pad | alinha errado | mb_str_pad 8.3+ |
trim | não vê espaços especiais | mb_trim 8.4+ |
substr_count | funciona | (ou mb_substr_count) |
str_replace, str_contains, str_starts_with, str_ends_with, explode, implode | funcionam | a versão comum |
wordwrap | conta bytes; com corte, parte letras | não há mb_wordwrap; cuidado |
str_word_count | trata acentos como separador | count(explode(" ", $s)) |
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
� 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).
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.
<?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".
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ção | O que faz | Exemplo → resultado |
|---|---|---|
mb_strlen($s) | Número de letras | mb_strlen("maçã") → 4 |
mb_strtoupper($s) | Tudo maiúsculo | mb_strtoupper("ação") → AÇÃO |
mb_strtolower($s) | Tudo minúsculo | mb_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úscula | mb_ucfirst("érica") → Érica |
mb_substr($s, $ini, $qtd) | Recorta letras | mb_substr("maçã", 0, 3) → maç |
mb_strpos($s, $p) | Posição em letras, ou false | mb_strpos("maçã verde", "verde") → 5 |
mb_str_split($s) | Lista de letras | mb_str_split("pé") → ["p", "é"] |
mb_str_pad($s, $tam, $com, $lado) 8.3+ | Completa contando letras | mb_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 aparece | mb_substr_count("maçã com maçã", "maçã") → 2 |
mb_strimwidth($s, 0, $larg, "...") | Encurta com reticências | mb_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:
2. Gritar em português
Complete para mostrar CORAÇÃO:
3. A primeira letra
Complete para pegar só a primeira letra de $nome (que pode ser "Érica"):
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ÇÃO17 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,explodeeimplodefuncionam normalmente com acentos.- Regra: texto em português →
mb_. E UTF-8 em todo lugar (arquivos, HTML, banco).