PHP 8.5 adiciona operador pipe nativo, mas array_map ainda exige ajuste manual
O PHP 8.5, disponível desde novembro de 2025, trouxe o operador |> para encadear transformações de dados. Nem toda função do array_* se adapta direto: o pipe só passa um argumento, e isso muda como você refatora pipelines legados.

PHP 8.5 adiciona operador pipe nativo, mas array_map ainda exige ajuste manual
Nota da redação: a fonte consultada para este artigo (o manual oficial do PHP sobre as novidades da versão 8.5) não traz a data de lançamento da versão. A afirmação de que o PHP 8.5 está disponível desde novembro de 2025 não pôde ser confirmada com essa fonte e deve ser lida com essa ressalva.
O PHP↳PHP44 conteúdosPadronizando seu código com PHP CS FixerDev (Back & Front) · mai 2019Docker de imagem. Para rodar projetos PHP em menos de 30 segundosDev (Back & Front) · jan 2026PHP 7.3: conheça as novidades desta versãoDev (Back & Front) · abr 2019Ver tudo em Dev (Back & Front) → 8.5, disponível desde novembro de 2025, trouxe o operador |> para encadear transformações de dados. Nem toda função do array_* se adapta direto: o pipe só passa um argumento, e isso muda como você refatora pipelines legados.
O PHP 8.5, disponível desde novembro de 2025, trouxe o operador |> para encadear transformações de dados. Nem toda função do array_* se adapta direto: o pipe só passa um argumento, e isso muda como você refatora pipelines legados.
O PHP 8.5 trouxe o operador |> para encadear transformações de dados. Nem toda função do array_* se adapta direto: o pipe só passa um argumento, e isso muda como você refatora pipelines legados.
O pipeline que todo monólito PHP acumula
Se você mantém um monolito PHP com uns anos de estrada, já esbarrou nesse padrão: uma função que recebe uma lista, normaliza, filtra e transforma de novo, tudo encadeado em chamadas aninhadas de array_map() e array_filter(). Funciona, mas lê-se de dentro para fora, e cada nível de indentação some um pouco mais da intenção original.
function getActiveUserEmails(array $users): array
{
return array_map(
fn ($user) => strtolower($user['email']),
array_filter(
array_map('normalizeUser', $users),
fn ($user) => $user['active'] === true
)
);
}Para entender essa função você precisa ler de dentro para fora: primeiro array_map('normalizeUser', ...), depois o array_filter, só então o array_map externo. É o tipo de código que todo time refatora em algum momento, geralmente para variáveis intermediárias nomeadas. O PHP 8.5 oferece uma terceira via: o operador pipe (|>).

Como o operador pipe funciona de verdade
O manual do PHP resume o operador com um exemplo mínimo:
$result = "Hello World" |> strlen(...);
print $result . PHP_EOL; // Prints "11"$valor |> callable(...) é açúcar sintático para callable($valor). O lado esquerdo vira o argumento, e só o argumento, do lado direito. Essa regra é o ponto inteiro deste artigo: o pipe é estritamente unário. Ele não sabe nada sobre a assinatura da função que você está chamando, só empurra um valor para dentro dela.
Isso funciona bem para funções que esperam exatamente um argumento obrigatório: trim(...), strlen(...), json_decode(...) (os demais parâmetros são opcionais), ou um método de instância que só precisa do $this implícito mais um valor. Para tudo que exige dois ou mais argumentos, como a maioria das funções da família array_*, o pipe sozinho não resolve.
Refatorando o pipeline, passo a passo
O primeiro passo, antes de pensar em |>, é simplesmente nomear cada etapa com uma variável. Isso já melhora a legibilidade sem mexer em sintaxe nova:
function getActiveUserEmails(array $users): array
{
$normalized = array_map('normalizeUser', $users);
$active = array_filter($normalized, fn ($user) => $user['active'] === true);
$emails = array_map(fn ($user) => strtolower($user['email']), $active);
return $emails;
}Agora a versão com pipe. Repare que cada etapa continua recebendo uma closure que aceita um único parâmetro, o array vindo da etapa anterior, e é essa closure, não array_map() ou array_filter() diretamente, que entra no pipe:
function getActiveUserEmails(array $users): array
{
return $users
|> fn (array $u) => array_map(normalizeUser(...), $u)
|> fn (array $u) => array_filter($u, fn ($user) => $user['active'] === true)
|> fn (array $u) => array_map(fn ($user) => strtolower($user['email']), $u);
}O ganho aqui não é mágico: trocamos indentação por uma leitura de cima para baixo, na ordem em que as transformações de fato acontecem. Para quem lê o código pela primeira vez, isso já ajuda bastante, principalmente em pipelines com quatro ou cinco etapas, comuns em rotinas de importação e ETL dentro de monolitos.
Onde o pipe não serve
A tentação, ao ver o exemplo acima, é tentar encurtar ainda mais e piar direto para dentro de array_map(...):
// Não faça isso: array_map() espera o callback como primeiro
// argumento, não o array. O pipe entrega $users nessa posição.
$result = $users |> array_map(...); // TypeError em tempo de execuçãoO problema é a ordem dos parâmetros. array_filter(array $array, ?callable $callback = null) recebe o array primeiro, então em casos simples (sem callback customizado) até dá para piar direto nela. Já array_map(?callable $callback, array $array, array ...$arrays) recebe o callback primeiro: o valor que o pipe entrega cai na posição errada. Por isso as closures de empacotamento nos exemplos acima não são estilo, são necessárias.
O mesmo limite aparece com métodos de instância que precisam de contexto além do valor que está sendo transformado:
final class PriceFormatter
{
public function normalize(float $amount): float { /* ... */ }
public function convert(float $amount, string $targetCurrency): float { /* ... */ }
}
$formatter = new PriceFormatter();
// Funciona: normalize() só espera o valor vindo do pipe
$normalized = $rawAmount |> $formatter->normalize(...);
// Não funciona: convert() espera um segundo argumento
$converted = $rawAmount |> $formatter->convert(...); // falta $targetCurrency
// Jeito certo: fechar o segundo argumento numa closure
$converted = $rawAmount |> fn (float $amount) => $formatter->convert($amount, 'BRL');O PHP não tem aplicação parcial nativa, então qualquer função, método estático ou método de instância que precise de mais de um argumento vai exigir uma closure de empacotamento no meio do pipe. Em pipelines com muitos parâmetros de configuração (moeda, locale, fuso horário), isso pode acabar anulando o ganho de legibilidade, porque você volta a ter uma closure por etapa, só que agora com |> entre elas em vez de parênteses aninhados.
Ajustando CI e lint para a nova sintaxe
Antes de espalhar |> pelo monolito, dois pontos do pipeline de build precisam de atenção. O primeiro é a versão do PHP usada em CI:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
tools: phpstan, php-cs-fixer
- run: composer install --no-interaction
- run: vendor/bin/phpstan analyse
- run: vendor/bin/php-cs-fixer fix --dry-runE o composer.json precisa declarar o requisito mínimo, para que o Composer rejeite instalação em ambientes mais antigos:
{
"require": {
"php": "^8.5"
}
}O segundo ponto é menos óbvio: analisadores estáticos e formatadores (PHPStan, Psalm, PHP-CS-Fixer, PHP_CodeSniffer) só entendem |> se o parser interno deles já suportar PHP 8.5. Antes de habilitar a sintaxe em qualquer repositório compartilhado, confirme a versão instalada dessas ferramentas no seu pipeline. Se o parser estiver desatualizado, o lint não vai apontar um problema de estilo, vai simplesmente falhar com um erro de token desconhecido, travando o build inteiro por um motivo que ninguém no time vai entender de cara.
Quando vale a pena migrar, e quando não
O pipe operator ajuda em pipelines lineares de transformação, do tipo dado bruto entra, série de passos, resultado sai. Em monolitos com rotinas de ETL, normalização de payload de API ou formatação de relatório, é exatamente esse formato.
Há situações em que eu não migraria:
- Bibliotecas compartilhadas entre serviços com versões diferentes de PHP. Se um pacote interno ainda precisa rodar em serviços presos ao PHP 8.2 ou 8.3, introduzir
|>quebra o parsing completo do arquivo nessas versões, não é um recurso que falha graciosamente. - Pipelines com muita injeção de contexto por etapa. Se cada passo precisa de dois ou três argumentos extras, as closures de empacotamento tiram boa parte do ganho visual do operador, e o
array_map/array_filteraninhado tradicional pode continuar mais direto de debugar. - Times que ainda depuram muito com breakpoint linha a linha. Uma cadeia longa de
|>concentra várias chamadas numa única expressão, o que dificulta colocar um breakpoint no meio do caminho sem quebrar a expressão em variáveis de novo.
Fora desses casos, vale trocar aninhamento por pipe nas rotinas novas e, com calma, nas mais lidas do legado. Assim como em qualquer troca de sintaxe num monolito, a regra prática é: refatore onde a leitura ganha de verdade, não onde o código só fica mais curto.
Este artigo foi escrito por Bisneto Braga, colunista de back-end. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.
Spring Boot 4.0: o que quebra na auto-configuração e no Jackson antes de migrar sua API
As release notes oficiais, atualizadas em setembro de 2026, confirmam Jackson 3.0 como padrão e renomeiam beans e propriedades usados por quem configura observabilidade e persistência. Nada disso aparece como erro óbvio: aparece como JSON ou métrica que silenciosamente parou de bater.














