PHP para Iniciantes: Variáveis - Escopos

Confesso que, antes de escrever esse artigo, dei uma boa lida na documentação oficial do PHP para ver se eu estaria esquecendo de alguma coisa sobre o que estava pensando em escrever. Acabei me deparando com assuntos que eu não fazia ideia da existência e ganhei um novo tópico para admirar: escopo de variável. Então, apesar desse artigo ser bem focado para iniciantes, aposto que vai ter conteúdo interessante até para desenvolvedores mais experientes. Aperta o cinto!

Um pouco sobre escopo

O que é isso, Kiko?

É um copo que quebrou

Escopo é o mesmo que alcance. É saber onde uma coisa é reconhecida. Então, quando falamos de escopo de variável, estamos discutindo sobre onde ela se torna acessível nos diferentes níveis de código. Seja um bloco, uma função, um método de um objeto ou até mesmo um outro script para incluir no fluxo, as variáveis que você usa ao longo do caminho podem ser manipuladas por fluxos inesperados.

Isso é muito importante, pois você precisa estar ciente de qualquer alteração que possa ocorrer no código. Por exemplo, quando você escreve uma função:

<?php
function minhaNossaQueFuncaoInutil(string $umTexto): string
{
    return strtolower($umTexto); // deixa tudo em letras minúsculas
}

Você cria uma assinatura que obriga quem vai chamar a informar uma string, certo? Essa string vira uma variável visível somente dentro daquela função. Esse é o escopo... Mas infelizmente você também pode acessar variáveis que já existiram antes da criação da sua função. Nesse caso, você quer burlar a limitação de escopo de uma variável que só existe dentro, acessando ou criando algo que pode ser visto do lado de fora.

Esse escopo é chamado de global e sua declaração é bem simples:

<?php
function minhaNossaQueFuncaoInutil(): string
{
    global $umTexto; // anuncia que você quer recuperar um dado fora do escopo da função
    return strtolower($umTexto);
}

Não façam isso em casa, rs. Dessa forma, se existir uma variável $umTexto do lado de fora da função, você irá usar o dado dela dentro. Se você alterar o dado, também irá afetar o valor fora da função. E esse é o caos: você não tem como controlar que tipo de dado estava antes... E se o dado pertencer a outro fluxo, você pode quebrar alguma coisa.

Vamos fazer um exercício de imaginação. Suponha que você tem algum arquivo no seu projeto com essas funções aqui:

<?php

function open(): void
{
    global $status;
    $status = 'open';
}

function close(): void
{
    global $status;
    $status = 'closed';
}

O que elas fazem é capturar uma variável global $status e tirar qualquer que seja seu valor para mudar sua informação. Então, seu código inicial ficaria mais ou menos assim:

<?php

$idCarrinho = 123;
$status = 'pending';
open();
// faz as coisas

var_dump($status); // string(open)
close();

var_dump($status); // string(closed)

E até aí, tudo mais ou menos bem... Até que alguém passa uma demanda que coloca mais uma gestão de status depois que o carrinho abre, isto é, status de entrega.

<?php // outro script só pro status de entrega

function send(): void
{
    global $status;
    $status = 'sending';
}

function sent(): void
{
    global $status;
    $status = 'sent';
}

E aí você modifica o código antes para:

<?php

$idCarrinho = 123;
$status = 'pending';
open();
// faz as coisas

send();
// faz mais coisas
sent();

var_dump($status); // string(sent), ao invés de string(open)!
close();

var_dump($status); // string(closed)

Ok, não foi o melhor exemplo, mas acontece que a mesma variável $status está servindo para dois conceitos diferentes: status do carrinho e status de entrega. No mínimo, precisariam ser duas variáveis distintas... E são propriedades de alguma coisa (carrinho e entrega), portanto a estrutura em si está ruim. Mas fique como lição que, ao longo das manutenções, você pode esquecer que está usando uma variável global (por não estar presente no escopo natural) e comprometer o fluxo do código.

Outra forma de acessar uma variável global é usando a variável pré-definida $_GLOBALS, onde o índice seria o nome da variável desejada sem o $:

<?php
function open(): void
{
    $_GLOBALS['status'] = 'open';
}

Enfim, até aqui, falamos sobre variáveis globais... Mas uma coisa que eu nunca tinha visto é...

Variáveis estáticas (static)

Não tem muito a ver com mudança de escopo e sim continuação. A gente pode até pensar:

Ah, static em classes serve para mudar o escopo de uma propriedade para não depender da instância de um objeto... Então, variáveis estáticas tem a ver com escopo!

Sim, mas não por esse motivo. Afinal, variáveis não estão presas a nada além do próprio escopo, certo? E se eu quiser tornar algo um escopo acima, então eu irei declarar isso como global, concorda?

Então pra que serve variável estática, Kiko?

Para transmitir escopo entre execuções do mesmo bloco!

E acho que fica muito mais fácil compreender isso com exemplos. Em algum momento da vida, todos precisamos desenvolver uma função que conta alguma coisa, certo? No geral, é comum encontrar códigos assim:

<?php
function contar(int $contagemAtual): int
{
    return ++$contagemAtual;
}

$contagem = 0;
$contagem = contar($contagem);
var_dump($contagem); // int(1)
$contagem = contar($contagem);
var_dump($contagem); // int(2)
$contagem = contar($contagem);
var_dump($contagem); // int(3)
$contagem = contar($contagem);
var_dump($contagem); // int(4)

Com esse exemplo, fica claro que a variável $contagem só serve para guardar o último valor contado, certo? Então, esse valor poderia muito bem ser uma continuação de escopo. E é aí que entram as variáveis estáticas:

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

var_dump(contar()); // int(1)
var_dump(contar()); // int(2)
var_dump(contar()); // int(3)
var_dump(contar()); // int(4)

Ao declarar uma variável estática, ela passará a existir permanentemente naquele escopo, mantendo sempre o último valor informado. Sendo assim:

  • na primeira vez que chamamos static $contagemAtual = 0;, ele cria a variável estática com valor 0;
  • nas próximas vezes, ele apenas recupera o valor armazenado, sem atribuir o valor inicial.

Com isso, o incremento ++$contagemAtual sempre soma 1 ao último valor inserido na variável estática! Prático, não é mesmo? Porém, como tudo, tem uma limitação: você só pode atribuir como valor inicial de uma variável estática expressões constantes, jamais expressões dinâmicas (como funções).

<?php

function exemplo(): void
{
    static $a = 10*123; // expressão constante: pode.
    static $b = count([1,2,3,4]) // expressão dinâmica: não pode.
}

Mesmo que o array [1,2,3,4] do exemplo seja fixo, o interpretador não sabe disso. Na visão dele, o resultado da chamada count() pode variar e, por isso, é considerado uma expressão dinâmica. Se o valor estático inicial pode variar, não é estático...

Atenção!!

Ao usar variáveis estáticas em métodos, sua continuação se aplicará em toda execução daquele método independente de qual instância da classe está acionando!

<?php

class Exemplo {
    public function metodo(): int
    {
        static $contador = 0;
        return ++$contador;
    }
}

$obj1 = new Exemplo();
$obj2 = new Exemplo();

var_dump($obj1->metodo()); // int(1)
var_dump($obj2->metodo()); // int(2)!!

Referências em escopo global / estático

No último artigo eu falei sobre como fazer uma referência com o E comercial (&), correto? E hoje você aprendeu que uma variável de dentro de uma função não existe fora dela. Então eis a charada que vos trago: o que acontece se forçarmos a uma variável global referenciar a uma variável interna de uma função?

<?php

function alou(): void
{
    global $musica;
    $letra = "eu liguei só para ouvir sua voz...";
    $musica = &$letra; // referência
}

alou();
var_dump($musica); // ?

O resultado será NULL, pois a variável $letra não existe fora da função. Então, assim que a função se encerra, a referência se perde. Da mesma forma, as variáveis estáticas não são diferentes:

<?php

function hello(): void
{
    static $count = 0;
    static $song;
    if ($count++ === 0) {
        $lyric = "from the other side";
        $song = &$lyric;
        hello();
    } else {
        var_dump($song);
    }
}

hello(); // ???

Bom, também vai dar NULL pelo mesmo motivo. Assim que se encerra a primeira execução, a variável $lyric é eliminada. Por tanto, a referência se perde e o valor da variável estática volta à estaca zero.

Curtiram?! Eu gostei muito de aprender sobre variáveis estáticas. Abriu meus horizontes, hehe. No próximo artigo falaremos sobre variáveis variáveis, a coisa mais bugante e interessante que eu já vi nessa linguagem. Mesmo depois de conhecer o static, isso continua sendo meu assunto favorito. Pode esperar!

Inté!