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

PSR e Padrões de Código
PSR e Padrões de Código

Por que as recomendações do PHP-FIG existem e o que muda no seu código ao…

ORM com Eloquent
ORM com Eloquent

Relacionamento em uma linha, cast automático de tipos, escopo que se encadeia…

Funções
Funções

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