Vírgula sobrando no fim da lista: use join em vez de cortar o último caractere
Cortar o último caractere para tirar a vírgula quebra acento, falha com lista vazia e esconde o bug. Veja como juntar com separador em PHP, JavaScript e SQL.
O e-mail de confirmação do pedido chegou assim: "Itens: Café, Pão de queijo, Maçã,". A vírgula sobrando no fim veio de um laço que acrescenta ", " depois de cada item, seguido de um substr($texto, 0, -1) que deveria limpar o fim. Só que o separador tem dois caracteres, a vírgula e o espaço, e o corte tirou só o espaço. A vírgula ficou.
A resposta curta: não ponha o separador para depois tirar; junte os itens com o separador entre eles. No PHP, implode(', ', $itens); no JavaScript, itens.join(', '); no SQL, GROUP_CONCAT ou STRING_AGG. Se você realmente precisa remover um fim, confira que ele está lá com str_ends_with e corte exatamente o tamanho dele.
Por que cortar o último caractere quase sempre é o conserto errado
Remover o fim trata o sintoma. O problema real é que o código monta o texto errado e depois tenta adivinhar o que sobrou. Essa adivinhação falha de vários jeitos, e cada um aparece num dia diferente:
- O separador muda de tamanho. Alguém troca ", " por " | " e o corte de 2 caracteres passa a deixar um espaço, ou a comer uma letra.
- O separador não está lá. Se uma regra pula a vírgula no último item, o corte apaga a última letra do item. "Maçã" vira "Maç".
- O caractere tem mais de um byte.
substrdo PHP anda em byte. Cortar 1 byte de "Maçã" tira só metade do "ã", e o e-mail mostra um losango. A explicação de por que isso acontece está em strlen não conta letra. - A lista está vazia. O laço não roda, o texto é vazio, e o corte tenta tirar de onde não há nada. No PHP 8,
substrdevolve string vazia; em versões antigas devolviafalse, que vazava para o resto do código.
Juntar com separador elimina todos esses casos de uma vez: com zero itens, o resultado é vazio; com um, é o item sozinho; com vários, o separador fica só entre eles.
Como montar a lista do jeito certo
- Guarde os itens numa lista (array) em vez de concatenar texto dentro do laço.
- Tire da lista os itens vazios ou só com espaços, se eles puderem aparecer.
- Junte com a função da linguagem, passando o separador.
- Se o formato é estruturado (CSV, JSON, query string), use a função específica do formato, não a junção manual.
- Teste com lista vazia, com um item e com um item que termina em acento.
O pedido em PHP, antes e depois
$itens = ['Café', 'Pão de queijo', 'Maçã'];
// antes
$texto = '';
foreach ($itens as $item) {
$texto .= $item . ', ';
}
$texto = substr($texto, 0, -1);
// "Café, Pão de queijo, Maçã,"
// depois
$texto = implode(', ', $itens);
// "Café, Pão de queijo, Maçã"
Quando os itens vêm do banco ou de um formulário, alguns podem estar vazios. Filtre antes de juntar, para não gerar ", ," no meio:
$limpos = array_filter(array_map('trim', $itens), fn ($item) => $item !== '');
$texto = implode(', ', $limpos);
O array_filter com a função explícita evita um detalhe do PHP: sem ela, o item '0' seria considerado falso e sumiria da lista.
Quando você precisa mesmo tirar um fim
Às vezes o texto vem pronto de outro sistema, com uma barra no fim da URL ou um ponto e vírgula sobrando. Nesse caso, confira o fim e corte pelo tamanho dele:
$fim = ', ';
if (str_ends_with($texto, $fim)) {
$texto = substr($texto, 0, -strlen($fim));
}
Aqui o substr é seguro, porque você sabe exatamente quais bytes está removendo: os do sufixo que acabou de conferir. A função str_ends_with existe a partir do PHP 8.0. Se o que você quer é remover a última letra visível de um texto qualquer, use mb_substr($texto, 0, -1, 'UTF-8'), que conta caracteres e não bytes.
Cuidado com rtrim($texto, ', '). Ele não remove o texto ", ", e sim qualquer combinação de vírgulas e espaços no fim, quantas houver. Às vezes é o que você quer; às vezes apaga mais do que devia. E, na lista de caracteres do rtrim, dois pontos seguidos (..) indicam um intervalo, então não use a função com sufixos que tenham pontos sem ler a documentação.
Em JavaScript
const itens = ['Café', 'Pão de queijo', 'Maçã'];
itens.join(', ');
// "Café, Pão de queijo, Maçã"
new Intl.ListFormat('pt-BR', { style: 'long', type: 'conjunction' }).format(itens);
// "Café, Pão de queijo e Maçã"
O Intl.ListFormat monta a lista como se escreve em português, com "e" antes do último item, e cuida dos casos de um e dois itens. No JavaScript, texto.slice(0, -1) remove uma unidade de UTF-16, o que parte emoji ao meio. Para cortar texto visível com segurança, o post cortar texto sem partir a letra mostra o Intl.Segmenter.
No banco de dados
-- MySQL
SELECT p.id, GROUP_CONCAT(i.nome ORDER BY i.nome SEPARATOR ', ') AS itens
FROM pedidos p
JOIN itens_pedido i ON i.pedido_id = p.id
GROUP BY p.id;
-- PostgreSQL e SQL Server
SELECT p.id, STRING_AGG(i.nome, ', ') AS itens
FROM pedidos p
JOIN itens_pedido i ON i.pedido_id = p.id
GROUP BY p.id;
No MySQL, GROUP_CONCAT tem um limite de tamanho controlado pela variável group_concat_max_len, que por padrão é 1024. Acima disso, o resultado é cortado com apenas um aviso, e o pedido com muitos itens perde os últimos sem erro nenhum. Se a lista pode ser longa, aumente o limite na sessão ou junte no código da aplicação.
Formatos que já têm função própria
Lista separada por vírgula às vezes é um formato, não um texto para gente ler. Nesses casos, montar à mão é pedir problema com aspas, vírgulas dentro do valor e caracteres especiais:
- CSV:
fputcsvno PHP coloca aspas onde precisa e escapa as que já existem. - Query string:
http_build_queryno PHP eURLSearchParamsno JavaScript codificam espaço, acento e "&". - JSON:
json_encodeeJSON.stringify, nunca concatenação com vírgula. - SQL com lista de ids: parâmetros, não um texto "1, 2, 3" colado na consulta. O motivo está em Node.js e MySQL com parâmetro.
Erros comuns
- Acrescentar o separador em todo item e cortar depois. Qualquer mudança no separador deixa sobra ou come uma letra.
- Cortar com
substrsem conferir o fim. Quando o separador não está lá, o último item perde o último caractere. - Cortar byte em texto com acento. O "ã" partido vira losango no e-mail do cliente.
- Não testar lista vazia. Em PHP antigo, o
falsevaza; em qualquer versão, o texto pode sair com um rótulo sem itens. - Confiar no
GROUP_CONCATsem pensar no limite. Pedidos grandes chegam com a lista cortada, sem erro. - Montar CSV ou query string na mão. O primeiro valor com vírgula ou "&" quebra o formato.
Checklist
- Nenhum laço acrescenta separador depois de cada item para cortar depois.
- Listas são juntadas com
implode,joinou equivalente. - Itens vazios são filtrados antes da junção.
- Todo corte de fim confere o sufixo antes e corta pelo tamanho dele.
- Corte de letra visível usa função de caractere, não de byte.
- Há teste com zero itens, um item e item terminando em acento.
- CSV, JSON e query string usam a função do formato.
- Consultas com
GROUP_CONCATconsideram o limite de tamanho.
Procure no seu código por substr( seguido de -1 e por slice(0, -1). Cada ocorrência está removendo algo que você conferiu antes, ou está apagando o último caractere e torcendo para ser a vírgula?
Referência: http://www.ctasoftware.com.br/blog/remover-ultimo-caractere-php/