Até agora escrevemos código sequencial — uma instrução após a outra, em um único bloco. Isso funciona para programas simples, mas rapidamente se torna um problema: código duplicado em vários lugares, dificuldade de manutenção, impossibilidade de testar partes isoladas.
Funções resolvem isso. Uma função é um bloco de código nomeado que pode ser chamado quantas vezes precisar, de qualquer lugar do programa. É o primeiro e mais fundamental mecanismo de reutilização de código em qualquer linguagem.
Neste artigo vamos cobrir tudo sobre funções no PHP moderno: definição, parâmetros, valores padrão, tipagem, retorno, escopo de variáveis, funções anônimas e arrow functions.
Definindo e chamando funções
A palavra-chave function define uma função. O nome segue as mesmas regras das variáveis, mas sem o $, e a convenção é camelCase:
<?php
// Definindo uma função
// A definição NÃO executa o código — apenas o registra
function saudacao(): void
{
echo "Olá, mundo!\n";
}
// Chamando a função — agora o código executa
saudacao(); // Olá, mundo!
saudacao(); // Olá, mundo! — pode chamar quantas vezes quiser
// Funções podem ser definidas depois de onde são chamadas
// O PHP carrega todas as definições antes de executar
dizOi();
function dizOi(): void
{
echo "Oi!\n";
}
Parâmetros e argumentos
Parâmetros são as variáveis declaradas na definição da função. Argumentos são os valores passados na chamada. A distinção é sutil mas importante para a comunicação técnica:
<?php
// $nome e $saudacao são PARÂMETROS — variáveis locais da função
function cumprimentar(string $nome, string $saudacao): void
{
// $nome e $saudacao só existem dentro desta função
echo "$saudacao, $nome!\n";
}
// "Ana" e "Bom dia" são ARGUMENTOS — valores passados na chamada
cumprimentar("Ana", "Bom dia"); // Bom dia, Ana!
cumprimentar("Carlos", "Boa tarde"); // Boa tarde, Carlos!
Valores padrão
Parâmetros podem ter valores padrão, tornando-os opcionais na chamada. Parâmetros com padrão sempre vêm depois dos obrigatórios:
<?php
// $saudacao tem um valor padrão — é opcional na chamada
function cumprimentar(string $nome, string $saudacao = "Olá"): void
{
echo "$saudacao, $nome!\n";
}
cumprimentar("Ana"); // Olá, Ana! — usa o padrão
cumprimentar("Carlos", "Oi"); // Oi, Carlos! — substitui o padrão
// Valores padrão podem ser strings, números, arrays, null — mas não expressões dinâmicas
function criarPerfil(
string $nome,
int $idade = 0,
array $hobbies = [], // array vazio como padrão
?string $bio = null // null como padrão — ? indica que aceita null
): void {
echo "$nome, $idade anos\n";
echo "Hobbies: " . implode(", ", $hobbies) . "\n";
echo "Bio: " . ($bio ?? "Sem bio") . "\n";
}
criarPerfil("Ana", 28, ["leitura", "código"]);
criarPerfil("Bruno"); // usa todos os padrões
Passagem por referência
Assim como no foreach, você pode passar argumentos por referência usando &. A função então modifica a variável original, não uma cópia:
<?php
// Sem referência — a função recebe uma cópia
function dobrarCopia(int $valor): void
{
$valor *= 2; // modifica apenas a cópia local
}
// Com referência — a função modifica a variável original
function dobrarOriginal(int &$valor): void
{
$valor *= 2; // modifica a variável original
}
$numero = 10;
dobrarCopia($numero);
echo $numero; // 10 — não mudou
dobrarOriginal($numero);
echo $numero; // 20 — mudou
Use passagem por referência com moderação. Na maioria dos casos, é melhor retornar o valor modificado do que modificar o original — o código fica mais previsível e testável.
Retorno de valores
A instrução return encerra a execução da função e devolve um valor ao chamador. Uma função que não tem return devolve null implicitamente:
<?php
// Retornando um valor simples
function somar(int $a, int $b): int
{
return $a + $b; // encerra a função e retorna o resultado
}
$resultado = somar(3, 4);
echo $resultado; // 7
// Retorno antecipado — útil para casos de guarda (early return)
// Em vez de aninhar if/else, retorne cedo e mantenha o código "reto"
function calcularDesconto(float $preco, float $percentual): float
{
// Guarda: valida as entradas antes de continuar
if ($preco <= 0) {
return 0.0;
}
if ($percentual <= 0 || $percentual > 100) {
return $preco; // sem desconto se percentual inválido
}
return $preco - ($preco * $percentual / 100);
}
echo calcularDesconto(100.0, 20.0); // 80
echo calcularDesconto(-50.0, 20.0); // 0 — guarda ativada
echo calcularDesconto(100.0, 150.0); // 100 — guarda ativada
// Retornando arrays — muito comum no PHP
function obterDimensoes(float $largura, float $altura): array
{
return [
"largura" => $largura,
"altura" => $altura,
"area" => $largura * $altura,
];
}
$dims = obterDimensoes(5.0, 3.0);
echo $dims["area"]; // 15
Tipagem de funções
O PHP moderno permite declarar os tipos dos parâmetros e do retorno. Isso torna o código mais seguro, mais legível e permite que ferramentas como IDEs e analisadores estáticos detectem erros antes de executar:
<?php
// Tipos escalares nos parâmetros e no retorno
function dividir(float $dividendo, float $divisor): float
{
if ($divisor === 0.0) {
throw new InvalidArgumentException("Divisão por zero não é permitida.");
}
return $dividendo / $divisor;
}
// Union types — PHP 8: aceita mais de um tipo
function formatar(int|float $numero): string
{
return number_format($numero, 2, ',', '.');
}
echo formatar(1234.5); // 1.234,50
echo formatar(1000); // 1.000,00
// Nullable types — o ? indica que o valor pode ser null
function encontrarUsuario(int $id): ?array
{
$usuarios = [
1 => ["nome" => "Ana", "email" => "ana@email.com"],
2 => ["nome" => "Carlos", "email" => "carlos@email.com"],
];
// Retorna null se o usuário não existir — em vez de false ou array vazio
return $usuarios[$id] ?? null;
}
$usuario = encontrarUsuario(1);
$ausente = encontrarUsuario(99);
var_dump($usuario); // array com dados
var_dump($ausente); // NULL
// void — função não retorna nada
function registrarLog(string $mensagem): void
{
echo "[LOG] $mensagem\n";
// return; // opcional — void proíbe retornar um valor
}
// never — PHP 8.1: função que NUNCA retorna (sempre lança exceção ou encerra)
function falharComErro(string $mensagem): never
{
throw new RuntimeException($mensagem);
// Ou: exit(1);
}
Modo estrito
Por padrão, o PHP faz coerção de tipos nos parâmetros. Com declare(strict_types=1), a tipagem se torna rígida e não faz coerção — é a prática recomendada em projetos modernos:
<?php
declare(strict_types=1); // deve ser a primeira linha do arquivo
function somar(int $a, int $b): int
{
return $a + $b;
}
somar(3, 4); // OK
somar(3.5, 4); // TypeError em strict mode — float não é int
somar("3", 4); // TypeError em strict mode — string não é int
Escopo de variáveis
No PHP, variáveis definidas fora de uma função não estão disponíveis dentro dela por padrão. Isso é diferente de muitas outras linguagens e pega muitos iniciantes de surpresa:
<?php
$mensagem = "Olá do escopo global";
function exibir(): void
{
// $mensagem não existe aqui — PHP não herda escopo automaticamente
echo $mensagem; // Notice: Undefined variable $mensagem
}
exibir();
// Para usar uma variável global dentro de uma função, declare com global
// (mas evite isso — é uma prática ruim que cria acoplamento oculto)
function exibirGlobal(): void
{
global $mensagem; // agora $mensagem está acessível
echo $mensagem; // Olá do escopo global
}
// A forma correta: passe o valor como parâmetro
function exibirCorreto(string $mensagem): void
{
echo $mensagem;
}
exibirCorreto($mensagem); // limpo, explícito, testável
Variáveis estáticas
Uma variável estática dentro de uma função mantém seu valor entre chamadas — sem precisar de uma variável global:
<?php
function contarChamadas(): int
{
// static faz a variável persistir entre chamadas da função
// É inicializada apenas uma vez
static $contador = 0;
$contador++;
return $contador;
}
echo contarChamadas(); // 1
echo contarChamadas(); // 2
echo contarChamadas(); // 3
Funções anônimas (closures)
Funções anônimas não têm nome e podem ser atribuídas a variáveis, passadas como argumentos ou retornadas por outras funções. São fundamentais para programação funcional em PHP:
<?php
// Função anônima atribuída a uma variável
$dobrar = function(int $n): int {
return $n * 2;
};
echo $dobrar(5); // 10
// Passando função anônima como argumento
// array_map aplica a função a cada elemento do array
$numeros = [1, 2, 3, 4, 5];
$dobrados = array_map(function(int $n): int {
return $n * 2;
}, $numeros);
print_r($dobrados); // [2, 4, 6, 8, 10]
// use — captura variáveis do escopo externo dentro da closure
$fator = 3;
$multiplicar = function(int $n) use ($fator): int {
// $fator foi capturado do escopo externo via use
return $n * $fator;
};
echo $multiplicar(5); // 15
// use por referência — captura e permite modificar a variável externa
$total = 0;
$acumular = function(int $n) use (&$total): void {
$total += $n; // modifica o $total externo
};
$acumular(10);
$acumular(20);
echo $total; // 30
Arrow functions
Introduzidas no PHP 7.4, as arrow functions são uma sintaxe compacta para funções anônimas de uma única expressão. Elas capturam variáveis do escopo externo automaticamente — sem precisar de use:
<?php
// Função anônima tradicional
$dobrar = function(int $n): int {
return $n * 2;
};
// Arrow function equivalente — muito mais concisa
$dobrar = fn(int $n): int => $n * 2;
echo $dobrar(5); // 10
// Arrow function captura o escopo externo automaticamente
$fator = 3;
$multiplicar = fn(int $n): int => $n * $fator; // $fator capturado automaticamente
echo $multiplicar(5); // 15
// Uso prático com array_map e array_filter
$numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filtra apenas os pares
$pares = array_filter($numeros, fn(int $n): bool => $n % 2 === 0);
// Dobra cada valor
$dobrados = array_map(fn(int $n): int => $n * 2, $numeros);
// Encadeando — filtra pares e dobra
$paresDobrados = array_map(
fn(int $n): int => $n * 2,
array_filter($numeros, fn(int $n): bool => $n % 2 === 0)
);
print_r($paresDobrados); // [4, 8, 12, 16, 20]
Boas práticas em funções
Uma função deve fazer uma coisa só. Se você precisar de "e" para descrever o que a função faz, ela provavelmente deveria ser dividida em duas.
Nomeie funções com verbos. calcularTotal(), buscarUsuario(), validarEmail() — o nome deve descrever a ação, não o dado.
Prefira early return ao invés de if/else aninhados. O código fica mais linear e fácil de ler.
Limite os parâmetros. Funções com mais de três ou quatro parâmetros geralmente indicam que a função faz coisas demais ou que os parâmetros deveriam ser agrupados em um array ou objeto.
Use declare(strict_types=1) em todos os seus arquivos PHP. Isso transforma erros de tipo em exceções explícitas, em vez de conversões silenciosas que geram bugs difíceis de encontrar.
<?php
declare(strict_types=1);
// ✗ Função que faz coisas demais
function processarPedido(array $itens, string $email, string $endereco, float $frete): void
{
// calcula total, envia email, salva no banco, gera nota fiscal...
}
// ✓ Responsabilidades separadas
function calcularTotalPedido(array $itens, float $frete): float
{
$subtotal = array_sum(array_column($itens, "preco"));
return $subtotal + $frete;
}
function enviarConfirmacao(string $email, float $total): void
{
// envia e-mail de confirmação
}
Uma função bem escrita é a menor unidade de confiança de um sistema: quem a chama não precisa ler o corpo dela, basta a assinatura. É por isso que tipar parâmetro e retorno rende tanto — não pelo que impede em tempo de execução, mas pelo que comunica a quem vai usar. O mesmo cuidado vale para o nome dos parâmetros, que desde o PHP 8 fazem parte do contrato, e para o que a função carrega de fora: quanto menos ela depender de estado que não recebeu, menos surpresas ela guarda para quando o sistema crescer.
Fontes e leituras recomendadas
-
Funções — Manual oficial do PHP https://www.php.net/manual/pt_BR/language.functions.php Documentação completa sobre funções, incluindo funções variádicas, named arguments e todos os detalhes de comportamento.
-
Funções anônimas (Closures) — Manual oficial do PHP https://www.php.net/manual/pt_BR/functions.anonymous.php Documentação específica sobre closures e o uso de
usepara captura de escopo. -
Arrow functions — Manual oficial do PHP https://www.php.net/manual/pt_BR/functions.arrow.php Documentação das arrow functions introduzidas no PHP 7.4, com exemplos e comparação com closures.
-
RFC: Arrow functions 2.0 https://wiki.php.net/rfc/arrow_functions_v2 A proposta formal das arrow functions. Explica a motivação, o design e as limitações (apenas uma expressão, sem corpo de bloco).
-
declare(strict_types) — Manual oficial do PHP https://www.php.net/manual/pt_BR/language.types.declarations.php#language.types.declarations.strict Como e por que usar o modo estrito de tipos. Fundamental para projetos PHP modernos.
-
MARTIN, Robert C. Clean Code. Prentice Hall, 2008. Capítulo 3: Functions. https://www.oreilly.com/library/view/clean-code-a/9780136083238/ O capítulo mais importante do livro. Trata de tamanho de funções, nomes, parâmetros e o princípio da responsabilidade única aplicado a funções.
Exercícios
Exercício 1
O desconto deveria ser de 20%, que é o valor de $percentual no momento da chamada. Sai 10%. Explique — e diga o que mudaria se fosse uma arrow function.
<?php
$percentual = 10;
$aplicar = function (float $preco) use ($percentual): float {
return $preco - ($preco * $percentual / 100);
};
$percentual = 20;
echo $aplicar(100); // 90, não 80
Ver resposta
✓ Resposta: O use captura por valor, no momento em que a closure é criada, não no momento em que ela é chamada. Quando a função foi definida, $percentual valia 10, e foi esse 10 que ficou guardado dentro dela — a atribuição seguinte muda a variável de fora, não a cópia de dentro. Para capturar por referência, e enxergar mudanças posteriores, escreve-se use (&$percentual); aí a saída passa a ser 80. Mas pense duas vezes antes de fazer isso: uma closure que depende do valor atual de uma variável externa é difícil de testar e de raciocinar, porque o resultado muda conforme quando ela é chamada. Quase sempre é melhor passar o percentual como parâmetro. Com arrow function o comportamento é o mesmo — fn(float $preco) => $preco - ($preco * $percentual / 100) também congela o valor 10. A diferença é só de sintaxe: a arrow function captura automaticamente as variáveis usadas no corpo, sem precisar declarar use, e captura sempre por valor — não existe forma de capturar por referência nela. Em compensação, ela é limitada a uma única expressão, e por isso serve bem como argumento de array_map e usort, onde o corpo é curto.
Exercício 2
Esta função gera códigos sequenciais. Em que situação ela passa a gerar códigos repetidos — e em que situação ela passa a não reiniciar quando deveria?
<?php
function proximoCodigo(): int {
static $n = 0;
return ++$n;
}
Ver resposta
✓ Resposta: As duas situações são o mesmo fato visto de dois ângulos: static guarda o valor enquanto o processo viver, e o tempo de vida do processo muda conforme onde o código roda. No modelo clássico do PHP, cada requisição HTTP é um processo novo: a variável estática volta a zero a cada requisição, e duas pessoas acessando ao mesmo tempo recebem os mesmos "códigos sequenciais" — códigos repetidos, e o defeito só aparece com movimento. Já num processo de longa duração — worker de fila, script de linha de comando, aplicação sob Swoole ou RoadRunner — o processo atende milhares de requisições sem morrer, e a variável nunca reinicia: o contador continua de onde parou entre um job e outro, vazando estado de um usuário para o seguinte. É o mesmo mecanismo produzindo problemas opostos, e é por isso que static dentro de função é uma das construções que menos envelhecem bem. Para sequência de verdade, a fonte tem de ser compartilhada e atômica: AUTO_INCREMENT do banco, INCR do Redis, ou um uuid se a ordem não importar. static tem uso legítimo — memorizar um cálculo caro dentro da mesma requisição, por exemplo —, mas nunca para gerar identidade, e sempre com a pergunta: o que acontece se este processo viver mil vezes mais do que eu imagino?
Exercício 3
Uma função da sua biblioteca é usada por outros times. Você renomeia um parâmetro de $qtd para $quantidade, sem mudar mais nada. Por que isso pode quebrar o código de quem chama?
Ver resposta
✓ Resposta: Porque desde o PHP 8 existem argumentos nomeados, e eles transformam o nome do parâmetro em parte da interface pública da função. Quem chamava calcularFrete(peso: 2.0, qtd: 3) passa a receber Error: Unknown named parameter $qtd — um erro em tempo de execução, no código de outra pessoa, causado por uma alteração que parecia puramente cosmética. Antes do PHP 8 o nome do parâmetro era detalhe interno, e renomear era seguro; hoje não é. As consequências práticas são três. Ao publicar uma função ou um método público, escolha o nome do parâmetro com o mesmo cuidado com que escolhe o nome da função, porque mudá-lo depois é quebra de compatibilidade e merece entrar no changelog como tal. Ao consumir biblioteca de terceiros, saiba que usar argumento nomeado cria um acoplamento a mais — é ótimo para legibilidade em chamadas com muitos parâmetros opcionais, e é um risco em dependência que muda rápido. E ao trabalhar com ...$args repassados adiante, lembre que ... preserva as chaves de string como nomes de parâmetro, então um array associativo desempacotado também depende desses nomes.
Exercício 4
Com declare(strict_types=1) no topo, o que acontece em cada chamada? E sem essa linha?
<?php
declare(strict_types=1);
function repetir(string $texto, int $vezes): string {
return str_repeat($texto, $vezes);
}
repetir("ok", 3);
repetir("ok", "3");
repetir(42, 3);
Ver resposta
✓ Resposta: Com strict_types=1, só a primeira chamada passa. A segunda lança TypeError, porque a string "3" não é um int — e o modo estrito não converte nada, mesmo quando a conversão seria óbvia. A terceira também lança TypeError, pelo mesmo motivo invertido. Sem a linha, o PHP opera em modo coercitivo, que é o padrão: a segunda chamada converte "3" em 3 e funciona, e a terceira converte 42 em "42" e também funciona, ambas em silêncio. A única exceção que o modo coercitivo não engole é string não numérica onde se espera número, que desde o PHP 8 lança TypeError de qualquer forma. Duas sutilezas costumam pegar quem está começando. A primeira: declare(strict_types=1) vale para o arquivo que faz a chamada, não para o que declara a função — se a sua biblioteca tem a linha e o consumidor não, as chamadas dele continuam coercitivas. A segunda: a declaração precisa ser a primeiríssima instrução do arquivo, antes de qualquer namespace ou use; fora dessa posição é erro fatal. A recomendação da comunidade é ligá-la em todo arquivo novo, e o motivo é o do exercício sobre tipos: no modo coercitivo, um dado malformado atravessa três camadas antes de virar problema, e aí ninguém mais sabe de onde veio.
Exercício 5
Desafio: escreva a assinatura de uma função que busca um usuário por e-mail e pode não encontrar nada. Compare as três formas de sinalizar "não achei" e defenda uma.
Ver resposta
✓ Resposta: As três formas são retornar null, retornar false e lançar exceção — e a escolha depende de uma pergunta só: não encontrar é esperado ou é excepcional?
<?php
declare(strict_types=1);
// 1. Busca: não achar é resultado normal → tipo nulável
function buscarPorEmail(string $email): ?Usuario
{
// ...
return null;
}
// 2. Exigência: não achar é quebra de contrato → exceção
function exigirPorEmail(string $email): Usuario
{
return buscarPorEmail($email)
?? throw new UsuarioNaoEncontrado($email); // PHP 8: throw é expressão
}
A forma que eu defendo é a combinação das duas, com nomes que anunciam o comportamento. ?Usuario é honesto quando a ausência é um resultado legítimo — numa tela de busca, não achar é uma resposta, não um erro —, e o tipo nulável obriga quem chama a tratar o caso, porque chamar $u->nome num null lança Error imediatamente. Já no fluxo de login, ou ao carregar o dono de um pedido que precisa existir, a ausência indica que alguma coisa está errada mais atrás: aí a exceção é melhor, porque carrega contexto (qual e-mail, em que operação) e não deixa o chamador seguir com um valor vazio por descuido.
Retornar false é a opção a evitar, apesar de comum na biblioteca nativa do PHP, que é cheia de funções assim por razões históricas. O motivo: false mistura "não achei" com "deu errado", força o chamador a lembrar de comparar com === (porque if (!$u) também é verdadeiro para array vazio e para 0), e o tipo de retorno vira Usuario|false, que descreve mal a intenção. Uma quarta opção aparece em bases maiores: devolver um objeto Result ou um Option, que torna o "pode não haver" explícito no tipo — vale quando o padrão se repete por todo o sistema, e é exagero quando não.