PHP para Iniciantes: Funções - Argumentos

Também conhecidos como parâmetros, argumentos são os dados de entrada de uma função. Você pode injetar nela qualquer coisa que quiser, mas sugiro evitar injetar demais... Use somente o necessário, pois "com grandes poderes vem grandes responsabilidades".

Heróis dançando

Se você é, de fato, iniciante, pode achar que apenas saber isso basta para começar a escrever suas funções com um grande número de argumentos. Eu mesmo já fiz isso no passado e não me orgulho disso. Cheguei a atuar num projeto onde um método tinha mais de quarenta argumentos, na versão 5.6 do PHP, onde não existia Named Arguments (veremos mais abaixo) e você tinha de escrever todos os dados na mesmíssima ordem, mesmo se todos fossem opcionais e você só queria preencher o último. Que fatal né? Isso só aconteceu porque no projeto em questão não tinha nenhuma classe para capturar e/ou sanitizar os dados de entrada de requisições... Mas isso não vem ao caso.

De todo modo, uma forma segura de deixar seu código o menos confundível possível é seguir as dicas abaixo:

  1. Jamais permita que sua função tenha mais de três argumentos, salvo raríssimas exceções, quando realmente não tem jeito. Mas olha, é raro mesmo hein? Geralmente sempre tem um jeito e apenas não estamos enxergando-o com clareza;
  2. [PHP7+] Determine de forma rígida o tipo de dados primitivos de cada argumento da função, assim, quem for chamá-la não precisa ler sua função inteira para saber o que você vai fazer com aquilo para descobrir qual o tipo de dado é preciso colocar ali, se tem alguma limitação e tal;
  3. Dê nomes claros e objetivos para cada argumento, de forma que seja possível identificar que informação é necessária para funcionar.

Para começar, isso basta. Com essas três dicas, suas funções serão impecáveis. É comum encontrar por aí funções mais ou menos assim:

<?php

function getPost($post) {
    // vamos fingir que alguma coisa acontece aqui
    // não estou colocando pra você realmente não conseguir saber o que é "sem ler"
}

Então, baseando-se somente na assinatura da função, do que ela precisa pra funcionar?

  • a) uma instância de uma classe Post;
  • b) um array $post com os atributos de uma busca;
  • c) uma identificação de um Post, que gerará uma instância.

Pense um pouco nas opções antes de ir para a próxima linha. Não tenha medo de errar.

. . .

Se você marcou a letra a... Parabéns, você errou! Aliás, pode ser que tenha acertado, afinal, nada está implementado de verdade. Logo, todas as três opções são possíveis a depender da loucura do desenvolvedor que implementar, HEHEHE.

Mas observando logicamente, cada opção tem algo que a faz perder sentido, por exemplo:

  • a) por que a entrada de uma função chamada getPost seria uma instância? se você já possui o objeto, provavelmente não precisa realizar um get, sabe? então desse ponto de vista, talvez não faça sentido ser isso mesmo;
  • b) em buscas, geralmente, há a possibilidade de retornar mais de um elemento. Essa função se chama getPost, no singular, portanto, não deve se tratar de uma busca... Mas nada impede de termos um argumento em array passando os dados de uma busca que retorna o primeiro elemento encontrado, claro... Então não é tão impossível assim;
  • c) essa é a que mais faz sentido, na minha opinião. Se você quer fazer um getPost, você precisa passar uma identificação única para receber os dados completos.

Caso a resposta certa seja a letra c, então temos um claro erro de nomenclatura de argumentos. Certamente haveria um campo que descreveria melhor essa identificação única. É normal chamar isso de postId. Se fosse o caso, a função seria escrita dessa forma:

<?php

function getPost($postId) {
    // o que será que rola aqui?
}

Não restaria dúvidas, certo? E para melhorar ainda mais, você poderia definir o tipo de dado da entrada. Aqui requer um pouco de conhecimento de banco de dados para entender sobre chave primária auto incrementável ou UUID... Se não souber, imagina que cada identidade é um número que cresce automaticamente a cada registro (caso um) ou um texto único gerado automaticamente (caso dois).

Se for o primeiro, o tipo de dado esperado é int. No segundo, é string. Vê que mesmo para identificações não há uma exatidão de definição? Por isso é importante restringir as possibilidades do que podem inserir no contexto da sua função. Assim, o desenvolvedor que for dar manutenção no projeto vai conseguir tanto aprender a consumir a função mais rápido como impulsionar sua curva de aprendizado.

Uau, Kiko, que legal! E sobre os valores padrões?

Ah é, tem isso também. Você pode determinar valores padrões para funções que dão possibilidades opcionais. Por exemplo, pensando numa coisa bem rápida aqui... Você sabia que tem pessoas pelo mundo que não tem sobrenome? Se você forçar o cadastro disso como obrigatório, elas vão ter de colocar um texto completamente aleatório no seu registro só para poder acessar o seu sistema.

Não obstante disso, tem pessoas cujo primeiro nome tem apenas duas letras. Você já deve ter visto por aí sistemas que exigem um nome com pelo menos três letras, não é? Pois é: você acabou de ferrar a acessibilidade do sistema com isso. Porém isso não vem ao caso [2].

Então, se você tem uma função imprimirNome que espera nome e sobrenome, você deve permitir que o sobrenome seja opcional. Para isso, você deverá determinar um valor padrão. É simples:

<?php

function imprimirNome(string $nome, string $sobrenome = null) {
    echo "$nome $sobrenome" . PHP_EOL;
}

imprimirNome('Kaique', 'Garcia'); // Kaique Garcia
imprimirNome('Oh'); // Oh

Perceba que você pode substituir o null por qualquer outra coisa e o PHP irá usar como valor se nada for preenchido.

<?php

function imprimirNome(string $nome, string $sobrenome = 'Goddess') {
    echo "$nome $sobrenome" . PHP_EOL;
}

imprimirNome('Kaique', 'Garcia'); // Kaique Garcia
imprimirNome('Oh'); // Oh Goddess

E saiba que os valores padrões não precisam ficar por último, porém é recomendado que sempre deixe, caso contrário a pessoa que for invocar a função precisará preencher todos os campos para chegar até a última posição. Por exemplo:

<?php

function imprimirNome(string $sobrenome = null, string $nome) {
    echo "$nome $sobrenome" . PHP_EOL;
}

imprimirNome('Garcia', 'Kaique');
imprimirNome(null, 'Oh'); // de que adianta um valor padrão se eu preciso preencher?

Outra observação importante, é que a notação que usei propositalmente nos exemplos é conflitante. Eu digo que o tipo de dado esperado é string, mas defino como padrão null. Isso é grosseiro, sabia?

Mesmo sendo permitido na interpretação não-rígida, o certo é informar que aquele tipo pode ser anulável. Há duas formas de fazer isso:

  1. PHP 8+: concatenando o tipo null na declaração, ficandostring|null $sobrenome = null;
  2. PHP 7+: inserindo uma interrogação na frente do tipo, ficando ?string $sobrenome = null.

Você pode ler a sintaxe com interrogação como opcionalmente string. Essa simbologia vem de expressões regulares. Se nunca ouviu falar, pesquise, mas você pode deixar pra fazer isso depois, porque não tem muito a ver com PHP, especificamente.

Voltando ao tópico, eu curto muito a sintaxe do PHP 8, então gosto de deixar a assinatura assim:

<?php

function imprimirNome(string $nome, string|null $sobrenome = null) {
    echo "$nome $sobrenome" . PHP_EOL;
}


imprimirNome('Oh');

E falando em PHP 8, uma das gigantescas novidades que veio nessa versão é o tópico que iremos adentrar agora...

Named Arguments

ou Argumentos Nomeados

O que é isso, Kiko? É de comer?

Para quem escreve a função, isso não é nada. O poder dessa funcionalidade só vem à tona quando você consome uma função complexa que tem campos ocultos fora de ordem. Por exemplo, o caso onde o $sobrenome foi colocado como primeiro argumento, poderia ser resolvido com Named Arguments:

<?php

function imprimirNome(string|null $sobrenome = null, string $nome) {
    echo "$nome $sobrenome" . PHP_EOL;
}

imprimirNome(nome: 'Oh');

Quando usamos Named Arguments, nós determinamos na chamada da função qual argumento estamos preenchendo. Assim, deixamos claro a que entrada aquele dado se refere. Um detalhe sobre isso é que os argumentos que você preencher NÃO precisam estar na ordem que aparecem, mas é recomendável que as mantenha sempre na ordem. O ganho real é apenas sobre ignorar completamente os argumentos opcionais!

Enfim, sobre sintaxe na declaração de argumentos, acredito que tenha sido fácil de sacar:

  1. primeiro vem os type hint ou tipo(s) do argumento;
  2. depois um espaço e o nome dele com o operador de variável ($). O argumento acaba virando uma variável interna da função, por isso usamos esse operador;
  3. e, se houver mais argumentos, coloca uma vírgula e volta para o passo 1 no próximo argumento.

E no PHP 8 é possível colocar uma vírgula depois do último argumento.

E por que eu faria isso, Kiko?

Esse é um comportamento padrão para quem trabalha com versionamento de código. Se a sua função tem a mínima possibilidade de receber novos argumentos em manutenções posteriores, faz sentido encerrar com vírgula para que o gerenciador de versão não conte aquela linha como alteração depois. Por exemplo:

function imprimirNome(
    string $nome,
    string|null $sobrenome = null
) {
// ...

Se em uma manutenção eu adicionar o campo $apelido:

function imprimirNome(
    string $nome,
    string|null $sobrenome = null,
    string|null $apelido = null
) {
// ...

A vírgula que surgiu depois de $sobrenome = null contará como alteração e aquela linha ficará marcada. Ora, por que não deixar uma vírgula já no último caso?

function imprimirNome(
    string $nome,
    string|null $sobrenome = null,
    string|null $apelido = null,
) {
// ...

Spread Operator

Preciso te contar que o PHP também conta com um operador incrível que nem foi citado na seção de operadores, o spread operator, que traduz-se como operador de propagação. Ele poderia ser encaixado como um operador de atribuição mas sua função é somente propagar, então não.

Acredito que não pensamos num artigo exclusivo para ele porque, na prática, associamos ele ao uso com argumentos.

Tá, mas pra que serve esse operador, Kiko?

O spread operator existe em diversas linguagens. Sua notação mais comum são três pontos (...) e serve para empacotar ou desempacotar listas. Assim sendo, você pode criar uma função que recebe quantos argumentos o desenvolvedor quiser passar, por exemplo, a função sortearNomes():

<?php

function sortearNomes(...$nomes) {
    $randomIndex = array_rand($nomes);
    return $nomes[$randomIndex];
}

$sorteio = sortearNomes('Kaique', 'Roberto', 'Fernando', 'Thiago', 'Luan', 'Rafael', 'Raabe', 'Lisa', 'Robson', 'Leo', 'Helena', 'Camila', 'Amanda', 'Jéssica', 'Daniel', 'Bruno', 'Diego', 'Beatriz', 'Su', 'Leandro');
var_dump($sorteio); // resulta numa string das opções passadas

No exemplo acima, o spread operator captura toda a propagação de argumentos e empacota em um único array $nomes. Note que é possível determinar o tipo de dados aceito nas propagações, fazendo a notação string ...$nomes, exigindo que todos os campos sejam string.

Tratando-se de propagação, o spread operator captura somente os dados a partir daquele ponto, o que sugere que você pode criar outras variáveis antes, como:

<?php

function sortearNomes(int $numSorteios, string ...$nomes) {
    $randomIndexes = array_rand($nomes, $numSorteios);
    if ($numSorteios === 1) {
        return array($nomes[$randomIndexes]);
    }
    $resultado = [];
    foreach($randomIndexes as $randomIndex) {
        $resultado[] = $nomes[$randomIndex];
    }
    return $resultado;
}

$sorteio = sortearNomes(5, 'Kaique', 'Roberto', 'Fernando', 'Thiago', 'Luan', 'Rafael', 'Raabe', 'Lisa', 'Robson', 'Leo', 'Helena', 'Camila', 'Amanda', 'Jéssica', 'Daniel', 'Bruno', 'Diego', 'Beatriz', 'Su', 'Leandro');
var_dump($sorteio); // resulta num array com 5 strings das opções passadas

Agora você pode definir a quantidade de nomes a sortear, então faz sentido sempre retornar um array com os nomes vencedores, certo?

Outro detalhe é que o spread operator também pode ser aplicado para desempacotar um array. Dá uma olhada:

<?php // index.php
// abre o terminal na pasta onde criar esse arquivo
// usa o comando: php -S localhost:888
// depois abre o navegador e faz testes
// 2 nomes: http://localhost:888/?qtd=2&nomes[]=Kaique&nomes[]=Roberto&nomes[]=Fernando&nomes[]=Thiago
// 1 nome: http://localhost:888/?qtd=1&nomes[]=Kaique&nomes[]=Roberto&nomes[]=Fernando&nomes[]=Thiago

$nomes = $_GET['nomes'] ?? []; // array de entrada
$numSorteios = intval($_GET['qtd'] ?? 1); // quantidade de nomes a sortear

if (empty($nomes)) {
    exit(PHP_EOL . "Informe algum nome");
}
if (!is_array($nomes)) {
    exit(PHP_EOL . "O campo 'nomes' precisa ser um array");
}
if ($numSorteios < 1) {
    exit(PHP_EOL . "Informe um número maior que zero.");
}

header('Content-type: application/json; charset=utf-8');
echo json_encode(sortearNomes($numSorteios, ...$nomes));

function sortearNomes(int $numSorteios, string ...$nomes) {
    $randomIndexes = array_rand($nomes, $numSorteios);
    if ($numSorteios === 1) {
        return array($nomes[$randomIndexes]);
    }
    $resultado = [];
    foreach($randomIndexes as $randomIndex) {
        $resultado[] = $nomes[$randomIndex];
    }
    return $resultado;
}

No exemplo acima, temos o spread operator tanto na definição da função, empacotando a propagação de dados no array $nomes, quanto na chamada, desempacotando o array $nomes o que veio do $_GET['nomes'].

Chamar sortear(...[1, 2, 3]) é o mesmo que sortear(1, 2, 3), entende? E capturar isso com function sortear(...$numeros) é o mesmo que function sortear($um, $dois, $tres), nesse caso em específico, onde posteriormente você faria $numeros = [$um, $dois, $tres].

Kiko, no caso lá do sorteio, recebendo um array... Nós não poderíamos simplesmente passar o array para o método?

Poderíamos, mas não conseguiríamos restringir o tipo de dado aceito. Ao fazer dessa forma, você diz que espera um array mas não um array de strings:

<?php

$nomes = $_GET['nomes'] ?? [];

var_dump(sortearNomes($nomes));

function sortearNomes(array $nomes) {
    $randomIndex = array_rand($nomes);
    return $nomes[$randomIndex];
}

Isso é inteiramente possível, só acho que fica mais fraco, concorda? No final das contas, o spread operator pode te ajudar a fazer essas validações, nesse caso. Captou?!

Ah, Kiko, eu trabalho com uma versão antiga do PHP que não existia isso...

Nesse caso, há uma gambiarra que muita gente fazia, usando as funções internas func_num_args(), func_get_arg() e func_get_args(). Essas funções servem para, respectivamente, capturar o número de argumentos recebidos na função, capturar o primeiro argumento ou capturar todos os argumentos em um array. Assim, nossa função de sorteio ficaria:

<?php

$nomes = $_GET['nomes'] ?? [];

var_dump(sortearNomes($nomes));

function sortearNomes() {
    $nomes = func_get_args();
    $randomIndex = array_rand($nomes);
    return $nomes[$randomIndex];
}

Funciona do mesmo jeito e é pior ainda para validar, pois o argumento nem aparenta existir, não é mesmo?! Felizmente não precisamos mais fazer isso. Se você puder atualizar as versões que consome, atualize logo!

Referência

Deixei esse por último porque não gosto que usem isso, mas é necessário dizer. Eu já mencionei o operador & no artigo Variáveis - O Básico. Aqui a aplicação e sintaxe são as mesmas: um & antes do operador de variável ($) e pronto.

É preciso ressaltar que, no caso de argumentos, é necessário que a referência esteja na declaração da função e não na chamada. Sendo assim:

sortear(&$nomes); // não pode

function sortear(&$nomes) { } // pode

Note que a sintaxe que não é permitida funcionava até a versão 5.3 do PHP. Depois disso passou a ser completamente proibido. Deve ser um tabu mencionar isso mas to aqui pra compartilhar conhecimento, né?!

E acredito que, a essa altura, você já deve saber das consequências de usar uma referência, certo? Se não souber, dá uma olhada no artigo que linkei ali em cima para refrescar as memórias, fechado?!

E por hoje é só! Curtiu? Comenta e compartilha! Essa parte do spread operator é maravilhosa. O named arguments também é de fu*312@!! São pequenas revoluções que vão acontecendo a cada versão na linguagem, tornando-a cada vez mais versátil e prática. No próximo artigo, falaremos sobre o retorno das funções. Vamos explorar as possibilidades do PHP? Conta comigo!

Inté!!