Strings em Profundidade

Strings em Profundidade

Texto em PHP é sequência de bytes, e é dessa decisão que vêm quase todas as surpresas: o acento que conta dois no limite de caracteres, o corte que parte a letra ao meio, a regex que recusa palavra em português. As funções essenciais, a família mb_, datas, expressões regulares e escape por contexto.
PHP

21 min de leitura

Strings são o tipo de dado mais presente em qualquer aplicação web. Toda entrada de usuário chega como string. Todo HTML gerado é uma string. Todo JSON enviado e recebido passa por strings. Manipular texto com precisão, segurança e eficiência é uma habilidade central do desenvolvimento PHP.

Neste artigo vamos além do básico — cobrimos as funções mais importantes da biblioteca de strings do PHP, formatação de números e datas, e uma introdução prática às expressões regulares com preg_match e preg_replace.

Revisão rápida: aspas simples vs duplas

Antes de avançar, vale consolidar um ponto do Artigo 04:

<?php
declare(strict_types=1);

$nome = "Ricardo";

// Aspas simples — literal, sem interpolação, mais rápido
echo 'Olá, $nome';       // Olá, $nome (literal)
echo 'Ela disse: "oi"';  // sem necessidade de escapar aspas duplas

// Aspas duplas — interpola variáveis e sequências de escape
echo "Olá, $nome";          // Olá, Ricardo
echo "Olá, {$nome}!";       // Olá, Ricardo! — chaves delimitam a variável
echo "Linha 1\nLinha 2";    // \n = quebra de linha
echo "Coluna1\tColuna2";    // \t = tabulação

// Heredoc — para strings longas com interpolação
$html = <<<HTML
    <div class="perfil">
        <h1>$nome</h1>
    </div>
    HTML;

// Nowdoc — como heredoc mas sem interpolação (equivalente às aspas simples)
$sql = <<<'SQL'
    SELECT * FROM usuarios WHERE nome = '$nome'
    SQL;
// $nome não é interpolado — útil para SQL, templates, etc.

A regra prática: use aspas simples quando não precisar de interpolação. O código fica mais rápido e deixa claro que a string é literal.

Funções fundamentais de string

<?php
declare(strict_types=1);

$texto = "  Dominando o PHP em 1 Ano  ";
$frase = "O rato roeu a roupa do rei de Roma";

// Tamanho e limpeza
echo strlen($texto);          // 28 — inclui os espaços
echo mb_strlen($texto);       // 28 — versão multibyte (use para Unicode)
echo trim($texto);            // "Dominando o PHP em 1 Ano"
echo ltrim($texto);           // remove espaços só da esquerda
echo rtrim($texto);           // remove espaços só da direita
echo trim($texto, " D");      // remove " " e "D" das bordas

// Capitalização
echo strtolower($frase);      // tudo minúsculo
echo strtoupper($frase);      // TUDO MAIÚSCULO
echo ucfirst("olá mundo");    // Olá mundo — primeira letra maiúscula
echo ucwords("olá mundo php");// Olá Mundo Php — primeira de cada palavra
echo mb_strtolower("AÇÃO");   // ação — versão multibyte para acentos

// Busca e posição
echo strpos($frase, "rato");     // 3 — posição da primeira ocorrência
echo strrpos($frase, "ro");      // 27 — posição da última ocorrência
echo substr_count($frase, "ro"); // 4 — quantas vezes "ro" aparece

// Atenção: strpos pode retornar 0 (início da string) — use !== false
$pos = strpos($frase, "O");
if ($pos !== false) {
    echo "Encontrado na posição $pos\n"; // 0
}

// substr — extrai parte da string
echo substr($frase, 3, 4);    // "rato" — começa na pos 3, pega 4 chars
echo substr($frase, -4);      // "Roma" — 4 caracteres do final
echo mb_substr($frase, 3, 4); // versão multibyte

// Substituição
echo str_replace("rato", "gato", $frase);
// "O gato roeu a roupa do rei de Roma"

// Substituição case-insensitive
echo str_ireplace("RAtO", "gato", $frase);
// "O gato roeu a roupa do rei de Roma"

// Múltiplas substituições de uma vez
echo str_replace(
    ["rato", "roupa", "rei"],    // buscar
    ["gato", "calça", "dono"],   // substituir por
    $frase
);

Divisão e junção

<?php
declare(strict_types=1);

$csv      = "Ana,Bruno,Carla,David";
$frase    = "PHP é incrível";
$nomes    = ["Ana", "Bruno", "Carla"];

// explode — divide string em array pelo separador
$partes = explode(",", $csv);
// ["Ana", "Bruno", "Carla", "David"]

// Terceiro argumento limita o número de partes
$limitado = explode(",", $csv, 2);
// ["Ana", "Bruno,Carla,David"] — divide no máximo em 2 partes

// implode — une array em string com separador
echo implode(", ", $nomes);     // "Ana, Bruno, Carla"
echo implode(" | ", $nomes);    // "Ana | Bruno | Carla"
echo implode("", $nomes);       // "AnaBrunoCarla"

// str_split — divide em pedaços de N caracteres
$chars = str_split("PHP", 1);   // ["P", "H", "P"]
$pares = str_split("ABCDEF", 2); // ["AB", "CD", "EF"]

// wordwrap — quebra string em linhas com largura máxima
$texto = "Este é um texto longo que precisa ser quebrado em múltiplas linhas para exibição.";
echo wordwrap($texto, 40, "\n", true);

Formatação

<?php
declare(strict_types=1);

// number_format — formata números para exibição
$preco    = 1234567.891;

echo number_format($preco);              // 1,234,568 — padrão americano
echo number_format($preco, 2);           // 1,234,567.89
echo number_format($preco, 2, ',', '.'); // 1.234.567,89 — padrão brasileiro

// sprintf — formata string com placeholders
$nome  = "Ana";
$nota  = 9.75;
$media = 8.423;

echo sprintf("Aluna: %s | Nota: %.2f", $nome, $nota);
// "Aluna: Ana | Nota: 9.75"

echo sprintf("Média: %.1f", $media);  // "Média: 8.4"
echo sprintf("Código: %05d", 42);     // "Código: 00042" — zero-padded
echo sprintf("Hex: %x", 255);         // "Hex: ff"
echo sprintf("Porcentagem: %.1f%%", 94.567); // "Porcentagem: 94.6%"

// printf — como sprintf mas já imprime
printf("%-15s %5.2f\n", "Produto A", 129.9);  // alinhamento
printf("%-15s %5.2f\n", "Produto B", 49.5);

// number_format vs sprintf para moeda
$valor = 1500.5;
$formatadoNF  = "R$ " . number_format($valor, 2, ',', '.');   // R$ 1.500,50
$formatadoSPF = sprintf("R$ %.2f", $valor);                    // R$ 1500.50

Datas e horas

<?php
declare(strict_types=1);

// date() — formata a data/hora atual
echo date("d/m/Y");       // 08/03/2026
echo date("H:i:s");       // 14:35:20
echo date("d/m/Y H:i");   // 08/03/2026 14:35
echo date("l, d F Y");    // Saturday, 08 March 2026
echo date("U");           // Unix timestamp atual

// Caracteres de formato mais usados
// d = dia com zero | j = dia sem zero
// m = mês com zero | n = mês sem zero
// Y = ano 4 dígitos | y = ano 2 dígitos
// H = hora 24h | h = hora 12h
// i = minutos | s = segundos
// D = dia abreviado | l = dia completo
// M = mês abreviado | F = mês completo
// N = dia da semana (1=seg, 7=dom)
// U = Unix timestamp

// mktime() — cria um timestamp a partir de componentes
$natal = mktime(0, 0, 0, 12, 25, 2026); // hora, min, seg, mês, dia, ano
echo date("d/m/Y", $natal); // 25/12/2026

// strtotime() — converte string para timestamp
$amanha    = strtotime("+1 day");
$semanaPassada = strtotime("-1 week");
$proximaMes = strtotime("+1 month");
$dataEspecifica = strtotime("2026-06-15");

echo date("d/m/Y", $amanha);         // data de amanhã
echo date("d/m/Y", $semanaPassada);  // data de uma semana atrás
echo date("d/m/Y", $dataEspecifica); // 15/06/2026

// DateTime — classe orientada a objetos (mais recomendada para projetos modernos)
$hoje   = new DateTime();
$futuro = new DateTime("+30 days");
$fixa   = new DateTime("2026-12-31");

echo $hoje->format("d/m/Y H:i");     // data e hora atual formatada
echo $fixa->format("l, d F Y");      // "Thursday, 31 December 2026"

// Diferença entre datas
$inicio = new DateTime("2026-01-01");
$fim    = new DateTime("2026-12-31");
$diff   = $inicio->diff($fim);

echo $diff->days;    // 364
echo $diff->months;  // 11

// DateTimeImmutable — versão que não modifica o objeto original
// Preferida em código funcional e quando você precisa preservar a data original
$base      = new DateTimeImmutable("2026-03-01");
$mais30    = $base->modify("+30 days"); // retorna NOVO objeto
$mais60    = $base->modify("+60 days"); // $base ainda é 01/03

echo $base->format("d/m/Y");    // 01/03/2026 — não modificado
echo $mais30->format("d/m/Y");  // 31/03/2026
echo $mais60->format("d/m/Y");  // 30/04/2026

Expressões regulares

Expressões regulares (regex) são padrões de busca que permitem encontrar, validar e extrair texto com uma precisão impossível de alcançar com funções simples de string. O PHP usa funções preg_* que implementam o padrão PCRE (Perl Compatible Regular Expressions):

<?php
declare(strict_types=1);

// preg_match — verifica se o padrão existe e captura grupos
// Retorna 1 se encontrou, 0 se não encontrou, false em erro
$email = "ana@email.com";

// Delimitadores: / no início e fim. Flags após o delimitador final.
$padrao = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';

if (preg_match($padrao, $email)) {
    echo "E-mail válido\n";
}

// Capturando grupos com parênteses
$data = "Hoje é 08/03/2026";
preg_match('/(\d{2})\/(\d{2})\/(\d{4})/', $data, $matches);

// $matches[0] = string completa correspondida
// $matches[1] = primeiro grupo capturado
// $matches[2] = segundo grupo
// $matches[3] = terceiro grupo
echo $matches[0]; // "08/03/2026"
echo $matches[1]; // "08"
echo $matches[2]; // "03"
echo $matches[3]; // "2026"

// Grupos nomeados — mais legível que índices numéricos
preg_match('/(?P<dia>\d{2})\/(?P<mes>\d{2})\/(?P<ano>\d{4})/', $data, $m);
echo $m["dia"]; // "08"
echo $m["mes"]; // "03"
echo $m["ano"]; // "2026"
<?php
declare(strict_types=1);

// preg_match_all — encontra TODAS as ocorrências
$html    = '<a href="https://php.net">PHP</a> e <a href="https://laravel.com">Laravel</a>';
$padrao  = '/<a href="([^"]+)">([^<]+)<\/a>/';

preg_match_all($padrao, $html, $matches);

// $matches[0] = array com as strings completas
// $matches[1] = array com o grupo 1 (URLs)
// $matches[2] = array com o grupo 2 (textos dos links)
print_r($matches[1]); // ["https://php.net", "https://laravel.com"]
print_r($matches[2]); // ["PHP", "Laravel"]

// preg_replace — substitui com regex
$telefone = "(11) 9 8765-4321";

// Remove tudo que não for dígito
$somenteDigitos = preg_replace('/\D/', '', $telefone);
echo $somenteDigitos; // "11987654321"

// preg_replace com referência de grupo ($1, $2...)
$data = "2026-03-08";
$dataFormatada = preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$3/$2/$1', $data);
echo $dataFormatada; // "08/03/2026"

// preg_split — divide string usando regex como separador
$texto  = "um  dois   três    quatro"; // múltiplos espaços
$partes = preg_split('/\s+/', $texto); // divide por um ou mais espaços
print_r($partes); // ["um", "dois", "três", "quatro"]

Referência rápida de metacaracteres regex

<?php
declare(strict_types=1);

// METACARACTERES ESSENCIAIS
// .   = qualquer caractere (exceto \n)
// \d  = dígito [0-9]
// \D  = não-dígito
// \w  = palavra [a-zA-Z0-9_]
// \W  = não-palavra
// \s  = espaço em branco (espaço, tab, newline)
// \S  = não-espaço

// QUANTIFICADORES
// *   = zero ou mais
// +   = um ou mais
// ?   = zero ou um (opcional)
// {n} = exatamente n vezes
// {n,} = n ou mais vezes
// {n,m} = entre n e m vezes

// ÂNCORAS
// ^   = início da string
// $   = fim da string
// \b  = limite de palavra

// CLASSES DE CARACTERE
// [abc]  = a, b ou c
// [^abc] = qualquer coisa exceto a, b ou c
// [a-z]  = qualquer letra minúscula

// FLAGS (após o delimitador)
// i = case-insensitive
// m = multiline (^ e $ por linha)
// s = . inclui \n
// u = modo Unicode

// Exemplos práticos de validação
$padroes = [
    "cep"      => '/^\d{5}-?\d{3}$/',
    "cpf"      => '/^\d{3}\.\d{3}\.\d{3}-\d{2}$/',
    "telefone" => '/^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$/',
    "url"      => '/^https?:\/\/[\w\-]+(\.[\w\-]+)+/',
    "cor_hex"  => '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/',
];

$testes = [
    "cep"      => "01310-100",
    "cpf"      => "123.456.789-09",
    "telefone" => "(11) 98765-4321",
    "url"      => "https://php.net",
    "cor_hex"  => "#FF5733",
];

foreach ($padroes as $tipo => $padrao) {
    $valido = preg_match($padrao, $testes[$tipo]) ? "válido" : "inválido";
    echo "$tipo: $valido\n";
}

Segurança com strings

<?php
declare(strict_types=1);

// htmlspecialchars — escapa caracteres HTML
// SEMPRE use ao exibir dados do usuário em HTML
$entrada = '<script>alert("XSS")</script>';
echo htmlspecialchars($entrada, ENT_QUOTES, 'UTF-8');
// &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// htmlspecialchars_decode — inverte a operação
// strip_tags — remove todas as tags HTML
$html   = '<p>Texto <strong>importante</strong></p>';
$limpo  = strip_tags($html);         // "Texto importante"
$comP   = strip_tags($html, '<p>');  // "<p>Texto importante</p>" — permite <p>

// Verificando e convertindo encoding
$utf8 = mb_convert_encoding("Ação", "UTF-8", "ISO-8859-1");

// mb_strlen e mb_substr para strings com acentos
$palavra = "Programação";
echo strlen($palavra);     // pode dar 12 em UTF-8 (bytes, não chars)
echo mb_strlen($palavra);  // 11 — caracteres reais
echo mb_substr($palavra, 0, 7); // "Program"

Boas práticas com strings

Use sempre mb_* para strings com caracteres não-ASCII. Em qualquer aplicação brasileira, mb_strlen, mb_strtolower, mb_substr são obrigatórios. As funções sem mb_ operam em bytes, não em caracteres.

Escape sempre entradas de usuário com htmlspecialchars. Nunca exiba dados de formulário ou banco de dados diretamente no HTML — isso abre vetores de ataque XSS.

Prefira sprintf a concatenações longas. sprintf("Olá, %s! Você tem %d mensagens.", $nome, $total) é muito mais legível do que "Olá, " . $nome . "! Você tem " . $total . " mensagens.".

Use DateTimeImmutable em código novo. Objetos imutáveis são mais seguros do que DateTime mutável — você não corre o risco de modificar acidentalmente uma data compartilhada.

Teste regex em ferramentas online antes de colocar no código. Sites como regex101.com permitem testar padrões com visualização em tempo real, o que economiza muito tempo de debugging.

Duas ideias deste artigo valem para todo o resto da série. A primeira é que byte não é caractere: enquanto o texto for só ASCII, as duas famílias de função parecem equivalentes, e o primeiro nome com cedilha desfaz a ilusão. A segunda é que escapar é trabalho da saída, e o escape correto depende de onde o valor vai parar — corpo do HTML, atributo, JavaScript e URL exigem tratamentos diferentes, e usar o de um no lugar do outro deixa a porta aberta com aparência de cuidado. Guardadas essas duas, o resto é consultar o manual quando precisar.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um campo aceita no máximo 20 caracteres. O usuário digita um nome com acentos e é recusado com 17 letras. Onde está o erro?

<?php
$nome = "João Gonçalves Sá";

if (strlen($nome) > 20) {
    echo "Nome longo demais";   // dispara, com 17 caracteres
}
Ver resposta

✓ Resposta: strlen conta bytes, não caracteres. Em UTF-8, letras sem acento ocupam um byte, mas "ã", "ç" e "á" ocupam dois cada — o nome do exemplo tem 17 caracteres e 20 bytes, e com mais um acento passaria do limite. A correção é mb_strlen($nome), que conta caracteres de verdade. E o problema não para em strlen: toda a família básica de funções de string trabalha em bytes. substr($nome, 0, 10) pode cortar um caractere ao meio e produzir um byte solto, que aparece como aquele losango preto com interrogação; o certo é mb_substr. strtoupper("ação") devolve "AçãO", deixando os acentuados intactos, porque não sabe o que fazer com eles; mb_strtoupper devolve "AÇÃO". str_split("ação", 2) quebra no meio dos bytes; mb_str_split (PHP 7.4+) separa por caractere. A regra que evita a família inteira de defeitos: em texto que vem do usuário, use sempre a versão mb_, e defina mb_internal_encoding('UTF-8') na inicialização do sistema. Há uma exceção deliberada: quando o que se quer é mesmo o tamanho em bytes — validar limite de coluna do banco, calcular Content-Length —, strlen é a função certa, e trocá-la por mb_strlen aí seria o erro inverso.

Exercício 2

Esta validação de nome de usuário recusa qualquer nome com acento. O padrão parece correto. O que falta?

<?php
if (preg_match('/^\w+$/', "ação")) {
    echo "válido";
} else {
    echo "inválido";   // é isto que sai
}
Ver resposta

✓ Resposta: Falta o modificador u, de Unicode: '/^\w+$/u'. Sem ele, a PCRE trabalha byte a byte e \w corresponde apenas a [A-Za-z0-9_] — os dois bytes do "ç" não se encaixam, e a correspondência falha. Com o u, o padrão passa a tratar a entrada como UTF-8, \w abrange letras acentuadas, e . passa a significar um caractere em vez de um byte. O modificador tem um efeito colateral útil: se a string não for UTF-8 válido, preg_match devolve false em vez de resultado errado — e é por isso que vale conferir o retorno com === false, já que "não casou" (0) e "deu erro" (false) são coisas diferentes que um if comum confunde. Dois cuidados fecham o assunto. Primeiro, \w com u aceita letras de qualquer alfabeto, inclusive cirílico e árabe — se a intenção era restringir a nomes latinos, o padrão precisa ser explícito, como /^[\p{Latin}\d_]+$/u. Segundo, cuidado com padrões que permitem muitos caminhos de correspondência, tipo (\s+)+$: eles causam backtracking catastrófico, em que uma entrada de algumas dezenas de caracteres trava o processo por minutos. O limite existe em pcre.backtrack_limit, e ao ser atingido preg_match devolve false silenciosamente — mais um motivo para testar o retorno.

Exercício 3

Um relatório mensal soma vendas de 31 de janeiro a 31 de janeiro do mês seguinte. Em fevereiro, o relatório inclui dias de março. Explique.

<?php
$inicio = new DateTimeImmutable('2026-01-31');
$fim    = $inicio->modify('+1 month');

echo $fim->format('d/m/Y');   // 03/03/2026
Ver resposta

✓ Resposta: "Mais um mês" soma 1 ao número do mês e mantém o dia: 31 de janeiro vira 31 de fevereiro, que não existe. O PHP então normaliza o excedente — fevereiro de 2026 tem 28 dias, sobram 3, e o resultado é 3 de março. Não é bug: é a única regra possível para uma operação ambígua, e o mesmo acontece em 31 de março, 31 de maio e em todo mês de 31 dias seguido de um mais curto. Para "o último dia do mês seguinte", diga isso explicitamente: $inicio->modify('last day of next month'). Para o início do mês seguinte, modify('first day of next month'). E para intervalos de relatório, o desenho que não erra é usar limite inferior fechado e superior aberto: em vez de "de 1º a 31 às 23:59:59", use >= '2026-02-01 00:00:00' e < '2026-03-01 00:00:00' — isso dispensa saber quantos dias o mês tem e não perde o último segundo do dia, um erro clássico com colunas DATETIME que guardam fração de segundo. Repare ainda na escolha de DateTimeImmutable no exemplo: com DateTime, o modify altera o próprio objeto e devolve ele mesmo, então $fim e $inicio seriam o mesmo objeto e a data inicial se perderia — um dos defeitos mais comuns e mais difíceis de enxergar em código com datas. Use sempre a versão imutável.

Exercício 4

O comentário de um usuário é exibido assim. Explique por que cada uma das três saídas precisa de escape diferente.

<?php
$texto = $comentario['texto'];   // vem do usuário
?>
<p><?= $texto ?></p>
<img src="avatar.png" alt="<?= $texto ?>">
<script>const c = "<?= $texto ?>";</script>
Ver resposta

✓ Resposta: As três estão vulneráveis, e a razão de precisarem de tratamentos distintos é que o escape depende do contexto em que o valor é inserido, não do valor em si. No corpo do HTML, o perigo é o texto fechar uma tag e abrir <script>; htmlspecialchars($texto, ENT_QUOTES, 'UTF-8') resolve, convertendo <, >, & e as aspas em entidades. Dentro de um atributo, o perigo é fechar a aspa do atributo e injetar outro, como " onerror="codigo — o mesmo htmlspecialchars serve, desde que com ENT_QUOTES, porque o padrão antigo escapava só as aspas duplas e deixava passar as simples; e o atributo precisa estar entre aspas no HTML, já que atributo sem aspas é vulnerável mesmo escapado. Dentro de <script>, htmlspecialchars não basta e ainda quebra o código: o navegador não decodifica entidades dentro de script, então o valor chega literalmente com &quot;. Ali o certo é json_encode($texto, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), que produz uma string JavaScript válida e escapa o </script> capaz de encerrar o bloco. Duas notas finais: strip_tags não é escape — ele remove marcação e pode transformar o texto do usuário em outra coisa, além de não proteger atributo; e o escape pertence à saída, não à entrada. Guardar o comentário já escapado no banco parece prático e cria o problema de dado escapado duas vezes, que aparece como &amp;quot; na tela.

Exercício 5

Desafio: escreva uma função que gere o slug de um título — "Programação em PHP: 10 dicas!" deve virar programacao-em-php-10-dicas. Explique cada etapa e o que acontece se a extensão intl não estiver disponível.

Ver resposta

✓ Resposta: A operação tem quatro etapas, e a ordem entre elas importa:

<?php

declare(strict_types=1);

function slug(string $titulo): string
{
    // 1. Remover os acentos, sem remover as letras.
    //    O Transliterator decompõe "ç" em "c" + cedilha e descarta a marca.
    if (class_exists(\Transliterator::class)) {
        $t = \Transliterator::create('Any-Latin; Latin-ASCII');
        $titulo = $t?->transliterate($titulo) ?? $titulo;
    } else {
        // Sem a extensão intl: iconv é a alternativa razoável.
        // O @ é necessário porque iconv emite notice em caractere intraduzível.
        $convertido = @iconv('UTF-8', 'ASCII//TRANSLIT', $titulo);
        $titulo = $convertido !== false ? $convertido : $titulo;
    }

    // 2. Minúsculas — depois de tirar acento, strtolower já basta
    $titulo = strtolower($titulo);

    // 3. Tudo que não for letra ou número vira hífen
    $titulo = preg_replace('/[^a-z0-9]+/', '-', $titulo);

    // 4. Hífens das pontas fora
    return trim($titulo, '-');
}

echo slug('Programação em PHP: 10 dicas!');   // programacao-em-php-10-dicas

Três decisões merecem explicação. A remoção de acentos vem antes de tudo: se a substituição por hífen viesse primeiro, "ç" e "ã" seriam tratados como "não é letra ASCII" e virariam hífens, transformando "ação" em a-a-o. O preg_replace usa + para colapsar sequências — ": " são dois caracteres e devem virar um hífen, não dois. E o trim no fim limpa o hífen que a exclamação final deixou.

Sobre o intl: ele não vem habilitado em toda instalação, e por isso o código testa a existência da classe em vez de assumir. O iconv com //TRANSLIT é o plano B, mas tem um defeito conhecido — o resultado depende da localidade do sistema, e em servidores com LC_ALL=C ele devolve ? no lugar dos acentuados, produzindo slugs errados que só aparecem em produção. É exatamente o tipo de diferença de ambiente que faz o código "funcionar na minha máquina". Em projeto que já usa Composer, a saída mais previsível é delegar a uma biblioteca dedicada e declarar ext-intl no require, para que a ausência apareça na instalação e não no primeiro título com acento.

Comentários

Mais em PHP

Herança, Interfaces e Traits
Herança, Interfaces e Traits

Herdar, assinar um contrato ou compor: três mecanismos que resolvem problemas…

Funções
Funções

Uma função guarda um trecho de lógica sob um nome e um contrato. Como declarar…

Exceções Avançadas
Exceções Avançadas

Uma hierarquia de exceções bem desenhada diz ao chamador o que ele pode tratar…