PHP Tipos e Retorno

Uma função é um combinado: "me dê isto e eu devolvo aquilo". Até agora esse combinado estava só na cabeça de quem escreveu a função. Nesta aula você aprende a escrevê-lo no código: declarar de que tipo é cada parâmetro (um número inteiro? um texto? um array?) e o que a função devolve com o return. Com os tipos declarados, o PHP passa a conferir o combinado e a avisar na hora em que alguém o quebra, em vez de deixar o erro aparecer lá na frente, com um resultado estranho.

Você vai aprender
  • Como o return devolve um valor (e encerra a função na hora).
  • A diferença entre mostrar (echo) e devolver (return).
  • Como declarar o tipo dos parâmetros: function f(int $x, string $nome, array $lista).
  • Como declarar o tipo do retorno: : int, : string, : ?string, : void, : mixed.
  • O que o PHP faz quando o tipo não bate: converter em silêncio (modo normal) ou dar TypeError (declare(strict_types=1)).
  • O que é o tipo never (PHP 8.1).

Antes, é bom ter feito Funções, Parâmetros e Tipos de Dados.


O return: devolver um resultado

Uma função pode trabalhar e entregar um resultado a quem a chamou, como a calculadora que mostra o total. A palavra return ("retornar", "devolver") faz isso.

<?php
function situacao($nota)
{
    if ($nota >= 7) {
        return "Aprovado";
    }
    if ($nota >= 5) {
        return "Recuperação";
    }
    return "Reprovado";
    echo "Esta linha nunca roda\n";
}
echo situacao(9.5), "\n";
echo situacao(6.0), "\n";
echo situacao(3.0), "\n";
$ php t-early.php
Aprovado
Recuperação
Reprovado               « e o "Esta linha nunca roda" de fato não apareceu

Duas coisas acontecem num return:

  1. O valor à direita vira o resultado da chamada: no lugar de situacao(9.5), fica "Aprovado".
  2. A função termina ali mesmo. Nada depois do return roda. Por isso o primeiro if não precisa de else: se a nota for 9.5, a função já acabou.

Esse estilo, com vários return que saem cedo, é muito usado e se chama early return ("retorno antecipado"). Ele evita ifs dentro de ifs.

Sem return, a função devolve null

<?php
function dobro($n)
{
    $resultado = $n * 2;     // calculou… e não devolveu
}
var_dump(dobro(4));
$ php t-null.php
NULL

Este é um erro clássico: a conta foi feita, mas ficou presa dentro da função. Faltou return $resultado;. Mais abaixo, você vai ver que declarar o tipo do retorno faz o PHP denunciar esse erro.

Mostrar (echo) não é devolver (return)

Para quem está começando, as duas coisas parecem iguais, porque nos dois casos "aparece o número". Não são:

<?php
function dobroEcho(int $n): void
{
    echo $n * 2;
}
function dobroReturn(int $n): int
{
    return $n * 2;
}
dobroEcho(5);
echo "\n";
$x = dobroReturn(5);
echo $x + 1, "\n";
$y = dobroEcho(5);
echo "\n";
var_dump($y);
$ php t-echoreturn.php
10                      « dobroEcho mostrou o 10 na tela
11                      « dobroReturn devolveu 10; deu para somar 1
10                      « dobroEcho mostrou de novo…
NULL                    « …mas não devolveu nada: $y ficou null
echo dentro da funçãoreturn
Para onde vai o valorDireto para a tela (ou a página)Para quem chamou
Dá para guardar numa variável?NãoSim
Dá para usar numa conta, num if?NãoSim

Regra prática: funções que calculam devolvem com return; quem chama decide se mostra, guarda ou usa em outra conta. Isso deixa a função reaproveitável: formatarPreco serve para a tela, para um e-mail, para um PDF…


Tipos nos parâmetros

Sem tipos, uma função aceita qualquer coisa, e o problema só aparece lá dentro, às vezes com uma mensagem que não ajuda:

<?php
function precoFinal($preco, $quantidade)
{
    return $preco * $quantidade;
}
echo precoFinal(18.9, 2), "\n";
echo precoFinal("18.9", "2"), "\n";
echo precoFinal("dezoito", 2), "\n";
$ php t-sem.php
37.8
37.8

Fatal error: Uncaught TypeError: Unsupported operand types: string * int in /home/ana/site/t-sem.php:4
Stack trace:
#0 /home/ana/site/t-sem.php(8): precoFinal('dezoito', 2)
#1 {main}
  thrown in /home/ana/site/t-sem.php on line 4

O erro aponta a linha 4 (a conta, dentro da função), e a mensagem fala de "operandos". A culpa, na verdade, é de quem chamou na linha 8. Agora com tipos:

<?php
function precoFinal(float $preco, int $quantidade)
{
    return $preco * $quantidade;
}
echo precoFinal(18.9, 2), "\n";
echo precoFinal("18.9", "2"), "\n";
echo precoFinal(18, 2), "\n";
echo precoFinal("dezoito", 2), "\n";
float $preco
O tipo vem antes do nome do parâmetro: "$preco deve ser um número com decimais".
int $quantidade
"$quantidade deve ser um número inteiro".
$ php t-param.php
37.8
37.8                    « "18.9" e "2" são textos numéricos: o PHP converteu
36                      « o int 18 foi aceito como float

Fatal error: Uncaught TypeError: precoFinal(): Argument #1 ($preco) must be of type float, string given, called in /home/ana/site/t-param.php on line 9 and defined in /home/ana/site/t-param.php:2
Stack trace:
#0 /home/ana/site/t-param.php(9): precoFinal('dezoito', 2)
#1 {main}
  thrown in /home/ana/site/t-param.php on line 2

A mensagem agora diz exatamente o que houve: "o argumento nº 1 ($preco) deve ser do tipo float, foi passada uma string, chamada na linha 9". O erro aparece na porta de entrada da função, e aponta o culpado.

Os tipos que você vai usar

TipoAceitaExemplo
intnúmero inteiroint $quantidade
floatnúmero com decimais (aceita int também)float $preco
stringtextostring $nome
booltrue ou falsebool $ativo
arrayqualquer arrayarray $notas
?stringstring ou null (o ? vale para qualquer tipo: ?int, ?array…)?string $email
int|floatum ou outro (tipos união, PHP 8.0+)int|float $valor
mixedqualquer coisa (é o mesmo que não pôr tipo, mas deixa claro que foi de propósito)mixed $valor

Também dá para usar nomes de classes como tipo (Produto $p), assunto do Nível 3. Os tipos mais elaborados estão em Tipos Avançados.


Modo normal × declare(strict_types=1)

Você deve ter reparado: com float $preco, a string "18.9" foi aceita. Isso é o modo normal do PHP, chamado de modo coercivo ("que força"): se o valor puder ser convertido para o tipo pedido sem perder informação importante, o PHP converte em silêncio. Veja até onde ele vai:

<?php
function dobro(int $n)
{
    return $n * 2;
}
var_dump(dobro(5));
var_dump(dobro("5"));
var_dump(dobro(true));
var_dump(dobro(5.0));
var_dump(dobro(5.5));
var_dump(dobro("5 reais"));
$ php t-coerc.php
int(10)
int(10)                 « "5" virou 5
int(2)                  « true virou 1!
int(10)                 « 5.0 virou 5

Deprecated: Implicit conversion from float 5.5 to int loses precision in /home/ana/site/t-coerc.php on line 2
int(10)                 « 5.5 virou 5, com um aviso: o ,5 se perdeu

Fatal error: Uncaught TypeError: dobro(): Argument #1 ($n) must be of type int, string given, called in /home/ana/site/t-coerc.php on line 11 and defined in /home/ana/site/t-coerc.php:2
Stack trace:
#0 /home/ana/site/t-coerc.php(11): dobro('5 reais')
#1 {main}
  thrown in /home/ana/site/t-coerc.php on line 2

Algumas dessas conversões são práticas (dados de formulário chegam como texto, aula $_GET e $_POST); outras escondem erros (true virando 1). Se você prefere que o PHP não converta nada, ligue o modo estrito com uma linha no topo do arquivo:

<?php
declare(strict_types=1);

function dobro(int $n)
{
    return $n * 2;
}
var_dump(dobro(5));
var_dump(dobro("5"));
declare(…)
"Declarar": uma instrução para o próprio PHP sobre como tratar o arquivo.
strict_types=1
"Tipos estritos: ligado". Sem espaços em volta do = por costume; o valor é sempre 1.
$ php t-strict.php
int(10)

Fatal error: Uncaught TypeError: dobro(): Argument #1 ($n) must be of type int, string given, called in /home/ana/site/t-strict.php on line 9 and defined in /home/ana/site/t-strict.php:4
Stack trace:
#0 /home/ana/site/t-strict.php(9): dobro('5')
#1 {main}
  thrown in /home/ana/site/t-strict.php on line 4

Com o modo estrito, "5" não é 5: TypeError. A única conversão que continua permitida é de int para float (um inteiro "cabe" num float sem perder nada). E o modo vale também para as funções nativas chamadas no arquivo:

<?php
declare(strict_types=1);

function metade(float $n)
{
    return $n / 2;
}
var_dump(metade(5));
var_dump(strlen("Ana"));
var_dump(strlen(123));
$ php t-strict-float.php
float(2.5)              « int → float continua aceito
int(3)

Fatal error: Uncaught TypeError: strlen(): Argument #1 ($string) must be of type string, int given in /home/ana/site/t-strict-float.php:10
Stack trace:
#0 {main}
  thrown in /home/ana/site/t-strict-float.php on line 10
Chamada para f(int $n)Modo normalCom strict_types=1
f(5)55
f("5")convertido para 5TypeError
f(5.0)convertido para 5TypeError
f(5.5)5, com DeprecatedTypeError
f(true)convertido para 1TypeError
f("5 reais")TypeErrorTypeError
para g(float $x): g(5)5.05.0 (a exceção permitida)

As regras do declare

  • Tem de ser a primeira instrução do arquivo, logo depois do <?php. Senão:
<?php
echo "Começando\n";
declare(strict_types=1);
$ php t-strict-lugar.php

Fatal error: strict_types declaration must be the very first statement in the script in /home/ana/site/t-strict-lugar.php on line 3
Stack trace:
#0 {main}
  • Vale só para o arquivo onde está escrito, e conta o arquivo de onde a função é chamada, não onde ela foi definida. Num projeto com vários arquivos (aula include e require), coloque-o em todos.
Qual usar?

Projetos modernos usam declare(strict_types=1); em todos os arquivos: erros de tipo aparecem cedo, na linha certa, em vez de virar um cálculo errado. Mas atenção ao que vem de fora: $_GET, $_POST, arquivos e explode sempre dão strings. No modo estrito, converta você mesmo ((int) $_GET["quantidade"]) ou valide antes (aulas Conversão de Tipos e Validar Dados).

Rode como está e leia o TypeError. Depois apague a linha do declare e rode de novo: o que muda? Experimente também dobro(5.5) e dobro(true) nos dois modos.

<?php
declare(strict_types=1);

function dobro(int $n): int
{
    return $n * 2;
}

var_dump(dobro(5));
var_dump(dobro("5"));

Tipo do retorno

Depois dos parênteses, dois-pontos e o tipo do que a função devolve:

<?php
function dobro(int $n): int
{
    return $n * 2;
}
function media(array $notas): float
{
    return array_sum($notas) / count($notas);
}
function saudacao(string $nome): string
{
    return "Olá, $nome!";
}
function eMaiorDeIdade(int $idade): bool
{
    return $idade >= 18;
}
var_dump(dobro(21));
var_dump(media([8, 9, 10]));
var_dump(saudacao("Ana"));
var_dump(eMaiorDeIdade(17));
function media(array $notas): float
Lê-se: "media recebe um array e devolve um float". Só de ler a primeira linha (a assinatura da função), você sabe como usá-la.
$ php t-retorno.php
int(42)
float(9)                « 27 / 3 dá o int 9; o tipo de retorno float o converteu para 9.0
string(10) "Olá, Ana!"
bool(false)

O PHP confere o valor do return do mesmo jeito que confere os parâmetros: no modo normal converte quando pode; no estrito, só aceita int → float. Quando não dá:

<?php
function codigo(string $produto): int
{
    return "sem código";
}
var_dump(codigo("café"));
$ php t-retorno-errado2.php

Fatal error: Uncaught TypeError: codigo(): Return value must be of type int, string returned in /home/ana/site/t-retorno-errado2.php:4
Stack trace:
#0 /home/ana/site/t-retorno-errado2.php(6): codigo('caf\xC3\xA9')
#1 {main}
  thrown in /home/ana/site/t-retorno-errado2.php on line 4

(No stack trace, o é de "café" aparece como os seus dois bytes, \xC3\xA9: o PHP mostra os bytes acentuados assim nos rastros de erro.)

E aquele erro clássico de esquecer o return? Com o tipo declarado, ele não passa mais em silêncio:

<?php
function dobro(int $n): int
{
    $resultado = $n * 2;
}
var_dump(dobro(4));
$ php t-semreturn.php

Fatal error: Uncaught TypeError: dobro(): Return value must be of type int, none returned in /home/ana/site/t-semreturn.php:5
Stack trace:
#0 /home/ana/site/t-semreturn.php(6): dobro(4)
#1 {main}
  thrown in /home/ana/site/t-semreturn.php on line 5

"Deve devolver int, nada foi devolvido" (none returned). Compare com o NULL silencioso do começo da aula.

O modo estrito também vale no retorno

<?php
declare(strict_types=1);

function media(array $notas): int
{
    return array_sum($notas) / count($notas);
}
var_dump(media([8, 9, 10]));
var_dump(media([7, 8]));
$ php t-strict2.php
int(9)                  « 27 / 3 = 9, um int: passou

Fatal error: Uncaught TypeError: media(): Return value must be of type int, float returned in /home/ana/site/t-strict2.php:6
Stack trace:
#0 /home/ana/site/t-strict2.php(9): media(Array)
#1 {main}
  thrown in /home/ana/site/t-strict2.php on line 6

15 / 2 = 7.5, um float. Sem o modo estrito, o mesmo código devolveria 7 com um Deprecated: Implicit conversion from float 7.5 to int loses precision, e a média estaria errada. O problema real aqui é o tipo: média deveria ser float.

?string: pode devolver "nada"

Quando uma função procura algo que pode não existir, é comum devolver null nesse caso. O ? antes do tipo diz "este tipo ou null":

<?php
$emails = ["Ana" => "ana@exemplo.com", "Bruno" => "bruno@exemplo.com"];

function buscarEmail(array $emails, string $nome): ?string
{
    return $emails[$nome] ?? null;
}

var_dump(buscarEmail($emails, "Ana"));
var_dump(buscarEmail($emails, "Zeca"));
echo buscarEmail($emails, "Zeca") ?? "sem e-mail", "\n";
$ php t-nullable.php
string(15) "ana@exemplo.com"
NULL
sem e-mail

Repare que o array $emails foi passado como parâmetro: uma função não enxerga as variáveis de fora dela (aula Escopo). Nos parâmetros, o ? funciona igual, e combina bem com o padrão null da aula Parâmetros:

<?php
function saudacao(?string $nome = null): string
{
    return "Olá, " . ($nome ?? "visitante") . "!";
}
echo saudacao("Ana"), "\n";
echo saudacao(), "\n";
echo saudacao(null), "\n";
$ php t-nullparam.php
Olá, Ana!
Olá, visitante!
Olá, visitante!

Sem o ?, um parâmetro string recusa null (must be of type string, null given). E escrever string $nome = null, sem o ?, ainda funciona mas é desaprovado desde o PHP 8.4 (veja Quando dá errado).

void: não devolve nada

Para funções que só fazem algo (mostrar, gravar), sem resultado, o tipo de retorno é void ("vazio"):

<?php
function linha(int $tamanho): void
{
    echo str_repeat("-", $tamanho), "\n";
}
linha(10);
var_dump(linha(3));
$ php t-void.php
----------
---
NULL                    « quem tenta usar o "resultado" de uma função void recebe null

Numa função void, return; sozinho (para sair mais cedo) é permitido; return com valor, não:

<?php
function linha(int $tamanho): void
{
    return str_repeat("-", $tamanho);
}
$ php t-void-erro.php

Fatal error: A void function must not return a value in /home/ana/site/t-void-erro.php on line 4
Stack trace:
#0 {main}

Esse erro aparece antes de qualquer coisa rodar, só de o PHP ler o arquivo.

mixed e tipos união

<?php
function descrever(mixed $valor): string
{
    return get_debug_type($valor);
}
echo descrever(10), " ", descrever("Ana"), " ", descrever([1, 2]), " ", descrever(null), " ", descrever(2.5), "\n";

function dobro(int|float $n): int|float
{
    return $n * 2;
}
var_dump(dobro(4), dobro(1.5));
$ php t-mixed.php
int string array null float     « get_debug_type diz o tipo de qualquer valor
int(8)
float(3)

mixed aceita tudo (inclusive null); use quando a função realmente lida com qualquer tipo. int|float aceita um ou outro. Prefira sempre o tipo mais preciso que fizer sentido.

array para devolver vários valores

Uma função devolve um valor. Para devolver dois ou mais, devolva um array, e desmonte-o com a desestruturação:

<?php
function minMax(array $numeros): array
{
    return [min($numeros), max($numeros)];
}
[$menor, $maior] = minMax([7, 3, 9, 5]);
echo "Menor: $menor, maior: $maior\n";
$ php t-array.php
Menor: 3, maior: 9

never: a função nunca volta PHP 8.1+

Existe ainda um tipo de retorno para funções que nunca terminam normalmente: elas sempre encerram o programa (exit) ou lançam uma exceção. É o caso de uma função redirecionar(), que manda o navegador para outra página e para tudo. Você vai encontrar never nas aulas header e Redirecionamento e Exceções; por ora, basta saber que ele existe.


Quando dá errado

TypeError: …(): Argument #1 ($n) must be of type int, string given

O que aconteceu: um argumento não é do tipo declarado (e não pôde ser convertido, ou o modo estrito está ligado). A mensagem traz o número e o nome do parâmetro, o tipo esperado, o tipo recebido e a linha da chamada (called in … on line …).

Como resolver: vá à linha da chamada. Se o valor veio de um formulário ou de um explode, converta: dobro((int) $texto). Se o tipo declarado é que está errado (uma média declarada como int), corrija a declaração.

TypeError: …(): Return value must be of type int, none returned

O que aconteceu: a função chegou ao fim sem passar por nenhum return. Às vezes é só um caminho esquecido: um if que tem return e um caso que não tem.

Como resolver: confira que todo caminho termina num return com valor do tipo certo. Se a função não deve devolver nada, o tipo é void.

Deprecated: Implicitly marking parameter $nome as nullable is deprecated
<?php
function saudacao(string $nome = null): string
{
    return "Olá, " . ($nome ?? "visitante") . "!";
}
echo saudacao(), "\n";
$ php t-implicit.php

Deprecated: saudacao(): Implicitly marking parameter $nome as nullable is deprecated, the explicit nullable type must be used instead in /home/ana/site/t-implicit.php on line 2
Olá, visitante!

O que aconteceu: o tipo é string, mas o padrão é null. Antigamente, isso tornava o parâmetro "string ou null" sem avisar. Desde o PHP 8.4, o PHP pede que você diga isso explicitamente.

Como resolver: ponha o ?: ?string $nome = null.

strict_types declaration must be the very first statement in the script

O que aconteceu: havia algo antes do declare (um echo, um require, até HTML ou uma linha em branco antes do <?php). Veja t-strict-lugar.php, acima.

Como resolver: o arquivo deve começar exatamente com <?php e, logo depois, declare(strict_types=1);. Comentários antes do declare são permitidos.

A void function must not return a value

O que aconteceu: uma função declarada : void tem um return com valor.

Como resolver: se a função deve devolver algo, troque void pelo tipo certo; se não deve, use só return; (ou nada).


Referência rápida

Você quer…Código
Devolver um valorreturn $x;
Sair da função mais cedoif (…) { return …; }
Tipo no parâmetrofunction f(int $n, string $s, array $a, bool $b, float $x)
Parâmetro que aceita nullfunction f(?string $s = null)
Tipo do retornofunction f(): int, : string, : array, : bool, : float
Retorno que pode ser nullfunction f(): ?string
Não devolve nadafunction f(): void
Qualquer tipo / um ou outromixed / int|float
Nunca volta (sempre sai ou lança erro)function f(): never (8.1)
Desligar conversões automáticasdeclare(strict_types=1); na 1ª linha
Saber o tipo de um valorget_debug_type($x)

Missão final

Missão: deixe a função "à prova de erros". (1) Ligue o modo estrito na primeira linha depois do <?php. (2) Declare o parâmetro como float $nota e o retorno como string. (3) Rode: uma das chamadas vai dar TypeError. Leia a mensagem e corrija a chamada. No fim, as três situações devem aparecer, sem nenhum erro.

<?php
function situacao($nota)
{
    if ($nota >= 7) {
        return "Aprovado";
    }
    if ($nota >= 5) {
        return "Recuperação";
    }
    return "Reprovado";
}

echo situacao(9.5), "\n";
echo situacao("6.0"), "\n";
echo situacao(3), "\n";
<?php
declare(strict_types=1);

function situacao(float $nota): string
{
    if ($nota >= 7) {
        return "Aprovado";
    }
    if ($nota >= 5) {
        return "Recuperação";
    }
    return "Reprovado";
}

echo situacao(9.5), "\n";
echo situacao(6.0), "\n";
echo situacao(3), "\n";

No passo 3, o PHP do laboratório mostra:

$ php situacao.php
Aprovado

Fatal error: Uncaught TypeError: situacao(): Argument #1 ($nota) must be of type float, string given, called in /home/ana/site/situacao.php on line 16 and defined in /home/ana/site/situacao.php:4
Stack trace:
#0 /home/ana/site/situacao.php(16): situacao('6.0')
#1 {main}
  thrown in /home/ana/site/situacao.php on line 4

A chamada situacao(3) não dá erro: no modo estrito, int → float continua permitido.


Exercícios

1. O tipo do retorno

Complete para dizer que a função devolve um texto:

function saudacao(string $nome)

2. Pode ser nulo

Complete o tipo do retorno: a função devolve um int ou null:

function buscarCodigo(string $produto):

3. Modo estrito

Complete a primeira instrução do arquivo:

<?php

4. Sem strict_types, o que acontece com dobro("8"), sendo function dobro(int $n): int?

5. Com declare(strict_types=1);, qual chamada a function metade(float $x) é aceita?

6. Uma função calcula um valor, mas var_dump(f()) mostra NULL. A causa mais provável?


Resumo

  • return devolve um valor e encerra a função. Sem return, o resultado é null. Devolver não é mostrar: echo vai para a tela, return vai para quem chamou.
  • Tipos nos parâmetros (int $n) e no retorno (): int) documentam o combinado e fazem o PHP conferir.
  • ?tipo aceita null; void não devolve nada; mixed aceita tudo; int|float aceita um ou outro; never nunca volta.
  • Modo normal: o PHP converte o que der ("5" → 5, true → 1). Com declare(strict_types=1); (1ª instrução do arquivo), só int → float; o resto vira TypeError.
  • As mensagens de TypeError dizem o parâmetro, o tipo esperado, o recebido e a linha da chamada.