Why does empty($object->arrayProperty) result in true while array elements are set and filled? [duplicate]

Without any other changes the following three lines in the same spot result in (for me) incomprehensible results. The first result in my head does not match to what the other lines return:

var_dump(empty($this->table)); //results in "bool(true)"
var_dump($this->table); //results in an array of 75 elements
var_dump(empty($this->table[0])); //results in "bool(false)"

First of all:

  • My question is NOT what to do, to make my code work. Many other topics explain this very good and I don’t want duplication here.

  • My question is NOT why PHP has an error here. As PHP is what it is and it is the behaviour that it is.

  • As !isset() for non-false elements is an equivalent to empty() in this case it obviously has same results. I could have interchanged “…does empty($array) result in true…” inside the title with “…why does an array not exist…”. I decided for more specific wording.

I search for the explanation that leads to this (at least for me) confusing result. I have a guess that it might be a mechanic related to __get() or ob_get_clean() but I have no real clue how these work internally.

The Environment:

  1. I have a method inside an object that extracts an array out of a csv-file. (changed names)
   $object = new Class();
   
   empty($object->method()); //results in false - as I would expect as an array with 75 elements is returned
  1. I wrap this together with other variables of different types into a params-array.
   $params =['table' => $object->method(),...];
   
   empty($params['table']); //still results in false here
  1. Via a View object I pass the params-array to a view
   return (new View('transactions/show',$params))->render();

For the passing inside the View object I use the __get magic method

   public function __get(string $name) {
      return $this->params[$name] ?? null;
   }

The render function includes the view with output buffering

   public function render() {
       ob_start();
       include VIEW_PATH . DIRECTORY_SEPARATOR . $this->view . '.php';
       return ob_get_clean();
   }
  1. Inside the view (‘transactions/show’) I am able to access the params by $this->variable and as mentioned above the array is accessible and working fine but the result “true” of the empty($this->table) confuses me.

All my testing lead me to the point that step 3. is responsible for the results I experience inside step 4.

To show a reproducible version hereafter complete files:

Controller class:

<?php

declare(strict_types=1);

namespace AppController;

use AppModelDemoClass;
use AppView;

class Demo {
    public function index() {
        $object = new DemoClass();

        empty($object->demoMethod()); //results in false - as I would expect as an array with 75 elements is returned

        $params =['table' => $object->demoMethod()];

        empty($params['table']); //still results in false here

        return (new View('demo/show',$params))->render();
    }
}

The Model class (here only simulating to get an array of arrays from a source (csv, Database, whereever)

<?php

declare(strict_types=1);

namespace AppModel;

class DemoClass {
    public function demoMethod() {
        $array = [
            [1, 2, 3],
            [4, 5, 6],
            [7, 8, 9],
        ];
        return $array;
    }
}

the View class (only the needed excerpt):

<?php

declare(strict_types=1);

namespace App;

use FrameworkExceptionViewNotFoundException;

class View {

    public function __construct(protected string $view, protected array $params =[]) {
    }

    public static function make(string $view, array $params =[]): static {
        return new static($view, $params);
    }
    
    public function render(): string {
        $viewPath = VIEW_PATH . DIRECTORY_SEPARATOR . $this->view . static::PHP_TYPE;
        
        if(!file_exists($viewPath)) {
            throw new ViewNotFoundException();
        }
        ob_start();
        include $viewPath;
        return (string) ob_get_clean();
    }
    
    public function __get(string $name) {
        return $this->params[$name] ?? null;
    }

    public function __toString():string {
        return $this->render();
    }
}

The view/html – kept it short without HTML header (it makes no difference here)

<?= var_dump(empty($this->table)); //results in "bool(true)" ?>
</br>

<?= var_dump($this->table); //results in an array of 75 elements ?>
</br>

<?= var_dump(empty($this->table[0])); //results in "bool(false)" ?>
</br>

with calling the Demo->index() (e.g. via Router) the result is:

bool(true)

array(3) { 
    [0]=> array(3) { 
        [0]=> int(1) 
        [1]=> int(2) 
        [2]=> int(3) 
    } 
    [1]=> array(3) { 
        [0]=> int(4) 
        [1]=> int(5) 
        [2]=> int(6) 
    } 
    [2]=> array(3) { 
        [0]=> int(7) 
        [1]=> int(8) 
        [2]=> int(9) 
    } 
}

bool(false)