Código PHP bugado: os 10 erros mais comuns que os desenvolvedores PHP cometem

Publicados: 2022-03-11

O PHP torna relativamente fácil construir um sistema baseado na web, o que é grande parte do motivo de sua popularidade. Mas, apesar de sua facilidade de uso, o PHP evoluiu para uma linguagem bastante sofisticada com muitos frameworks, nuances e sutilezas que podem incomodar os desenvolvedores, levando a horas de depuração de puxar os cabelos. Este artigo destaca dez dos erros mais comuns que os desenvolvedores PHP precisam tomar cuidado.

Erro comum nº 1: deixar referências de matriz pendentes após loops foreach

Não sabe como usar loops foreach em PHP? O uso de referências em loops foreach pode ser útil se você quiser operar em cada elemento da matriz sobre a qual está iterando. Por exemplo:

 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)

O problema é que, se você não tomar cuidado, isso também pode ter alguns efeitos colaterais e consequências indesejáveis. Especificamente, no exemplo acima, após a execução do código, $value permanecerá no escopo e manterá uma referência ao último elemento da matriz. As operações subsequentes envolvendo $value podem, portanto, involuntariamente acabar modificando o último elemento da matriz.

A principal coisa a lembrar é que foreach não cria um escopo. Assim, $value no exemplo acima é uma referência dentro do escopo superior do script. Em cada iteração foreach define a referência para apontar para o próximo elemento de $array . Após a conclusão do loop, portanto, $value ainda aponta para o último elemento de $array e permanece no escopo.

Aqui está um exemplo do tipo de bugs evasivos e confusos que isso pode levar:

 $array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (ie, copy) echo implode(',', $array), "\n";

O código acima produzirá o seguinte:

 1,2,3 1,2,3 1,2,2

Não, isso não é um erro de digitação. O último valor na última linha é de fato um 2, não um 3.

Por quê?

Depois de passar pelo primeiro loop foreach , $array permanece inalterado, mas, como explicado acima, $value é deixado como uma referência pendente para o último elemento em $array (já que o loop foreach acessou $value por referência ).

Como resultado, quando passamos pelo segundo loop foreach , “coisas estranhas” parecem acontecer. Especificamente, como $value agora está sendo acessado por value (ou seja, por copy ), foreach copia cada elemento $array sequencial em $value em cada etapa do loop. Como resultado, veja o que acontece durante cada etapa do segundo loop foreach :

  • Passo 1: Copia $array[0] (ou seja, “1”) em $value (que é uma referência a $array[2] ), então $array[2] agora é igual a 1. Então $array agora contém [1, 2, 1].
  • Passo 2: Copia $array[1] (ou seja, “2”) em $value (que é uma referência a $array[2] ), então $array[2] agora é igual a 2. Então $array agora contém [1, 2, 2].
  • Passo 3: Copia $array[2] (que agora é igual a “2”) em $value (que é uma referência a $array[2] ), então $array[2] ainda é igual a 2. Então $array agora contém [1 , 2, 2].

Para ainda obter o benefício de usar referências em loops foreach sem correr o risco desses tipos de problemas, chame unset() na variável, imediatamente após o loop foreach , para remover a referência; por exemplo:

 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]

Erro comum nº 2: comportamento isset() incorreto

Apesar do nome, isset() não apenas retorna false se um item não existir, mas também retorna false para valores null .

Esse comportamento é mais problemático do que pode parecer à primeira vista e é uma fonte comum de problemas.

Considere o seguinte:

 $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }

O autor deste código presumivelmente queria verificar se keyShouldBeSet foi definido em $data . Mas, conforme discutido, isset($data['keyShouldBeSet']) também retornará false se $data['keyShouldBeSet'] foi definido, mas foi definido como null . Portanto, a lógica acima é falha.

Aqui está outro exemplo:

 if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }

O código acima assume que se $_POST['active'] retornar true , então postData será necessariamente definido e, portanto, isset($postData) retornará true . Então, inversamente, o código acima assume que a única maneira que isset($postData) retornará false é se $_POST['active'] retornar false também.

Não.

Conforme explicado, isset($postData) também retornará false se $postData foi definido como null . Portanto, é possível que isset($postData) retorne false mesmo que $_POST['active'] retorne true . Então, novamente, a lógica acima é falha.

E a propósito, como um ponto lateral, se a intenção no código acima era realmente verificar novamente se $_POST['active'] retornou true, confiar em isset() para isso foi uma decisão de codificação ruim em qualquer caso. Em vez disso, teria sido melhor verificar novamente $_POST['active'] ; ou seja:

 if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }

Para casos, porém, onde é importante verificar se uma variável foi realmente definida (ou seja, para distinguir entre uma variável que não foi definida e uma variável que foi definida como null ), o método array_key_exists() é muito mais robusto solução.

Por exemplo, poderíamos reescrever o primeiro dos dois exemplos acima da seguinte forma:

 $data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }

Além disso, combinando array_key_exists() com get_defined_vars() , podemos verificar com segurança se uma variável dentro do escopo atual foi definida ou não:

 if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }

Erro comum nº 3: confusão sobre retornar por referência versus valor

Considere este trecho de código:

 class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

Se você executar o código acima, obterá o seguinte:

 PHP Notice: Undefined index: test in /path/to/my/script.php on line 21

O que há de errado?

O problema é que o código acima confunde o retorno de arrays por referência com o retorno de arrays por valor. A menos que você diga explicitamente ao PHP para retornar um array por referência (ou seja, usando & ), o PHP por padrão retornará o array “por valor”. Isso significa que uma cópia do array será retornada e, portanto, a função chamada e o chamador não acessarão a mesma instância do array.

Portanto, a chamada acima para getValues() retorna uma cópia do array $values ​​em vez de uma referência a ele. Com isso em mente, vamos revisitar as duas linhas principais do exemplo acima:

 // getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the "undefined index" message). echo $config->getValues()['test'];

Uma possível correção seria salvar a primeira cópia do array $values ​​retornado por getValues() e então operar nessa cópia subseqüentemente; por exemplo:

 $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];

Esse código funcionará bem (ou seja, ele produzirá test sem gerar nenhuma mensagem de “índice indefinido”), mas dependendo do que você está tentando realizar, essa abordagem pode ou não ser adequada. Em particular, o código acima não modificará o array $values ​​original. Portanto, se você quiser que suas modificações (como adicionar um elemento 'test') afetem o array original, você precisaria modificar a função getValues() para retornar uma referência ao próprio array $values . Isso é feito adicionando um & antes do nome da função, indicando assim que ela deve retornar uma referência; ou seja:

 class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

A saída disso será test , conforme o esperado.

Mas para tornar as coisas mais confusas, considere o seguinte trecho de código:

 class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

Se você adivinhou que isso resultaria no mesmo erro de “índice indefinido” do nosso exemplo de array anterior, você estava errado. Na verdade, este código funcionará muito bem. A razão é que, ao contrário dos arrays, o PHP sempre passa objetos por referência . ( ArrayObject é um objeto SPL, que imita totalmente o uso de arrays, mas funciona como um objeto.)

Como esses exemplos demonstram, nem sempre é totalmente óbvio em PHP se você está lidando com uma cópia ou uma referência. Portanto, é essencial entender esses comportamentos padrão (ou seja, variáveis ​​e arrays são passados ​​por valor; objetos são passados ​​por referência) e também verificar cuidadosamente a documentação da API para a função que você está chamando para ver se ela está retornando um valor, um cópia de uma matriz, uma referência a uma matriz ou uma referência a um objeto.

Dito isso, é importante observar que a prática de retornar uma referência a um array ou a um ArrayObject geralmente é algo que deve ser evitado, pois fornece ao chamador a capacidade de modificar os dados privados da instância. Isso “voa na cara” do encapsulamento. Em vez disso, é melhor usar “getters” e “setters” de estilo antigo, por exemplo:

 class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'

Essa abordagem dá ao chamador a capacidade de definir ou obter qualquer valor no array sem fornecer acesso público ao próprio array $values , que seria privado.

Erro comum nº 4: realizando consultas em um loop

Não é incomum encontrar algo assim se o seu PHP não estiver funcionando:

 $models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }

Embora possa não haver absolutamente nada de errado aqui, mas se você seguir a lógica do código, poderá descobrir que a chamada inocente acima para $valueRepository->findByValue() resulta em uma consulta de algum tipo, como:

 $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);

Como resultado, cada iteração do loop acima resultaria em uma consulta separada ao banco de dados. Portanto, se, por exemplo, você fornecesse um array de 1.000 valores ao loop, ele geraria 1.000 consultas separadas para o recurso! Se um script desse tipo for chamado em vários threads, ele poderá levar o sistema a uma parada brusca.

Portanto, é crucial reconhecer quando as consultas estão sendo feitas pelo seu código e, sempre que possível, reunir os valores e depois executar uma consulta para buscar todos os resultados.

Um exemplo de um lugar bastante comum para encontrar consultas sendo feitas de forma ineficiente (ou seja, em um loop) é quando um formulário é postado com uma lista de valores (IDs, por exemplo). Em seguida, para recuperar os dados de registro completos para cada um dos IDs, o código percorrerá a matriz e fará uma consulta SQL separada para cada ID. Isso geralmente será algo assim:

 $data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }

Mas a mesma coisa pode ser realizada com muito mais eficiência em uma única consulta SQL da seguinte maneira:

 $data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }

Portanto, é crucial reconhecer quando as consultas estão sendo feitas, direta ou indiretamente, pelo seu código. Sempre que possível, reúna os valores e execute uma consulta para buscar todos os resultados. No entanto, deve-se ter cautela também, o que nos leva ao nosso próximo erro comum do PHP…

Erro comum nº 5: falhas e ineficiências no uso da memória

Embora buscar muitos registros de uma vez seja definitivamente mais eficiente do que executar uma única consulta para cada linha a ser buscada, tal abordagem pode levar a uma condição de “falta de memória” no libmysqlclient ao usar a extensão mysql do PHP.

Para demonstrar, vamos dar uma olhada em uma caixa de teste com recursos limitados (512 MB de RAM), MySQL e php-cli .

Vamos inicializar uma tabela de banco de dados como esta:

 // connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }

OK, agora vamos verificar o uso de recursos:

 // connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";

Saída:

 Before: 224704 Limit 1: 224704 Limit 10000: 224704

Frio. Parece que a consulta é gerenciada com segurança internamente em termos de recursos.

Só para ter certeza, porém, vamos aumentar o limite mais uma vez e configurá-lo para 100.000. Uh-oh. Quando fazemos isso, obtemos:

 PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11

O que aconteceu?

O problema aqui é como o módulo mysql do PHP funciona. É realmente apenas um proxy para libmysqlclient , que faz o trabalho sujo. Quando uma parte dos dados é selecionada, ela vai diretamente para a memória. Como essa memória não é gerenciada pelo gerenciador do PHP, memory_get_peak_usage() não mostrará nenhum aumento na utilização de recursos à medida que aumentamos o limite em nossa consulta. Isso leva a problemas como o demonstrado acima, onde somos levados à complacência pensando que nosso gerenciamento de memória está bom. Mas, na realidade, nosso gerenciamento de memória é seriamente falho e podemos ter problemas como o mostrado acima.

Você pode pelo menos evitar o headfake acima (embora ele não melhore sua utilização de memória) usando o módulo mysqlnd . mysqlnd é compilado como uma extensão nativa do PHP e usa o gerenciador de memória do PHP.

Portanto, se executarmos o teste acima usando mysqlnd em vez de mysql , obteremos uma imagem muito mais realista de nossa utilização de memória:

 Before: 232048 Limit 1: 324952 Limit 10000: 32572912

E é ainda pior do que isso, a propósito. De acordo com a documentação do PHP, o mysql usa duas vezes mais recursos que o mysqlnd para armazenar dados, então o script original usando o mysql realmente usava ainda mais memória do que o mostrado aqui (aproximadamente o dobro).

Para evitar esses problemas, considere limitar o tamanho de suas consultas e usar um loop com um pequeno número de iterações; por exemplo:

 $totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }

Quando consideramos esse erro PHP e o erro #4 acima, percebemos que há um equilíbrio saudável que seu código idealmente precisa alcançar entre, por um lado, ter suas consultas muito granulares e repetitivas, versus ter cada um de seus consultas individuais sejam muito grandes. Como acontece com a maioria das coisas na vida, o equilíbrio é necessário; qualquer um dos extremos não é bom e pode causar problemas com o PHP não funcionando corretamente.

Erro comum nº 6: ignorando problemas de Unicode/UTF-8

De certa forma, isso é realmente mais um problema no próprio PHP do que algo que você encontraria ao depurar o PHP, mas nunca foi abordado adequadamente. O núcleo do PHP 6 deveria ser compatível com Unicode, mas isso foi suspenso quando o desenvolvimento do PHP 6 foi suspenso em 2010.

Mas isso de forma alguma absolve o desenvolvedor de entregar UTF-8 corretamente e evitar a suposição errônea de que todas as strings serão necessariamente “ASCII simples”. O código que falha em lidar adequadamente com strings não ASCII é notório por introduzir bugs heisenbug no seu código. Mesmo simples chamadas strlen($_POST['name']) podem causar problemas se alguém com um sobrenome como “Schrodinger” tentar se registrar em seu sistema.

Aqui está uma pequena lista de verificação para evitar esses problemas em seu código:

  • Se você não sabe muito sobre Unicode e UTF-8, deve pelo menos aprender o básico. Há uma ótima cartilha aqui.
  • Certifique-se de sempre usar as funções mb_* em vez das funções de string antigas (certifique-se de que a extensão “multibyte” esteja incluída em sua compilação PHP).
  • Certifique-se de que seu banco de dados e tabelas estejam configurados para usar Unicode (muitas compilações do MySQL ainda usam latin1 por padrão).
  • Lembre-se de que json_encode() converte símbolos não ASCII (por exemplo, “Schrodinger” se torna “Schr\u00f6dinger”) mas serialize() não .
  • Certifique-se de que seus arquivos de código PHP também sejam codificados em UTF-8 para evitar colisões ao concatenar strings com constantes de string codificadas ou configuradas.

Um recurso particularmente valioso a esse respeito é a postagem UTF-8 Primer for PHP and MySQL de Francisco Claria neste blog.

Erro comum nº 7: Assumir que $_POST sempre conterá seus dados POST

Apesar do nome, o array $_POST nem sempre conterá seus dados POST e pode ser facilmente encontrado vazio. Para entender isso, vamos dar uma olhada em um exemplo. Suponha que fazemos uma solicitação de servidor com uma chamada jQuery.ajax() da seguinte forma:

 // js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });

(Aliás, observe o contentType: 'application/json' aqui. Enviamos dados como JSON, que é bastante popular para APIs. É o padrão, por exemplo, para postagem no serviço AngularJS $http .)

No lado do servidor do nosso exemplo, simplesmente despejamos o array $_POST :

 // php var_dump($_POST);

Surpreendentemente, o resultado será:

 array(0) { }

Por quê? O que aconteceu com nossa string JSON {a: 'a', b: 'b'} ?

A resposta é que o PHP só analisa uma carga POST automaticamente quando tem um tipo de conteúdo de application/x-www-form-urlencoded ou multipart/form-data . As razões para isso são históricas — esses dois tipos de conteúdo eram essencialmente os únicos usados ​​anos atrás quando o $_POST do PHP foi implementado. Assim, com qualquer outro tipo de conteúdo (mesmo aqueles que são bastante populares hoje em dia, como application/json ), o PHP não carrega automaticamente a carga POST.

Como $_POST é uma superglobal, se o substituirmos uma vez (de preferência no início de nosso script), o valor modificado (ou seja, incluindo a carga útil do POST) será referenciado em todo o nosso código. Isso é importante, pois $_POST é comumente usado por frameworks PHP e quase todos os scripts personalizados para extrair e transformar dados de solicitação.

Assim, por exemplo, ao processar uma carga POST com um tipo de conteúdo application/json , precisamos analisar manualmente o conteúdo da solicitação (ou seja, decodificar os dados JSON) e substituir a variável $_POST , da seguinte maneira:

 // php $_POST = json_decode(file_get_contents('php://input'), true);

Então, quando despejamos o array $_POST , vemos que ele inclui corretamente o payload POST; por exemplo:

 array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

Erro comum nº 8: Pensar que o PHP suporta um tipo de dados de caractere

Veja este exemplo de código e tente adivinhar o que será impresso:

 for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }

Se você respondeu de 'a' a 'z', pode se surpreender ao saber que estava errado.

Sim, ele imprimirá de 'a' a 'z', mas também imprimirá 'aa' a 'yz'. Vamos ver por quê.

No PHP não existe o tipo de dados char ; apenas a string está disponível. Com isso em mente, incrementar a string z no PHP produz aa :

 php> $c = 'z'; echo ++$c . "\n"; aa

No entanto, para confundir ainda mais as coisas, aa é lexicograficamente menor que z :

 php> var_export((boolean)('aa' < 'z')) . "\n"; true

É por isso que o código de exemplo apresentado acima imprime as letras a a z , mas também imprime de aa a yz . Ele para quando atinge za , que é o primeiro valor que encontra que é “maior que” z :

 php> var_export((boolean)('za' < 'z')) . "\n"; false

Sendo esse o caso, aqui está uma maneira de percorrer corretamente os valores de 'a' a 'z' no PHP:

 for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }

Ou alternativamente:

 $letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }

Erro comum nº 9: Ignorando os padrões de codificação

Embora ignorar os padrões de codificação não leve diretamente à necessidade de depurar o código PHP, ainda é provavelmente uma das coisas mais importantes a serem discutidas aqui.

Ignorar os padrões de codificação pode causar uma série de problemas em um projeto. Na melhor das hipóteses, resulta em um código inconsistente (já que cada desenvolvedor está “fazendo suas próprias coisas”). Mas, na pior das hipóteses, produz código PHP que não funciona ou pode ser difícil (às vezes quase impossível) de navegar, tornando extremamente difícil depurar, aprimorar, manter. E isso significa produtividade reduzida para sua equipe, incluindo muito esforço desperdiçado (ou pelo menos desnecessário).

Felizmente para os desenvolvedores PHP, existe a Recomendação de Padrões do PHP (PSR), composta pelos cinco padrões a seguir:

  • PSR-0: Padrão de carregamento automático
  • PSR-1: Padrão Básico de Codificação
  • PSR-2: Guia de estilo de codificação
  • PSR-3: Interface do registrador
  • PSR-4: Autoloader

O PSR foi criado originalmente com base em insumos de mantenedores das plataformas mais reconhecidas do mercado. Zend, Drupal, Symfony, Joomla e outros contribuíram para esses padrões e agora os estão seguindo. Até o PEAR, que tentou ser um padrão por anos antes disso, participa do PSR agora.

De certa forma, quase não importa qual é o seu padrão de codificação, desde que você concorde com um padrão e o cumpra, mas seguir o PSR geralmente é uma boa ideia, a menos que você tenha algum motivo convincente em seu projeto para fazer o contrário. . Cada vez mais equipes e projetos estão em conformidade com o PSR. O Tt é definitivamente reconhecido neste momento como “o” padrão pela maioria dos desenvolvedores PHP, portanto, usá-lo ajudará a garantir que os novos desenvolvedores estejam familiarizados e confortáveis ​​com seu padrão de codificação quando ingressarem em sua equipe.

Erro comum nº 10: uso incorreto de empty()

Alguns desenvolvedores PHP gostam de usar empty() para verificações booleanas para quase tudo. Há casos, porém, em que isso pode levar à confusão.

Primeiro, vamos voltar para arrays e instâncias de ArrayObject (que imitam arrays). Dada a sua semelhança, é fácil supor que arrays e instâncias ArrayObject se comportarão de forma idêntica. Isso prova, no entanto, ser uma suposição perigosa. Por exemplo, no PHP 5.0:

 // PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?

E para piorar ainda mais, os resultados teriam sido diferentes antes do PHP 5.0:

 // Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)

Esta abordagem é, infelizmente, bastante popular. Por exemplo, esta é a forma como Zend\Db\TableGateway do Zend Framework 2 retorna dados ao chamar current() no TableGateway::select() result como o documento sugere. O desenvolvedor pode facilmente se tornar vítima desse erro com esses dados.

Para evitar esses problemas, a melhor abordagem para verificar estruturas de array vazias é usar count() :

 // Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)

E, incidentalmente, como o PHP converte 0 em false , count() também pode ser usado dentro das condições if () para verificar arrays vazios. Também vale a pena notar que, em PHP, count() é uma complexidade constante (operação O(1) ) em arrays, o que deixa ainda mais claro que é a escolha certa.

Outro exemplo em que empty() pode ser perigoso é ao combiná-lo com a função de classe mágica __get() . Vamos definir duas classes e ter uma propriedade de test em ambas.

Primeiro vamos definir uma classe Regular que inclui test como uma propriedade normal:

 class Regular { public $test = 'value'; }

Então vamos definir uma classe Magic que usa o operador magic __get() para acessar sua propriedade de test :

 class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }

OK, agora vamos ver o que acontece quando tentamos acessar a propriedade test de cada uma dessas classes:

 $regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"

Bem até agora.

Mas agora vamos ver o que acontece quando chamamos empty() em cada um deles:

 var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)

ECA. Portanto, se confiarmos em empty() , podemos ser enganados ao acreditar que a propriedade test de $magic está vazia, enquanto na realidade está definida como 'value' .

Infelizmente, se uma classe usa a função mágica __get() para recuperar o valor de uma propriedade, não há uma maneira infalível de verificar se esse valor de propriedade está vazio ou não. Fora do escopo da classe, você pode realmente apenas verificar se um valor null será retornado, e isso não significa necessariamente que a chave correspondente não esteja definida, pois na verdade ela poderia ter sido definida como null .

Por outro lado, se tentarmos fazer referência a uma propriedade inexistente de uma instância da classe Regular , receberemos um aviso semelhante ao seguinte:

 Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0

Portanto, o ponto principal aqui é que o método empty() deve ser usado com cuidado, pois pode levar a resultados confusos – ou até potencialmente enganosos – se não for cuidadoso.

Embrulhar

A facilidade de uso do PHP pode levar os desenvolvedores a uma falsa sensação de conforto, deixando-os vulneráveis ​​a longas depurações do PHP devido a algumas das nuances e idiossincrasias da linguagem. Isso pode resultar em PHP não funcionar e problemas como os descritos aqui.

A linguagem PHP evoluiu significativamente ao longo de seus 20 anos de história. Familiarizar-se com suas sutilezas é um esforço que vale a pena, pois ajudará a garantir que o software que você produz seja mais escalável, robusto e de fácil manutenção.