Funções

Funções

Uma função guarda um trecho de lógica sob um nome e um contrato. Como declarar parâmetros e valores padrão, o que muda ao tipar a assinatura e ligar o modo estrito, por que o escopo isola cada chamada, e o que closures e arrow functions carregam consigo quando são criadas.
PHP

19 min de leitura

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

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.

Comentários

Mais em PHP

Tratamento de Erros e Exceções
Tratamento de Erros e Exceções

O PHP tem dois sistemas para lidar com o que dá errado: os níveis de erro…

O que é PHP e por que ele ainda importa
O que é PHP e por que ele ainda importa

PHP roda no servidor, e é essa única característica que explica quase tudo…

Traits Avançados
Traits Avançados

Trait resolve o que herança não alcança, e traz regras próprias: quem vence…