PHP Escopo

Você cria $taxa = 0.1; no começo do arquivo, escreve uma função que usa $taxa… e o PHP diz que a variável não existe. Não é defeito: é o escopo, a regra que diz onde cada variável pode ser vista. Entender essa regra evita um dos avisos mais comuns de quem começa e explica por que funções são tão seguras de usar.

Você vai aprender
  • O que é o escopo global (o arquivo) e o escopo local (cada função).
  • Por que a função não enxerga a variável de fora (Warning: Undefined variable) e vice-versa.
  • O jeito certo de levar dados para dentro e para fora: parâmetros e return.
  • global e $GLOBALS: como funcionam e por que evitar.
  • static: uma variável de função que se lembra do valor entre as chamadas.
  • Uma surpresa do PHP: if, for e foreach não criam escopo.

Você precisa saber criar funções com parâmetros e return. Os exemplos rodam na máquina da Ana, na pasta ~/site.


A ideia antes do código: a cozinha do restaurante

Pense na lojinha de doces da Ana. No salão ficam os clientes, o caixa, o cardápio. A cozinha é uma sala fechada: o confeiteiro lá dentro não vê o que está no salão. Se ele precisa de alguma coisa, alguém tem que entregar pela janelinha (o pedido). Quando o doce fica pronto, ele devolve pela janelinha. O que acontece na cozinha (as tigelas, a farinha espalhada) fica na cozinha.

Na lojinhaNo PHP
O salãoO escopo global: o arquivo, fora de qualquer função.
A cozinha (uma sala fechada)O escopo local de uma função. Cada função tem a sua.
O pedido entregue pela janelinhaOs parâmetros.
O doce devolvido pela janelinhaO return.
As tigelas sujasAs variáveis criadas dentro da função: somem quando ela termina.

A função não vê as variáveis de fora

<?php
$x = 10;

function mostrar()
{
    echo "Dentro da função: x = $x\n";
}

mostrar();
echo "Fora da função: x = $x\n";
$ php fora.php

Warning: Undefined variable $x in /home/ana/site/fora.php on line 6
Dentro da função: x =
Fora da função: x = 10
Warning
"Aviso": o PHP não parou o programa, mas algo está errado (veja Lendo Erros do PHP).
Undefined variable $x
"Variável indefinida $x". Dentro da função, não existe nenhum $x.
on line 6
A linha do echo dentro da função. A linha 2, onde o $x foi criado, está fora.
x =
No lugar da variável que não existe, o PHP usa null, que no texto vira nada.

A última linha mostra que o $x de fora continua lá, valendo 10. Ele só não é visível dentro da função. Em PHP, uma função começa sempre com a "sala vazia": as únicas variáveis que existem lá dentro são os parâmetros e as que ela mesma criar.

Isso é diferente de outras linguagens

Em JavaScript e Python, uma função enxerga as variáveis de fora automaticamente. Em PHP, não. Se você vem dessas linguagens, este é o aviso que mais vai aparecer no começo.

E o contrário: o que nasce na função fica na função

<?php
function calcular()
{
    $resultado = 2 + 3;
    echo "Dentro: $resultado\n";
}

calcular();
echo "Fora: $resultado\n";
$ php dentro.php
Dentro: 5

Warning: Undefined variable $resultado in /home/ana/site/dentro.php on line 9
Fora: 

$resultado é uma variável local: nasce quando a função começa e é apagada quando ela termina (as tigelas sujas da cozinha). Na linha 9, fora da função, ela não existe mais.

Mesmo nome, variáveis diferentes

Como cada escopo é separado, uma variável de dentro pode ter o mesmo nome de uma de fora sem nenhum conflito. São duas caixas diferentes, com a mesma etiqueta, em salas diferentes:

<?php
$nome = "Ana";

function trocarNome()
{
    $nome = "Bruno";
    echo "Dentro: $nome\n";
}

trocarNome();
echo "Fora: $nome\n";
$ php mesmonome.php
Dentro: Bruno
Fora: Ana             « o $nome de fora nunca foi tocado

Essa é a grande vantagem do escopo: você pode usar uma função sem medo de ela bagunçar as suas variáveis. Ela pode ter um $i, um $total, um $nome lá dentro, e os seus continuam intactos.

Cada chamada começa do zero

As variáveis locais são criadas de novo a cada chamada. Por isso este contador não conta:

<?php
function contarVisita()
{
    $visitas = 0;
    $visitas++;
    echo "Visitas: $visitas\n";
}

contarVisita();
contarVisita();
contarVisita();
$ php recomeca.php
Visitas: 1
Visitas: 1
Visitas: 1

Cada chamada cria um $visitas novo valendo 0, soma 1, mostra e joga fora. Guarde este exemplo: ele vai ser consertado com static, mais abaixo.


O jeito certo: parâmetros para dentro, return para fora

Se a função precisa de um valor de fora, entregue pela janelinha: um parâmetro. Se o resultado precisa sair, devolva pela janelinha: return.

<?php
$preco = 50;
$taxa = 0.1;

function comTaxa(float $preco, float $taxa): float
{
    return $preco + $preco * $taxa;
}

$total = comTaxa($preco, $taxa);
echo "Total: $total\n";
echo "Outro: ", comTaxa(200, 0.05), "\n";
TrechoO que faz
function comTaxa(float $preco, float $taxa)Os parâmetros são variáveis locais que já nascem com os valores entregues na chamada.
comTaxa($preco, $taxa)Entrega os valores das variáveis de fora. Os nomes iguais são coincidência: poderiam ser outros.
return …Devolve o resultado.
$total = …Quem chamou decide onde guardar o resultado.
$ php certo.php
Total: 55
Outro: 210

Repare na última linha: como a função não depende de nenhuma variável de fora, ela funciona com quaisquer valores. É isso que torna uma função reaproveitável.

O parâmetro é uma cópia

Quando você passa uma variável para uma função, a função recebe uma cópia do valor. Mudar o parâmetro lá dentro não muda a variável de fora:

<?php
function dobrar($numero)
{
    $numero = $numero * 2;
    echo "Dentro: $numero\n";
}

$valor = 5;
dobrar($valor);
echo "Fora: $valor\n";
$ php parametro.php
Dentro: 10
Fora: 5

Para mudar o valor de fora, o caminho normal é devolver e guardar: $valor = dobrar($valor); (com return na função). Existe também a passagem por referência (function dobrar(&$numero)), em que a função mexe na própria variável; ela aparece em Parâmetros e no foreach, mas use com cuidado, pelo mesmo motivo do global.

Missão: a função tenta usar $loja, que está do lado de fora. Rode, veja o aviso e corrija sem usar global: faça a função receber o nome da loja por parâmetro e passe $loja na chamada. A saída deve ser Bem-vinda à Doces da Ana!.

<?php
$loja = "Doces da Ana";

function saudacao(): string
{
    return "Bem-vinda à $loja!";
}

echo saudacao(), "\n";
<?php
$loja = "Doces da Ana";

function saudacao(string $loja): string
{
    return "Bem-vinda à $loja!";
}

echo saudacao($loja), "\n";

Antes da correção, o PHP real mostra:

$ php saudacao.php

Warning: Undefined variable $loja in /home/ana/site/saudacao.php on line 6
Bem-vinda à !

global: abrir uma porta da cozinha para o salão

O PHP tem um jeito de a função usar uma variável do escopo global: a palavra global ("global"), seguida do nome da variável, dentro da função.

<?php
$x = 10;

function mostrar()
{
    global $x;
    echo "Dentro da função: x = $x\n";
    $x = 99;
}

mostrar();
echo "Fora da função: x = $x\n";
global $x;
"Dentro desta função, $x é a variável $x lá de fora." A partir dessa linha, a função lê e escreve na variável global.
$x = 99;
Muda a variável de fora, de verdade.
$ php global.php
Dentro da função: x = 10
Fora da função: x = 99      « a função mudou a variável de fora

Existe ainda o array $GLOBALS, que está disponível em qualquer lugar e guarda todas as variáveis globais pelo nome (é uma das superglobais):

<?php
$loja = "Doces da Ana";

function titulo()
{
    return "Bem-vindo à " . $GLOBALS["loja"];
}

echo titulo(), "\n";
$ php globals.php
Bem-vindo à Doces da Ana

Por que evitar o global

Funciona, mas tira da função a maior qualidade dela: ser uma sala fechada. Veja um bug típico. Duas funções de um carrinho de compras usam o mesmo $total global, e uma delas reaproveita o nome sem pensar:

<?php
$total = 0;

function adicionarAoCarrinho($preco)
{
    global $total;
    $total += $preco;
}

function calcularFrete()
{
    global $total;
    $total = $total > 100 ? 0 : 15;   // ops: reaproveitou o nome $total
    return $total;
}

adicionarAoCarrinho(40);
adicionarAoCarrinho(30);
echo "Frete: R$ ", calcularFrete(), "\n";
echo "Total das compras: R$ $total\n";
$ php globalbug.php
Frete: R$ 15
Total das compras: R$ 15        « deveria ser 70!

Nenhum aviso: o código é válido. Mas calcularFrete sobrescreveu o total das compras. Num programa grande, descobrir qual das funções mexeu na variável global pode levar horas. Os problemas do global:

  • Dependência escondida: olhando só a primeira linha da função (function calcularFrete()), você não sabe que ela precisa de $total nem que vai mudá-lo.
  • Qualquer função pode estragar o valor, como no exemplo.
  • Difícil de reaproveitar e de testar: a função só funciona se existir uma variável com aquele nome exato do lado de fora.

A versão sem global deixa tudo à vista: function calcularFrete(float $total): float, chamada com calcularFrete($total). Para valores fixos que todo o programa usa (o nome da loja, a taxa de entrega), prefira constantes, que são visíveis em todo lugar e não podem ser alteradas por engano.


static: uma variável local que não esquece

Lembra do contador de visitas que sempre mostrava 1? A palavra static ("estática") muda a regra para uma variável: ela continua local (ninguém de fora a vê), mas não é apagada quando a função termina. Na próxima chamada, ela ainda tem o valor anterior.

<?php
function contarVisita()
{
    static $visitas = 0;
    $visitas++;
    echo "Visitas: $visitas\n";
}

contarVisita();
contarVisita();
contarVisita();
static
"Esta variável sobrevive entre as chamadas desta função."
$visitas = 0
O valor inicial. Atenção: essa atribuição acontece só na primeira chamada. Nas seguintes, a linha é pulada e a variável mantém o valor que tinha.
$ php static.php
Visitas: 1
Visitas: 2
Visitas: 3

Um uso prático: gerar números em sequência, como os pedidos da lojinha. E repare que, mesmo sendo static, a variável continua invisível do lado de fora:

<?php
function proximoPedido(): string
{
    static $numero = 1000;
    $numero++;
    return "Pedido nº $numero";
}

echo proximoPedido(), "\n";
echo proximoPedido(), "\n";
echo proximoPedido(), "\n";
echo $numero, "\n";
$ php proximoid.php
Pedido nº 1001
Pedido nº 1002
Pedido nº 1003

Warning: Undefined variable $numero in /home/ana/site/proximoid.php on line 12
Valor inicial calculado PHP 8.3+

Até o PHP 8.2, o valor inicial de uma static tinha de ser fixo (um número, um texto, uma constante). Desde o 8.3, pode ser qualquer expressão, como static $inicio = date("H:i");, calculada uma única vez, na primeira chamada.

A static não sobrevive entre páginas

"Uma variável que não esquece" serve para um contador de visitas do site? Não. Ela só lembra enquanto o programa está rodando. Na web, cada vez que alguém abre a página, o PHP roda o script do zero e, no fim, esquece tudo. A Ana testou com o servidor embutido:

<?php
function contarVisita(): int
{
    static $visitas = 0;
    $visitas++;
    return $visitas;
}

contarVisita();
echo "Você é o visitante nº ", contarVisita(), "\n";
$ php -S localhost:8000 > /tmp/servidor.log 2>&1 &     « o servidor, em segundo plano
[1] 477837
$ curl -s localhost:8000/visitas.php
Você é o visitante nº 2            « duas chamadas na mesma página: a static funcionou
$ curl -s localhost:8000/visitas.php
Você é o visitante nº 2            « página aberta de novo: tudo recomeçou
$ kill %1                          « desliga o servidor

Para guardar algo entre uma página e outra, você precisa de um lugar que dure mais que o script: um arquivo (Ler e Escrever Arquivos), a sessão do visitante (Sessões) ou um banco de dados.


Uma surpresa: if, for e foreach não criam escopo

Em PHP, só as funções (e as classes, mais adiante) criam uma sala separada. As chaves de um if ou de um laço não: uma variável criada lá dentro continua existindo depois.

<?php
$nota = 8;
if ($nota >= 7) {
    $situacao = "aprovada";
}
echo "Situação: $situacao\n";

foreach (["pão", "leite", "café"] as $item) {
    $ultimo = $item;
}
echo "Depois do laço: \$item = $item, \$ultimo = $ultimo\n";

for ($i = 0; $i < 3; $i++) {
}
echo "Depois do for: \$i = $i\n";
$ php blocos.php
Situação: aprovada
Depois do laço: $item = café, $ultimo = café
Depois do for: $i = 3

O $item do foreach continua valendo o último elemento, e o $i do for ficou em 3 (o valor que fez a condição $i < 3 falhar). Isso é útil às vezes, mas também é a origem de um bug famoso do foreach por referência, explicado em foreach. E cuidado com o if: se a nota fosse 5, $situacao nunca seria criada, e a linha do echo daria Undefined variable $situacao. Crie a variável antes do if com um valor padrão.

Resumo dos escopos do PHP

Onde a variável foi criadaOnde ela é visívelQuanto tempo dura
Fora de funções (global)Fora de funções; dentro, só com global ou $GLOBALSAté o fim do script
Dentro de uma função (local), inclusive parâmetrosSó dentro daquela chamada da funçãoAté a função terminar
Dentro de uma função, com staticSó dentro da funçãoAté o fim do script (sobrevive entre chamadas)
Dentro de if, for, foreach, whileNo escopo em volta (o bloco não isola nada)Como o escopo em volta
Superglobais: $_GET, $_POST, $GLOBALS…Em todo lugarAté o fim do script
Constantes (const, define)Em todo lugarAté o fim do script

As funções anônimas têm a sua própria forma de trazer variáveis de fora, o use; veja Funções Anônimas e Arrow Functions. E arquivos carregados com include compartilham o escopo de onde foram incluídos (include e require).


Quando dá errado

Warning: Undefined variable $x dentro de uma função
$ php fora.php

Warning: Undefined variable $x in /home/ana/site/fora.php on line 6
Dentro da função: x =
Fora da função: x = 10

O que aconteceu: a função usou uma variável que só existe fora dela.

Como consertar: receba o valor por parâmetro (function mostrar($x), chamada com mostrar($x)). Se for um valor fixo do programa inteiro, transforme em constante. global é o último recurso.

Warning: Undefined variable fora da função
$ php dentro.php
Dentro: 5

Warning: Undefined variable $resultado in /home/ana/site/dentro.php on line 9
Fora: 

O que aconteceu: você tentou usar, fora, uma variável criada dentro da função. Ela foi apagada quando a função terminou.

Como consertar: a função deve return $resultado;, e quem chama guarda: $resultado = calcular();.

O contador sempre mostra 1 (sem erro)
$ php recomeca.php
Visitas: 1
Visitas: 1
Visitas: 1

O que aconteceu: variável local comum é recriada a cada chamada.

Como consertar: static $visitas = 0; (se o valor só precisa durar enquanto o script roda), ou guarde o valor fora da função e passe/devolva.

global $x = 5;: unexpected token "="
<?php
function f()
{
    global $x = 5;
}
$ php globalatrib.php

Parse error: syntax error, unexpected token "=", expecting "," or ";" in /home/ana/site/globalatrib.php on line 4

O que significa: depois de global $x o PHP espera , (para mais variáveis: global $x, $y;) ou ;. O global só "abre a porta", não atribui.

Como consertar: duas linhas: global $x; e depois $x = 5;. (Com static, ao contrário, o valor inicial vai na mesma linha: static $x = 5;.)

Uma variável global mudou "sozinha" (sem erro)
$ php globalbug.php
Frete: R$ 15
Total das compras: R$ 15

O que aconteceu: alguma função com global $total alterou a variável.

Como consertar: procure todos os global $total do projeto (é por isso que é difícil). A solução duradoura é eliminar o global: parâmetros para dentro, return para fora.


Referência

FormaO que faz
variável dentro da funçãoLocal: só existe naquela chamada.
parâmetroLocal, já criado com uma cópia do valor passado.
return $valor;Leva um valor para fora da função.
global $a, $b;Dentro da função, $a e $b passam a ser as variáveis globais (lê e escreve). Evite.
$GLOBALS["nome"]Acessa a variável global $nome de qualquer lugar. Evite.
static $n = 0;Variável local que mantém o valor entre as chamadas; o = 0 roda só na primeira. Expressões no valor inicial: PHP 8.3+.
use ($x)Em funções anônimas, leva uma cópia de $x (veja Closures).

Exercícios

1. Lembrar entre as chamadas

Complete para o contador continuar de onde parou a cada chamada:

$vezes = 0;

2. Levar o valor para fora

Complete a última linha da função para o resultado sair dela:

function area($l, $a) { $r = $l * $a; }

3. O jeito antigo (e desaconselhado)

Complete para a função usar a variável global $loja:

function titulo() { return $loja; }

4. O que mostra? $a = 1; function f() { $a = 2; } f(); echo $a;

5. O que mostra? foreach ([1, 2, 3] as $n) { } echo $n;

6. Qual é a melhor forma de uma função usar a taxa de entrega que está numa variável fora dela?

Missão final

Missão: a padaria da esquina distribui senhas, mas a função sempre devolve Senha 1. Mude uma linha para as três chamadas mostrarem Senha 1, Senha 2 e Senha 3.

<?php
function proximaSenha(): string
{
    $senha = 0;
    $senha++;
    return "Senha $senha";
}

echo proximaSenha(), "\n";
echo proximaSenha(), "\n";
echo proximaSenha(), "\n";
<?php
function proximaSenha(): string
{
    static $senha = 0;
    $senha++;
    return "Senha $senha";
}

echo proximaSenha(), "\n";
echo proximaSenha(), "\n";
echo proximaSenha(), "\n";

Resumo

  • Cada função é uma "sala fechada": não vê as variáveis de fora, e as de dentro somem quando ela termina. Usar uma variável do outro lado gera Warning: Undefined variable.
  • Variáveis com o mesmo nome dentro e fora são variáveis diferentes.
  • O jeito certo: dados entram por parâmetros (cópias) e saem por return.
  • global e $GLOBALS dão acesso às variáveis de fora, mas escondem dependências e permitem que qualquer função estrague o valor. Evite; para valores fixos, use constantes.
  • static cria uma variável local que lembra o valor entre as chamadas, só enquanto o script roda (na web, cada página recomeça).
  • if, for, foreach e while não criam escopo: o que nasce dentro continua existindo depois.