Question

This is a bit philosophical but I think many people encountered this problem. The goal is to access various (dynamically declared) properties in PHP and get rid of notices when they are not set.

Why not to __get?
That's good option if you can declare your own class, but not in case of stdClass, SimpleXML or similar. Extending them is not and option since you usually do not instantiate these classes directly, they are returned as a result of JSON/XML parsing.

Example:

$data = '{"name": "Pavel", "job": "programmer"}';
$object = json_decode($data);

We have simple stdClass object. The problems is obvious:

$b = $data->birthday;

The property is not defined and therefore a notice is raised:

PHP Notice:  Undefined property: stdClass::$birthday

This can happen very often if you consider that you get that object from parsing some JSON. The naive solution is obvious:

$b = isset($data->birthday) ? $data->birthday : null;

However, one gets tired very soon when wrapping every accessor into this. Especially when chaining the objects, such as $data->people[0]->birthday->year. Check whether people is set. Check if the first element is set. Check if birthday is set. Check if year is set. I feel a bit overchecked...

Question: Finally, my question is here.
What is the best approach to this issue? Silencing notices does not seem to be the best idea. And checking every property is difficult. I have seen some solutions such as Symfony property access but I think it is still too much boilerplate. Is there any simpler way? Either third party library, PHP setting, C extension, I don't care as far as it works... And what are the possible pitfalls?

Was it helpful?

Solution

If I understand correctly, you want to deal with 3rd party Objects, where you have no control, but your logic requires certain properties that may not be present on the Object. That means, the data you accepting are invalid (or should be declared invalid) for your logic. Then the burden of checking the validity goes into your validator. Which I hope you already have following best practices to deal with 3rd party data. :)

You can use your own validator or one by frameworks. A common way is to write a set of Rules that your data needs to obey in order to be valid.

Now inside your validator, whenever a rule is not obeyed, you throw an Exception describing the error and attaching Exception properties that carry the information you want to use. Later when you call your validator somewhere in your logic, you place it inside try {...} block and you catch() your Exceptions and deal with them, that is, write your special logic reserved for those exceptions. As general practice, if your logic becomes too large in a block, you want to "outsource" it as function. Quoting the great book by Robert Martin "Clean Code", highly recommended for any developer:

The first rule of function is that they should be small. The second is that they should be smaller than that.

I understand your frustration dealing with eternal issets and see as cause of the problem here that each time you need to write a handler dealing with that technical issue of this or that property not present. That technical issue is of very low level in your abstraction hierarchy, and in order to handle it properly, you have to go all the way up your abstraction chain to reach a higher step that has a meaning for your logic. It is always hard to jump between different levels of abstraction, especially far apart. It is also what makes your code hard to maintain and is recommended to avoid. Ideally your whole architecture is designed as a tree where Controllers sitting at its nodes only know about the edges going down from them.

For instance, coming back to your example, the question is -

  • Q - What is the meaning for your app of the situation that $data->birthday is missing?

The meaning will depend on what the current function throwing the Exception wants to achieve. That is a convenient place to handle your Exception.

Hope it helps :)

OTHER TIPS

One solution (I don't know if it's the better solution, but one possible solution) is to create a function like this:

function from_obj(&$type,$default = "") {
    return isset($type)? $type : $default;
}

then

$data   = '{"name": "Pavel", "job": "programmer"}';
$object = json_decode($data);

$name   = from_obj( $object->name      , "unknown");
$job    = from_obj( $object->job       , "unknown");
$skill  = from_obj( $object->skills[0] , "unknown");
$skills = from_obj( $object->skills    , Array());

echo "Your name is $name. You are a $job and your main skill is $skill";

if(count($skills) > 0 ) {
    echo "\n\nYour skills: " . implode(",",$skills);
}

I think it's convienent because you have at the top of your script what you want and what it should be (array, string, etc)

EDIT:

Another solution. You could create a Bridge class that extends ArrayObject:

class ObjectBridge extends ArrayObject{
    private $obj;
    public function __construct(&$obj) {
        $this->obj = $obj;
    }

    public function __get($a) {
        if(isset($this->obj->$a)) {
            return $this->obj->$a;
        }else {
            // return an empty object in order to prevent errors with chain call
            $tmp = new stdClass();
            return new ObjectBridge($tmp);
        }
    }
    public function __set($key,$value) {
        $this->obj->$key = $value;
    }
    public function __call($method,$args) {
        call_user_func_array(Array($this->obj,$method),$args);
    }
    public function __toString() {
        return "";
    }
}

$data   = '{"name": "Pavel", "job": "programmer"}';
$object = json_decode($data);

$bridge = new ObjectBridge($object);

echo "My name is {$bridge->name}, I have " . count($bridge->skills). " skills and {$bridge->donald->duck->is->paperinik}<br/>";  
// output: My name is Pavel, I have 0 skills and 
// (no notice, no warning)

// we can set a property
$bridge->skills = Array('php','javascript');

// output: My name is Pavel, my main skill is php
echo "My name is {$bridge->name}, my main skill is {$bridge->skills[0]}<br/>";


// available also on original object
echo $object->skills[0]; // output: php

Personally I would prefer the first solution. It's more clear and more safe.

Data formats which have optional fields are quite difficult to deal with. They're problematic in particular if you have third parties accessing or providing the data, since there rarely is enough documentation to comprehensively cover all causes for the fields to appear or disappear. And of course, the permutations tend to be harder to test, because coders won't instinctively realize that the fields may be there.

That's a long way of saying that if you can avoid having optional fields in your data, the best approach to dealing with missing object properties in PHP is to not have any missing object properties...

If the data you're dealing with is not up to you, then I'd look into forcing default values on all fields, perhaps via a helper function or some sort of crazy variation of the prototype pattern. You could build a data template, which contains default values for all fields of the data, and merge that with the real data.

However, if you do that, are you failing, unless? (Which is another programming philosophy to take into heart.) I suppose one could make the case that providing safe default parameters satisfies data validation for any missing fields. But particularly when dealing with third party data, you should exercise high level of paranoia against any field you're plastering with default values. It's too easy to just set it to null and -- in the process -- fail to understand why it was missing in the first place.

You should also ask what are you trying to achieve? Clarity? Safety? Stability? Minimal code duplication? These are all valid goals. Being tired? Less so. It suggests a lack disciprine, and a good programmer is always disciprined. Of course, I'll accept that people are less likely to do something, if they view it as a chore.

My point is, the answer to your question may differ depending on why it's being asked. Zero effort solution is probably not going to be available, so if you're only exchanging one menial programming task to another one, are you solving anything?

If you are looking for a systematic solution that will guarantee that the data is always in the format you have specified, leading to reduced number of logical tests in the code that processes that data, then perhaps what I suggested above will be of help. But it will not come without a cost and effort.

The best answers have been given, but here is a lazy one:

$data = '{"name": "Pavel", "job": "programmer"}';
$object = json_decode($data);
if(
    //...check mandatory properties: !isset($object->...)&&
    ){
    //error
}
error_reporting(E_ALL^E_NOTICE);//Yes you're right, not the best idea...
$b = $object->birthday?:'0000-00-00';//thanks Elvis (php>5.3)
//Notice that if your default value is "null", you can just do $b = $object->birthday;
//assign other vars here
error_reporting(E_ALL);
//Your code

Use a Proxy object - it will add just one tiny class and one line per object instantiation to use it.

class ProxyObj {
   protected $obj;
   public function __construct( $obj ) {
      $this->_obj = $obj;
   }
   public function __get($key) {
     if (isset($this->_obj->$key)) {
        return $this->_obj->$key;
     }
     return null;
   }
   public function __set($key, $value) {
      $this->_obj->$key = $value;
   }
}

$proxy = new ProxyObj(json_decode($data));
$b = $proxy->birthday;

You can decode the JSON object to an array:

$data = '{"name": "Pavel", "job": "programmer"}';
$jsonarray = json_decode($data, true);
$b = $jsonarray["birthday"];    // NULL

in PHP version 8

you can use Nullsafe operator as follow:

$res = $data?->people[0]?->birthday?->year;
function check($temp=null) {
 if(isset($temp))
 return $temp;

else
return null;
}

$b = check($data->birthday);

I've hit this problem, mainly from getting json data from a nosql backed api that by design has inconsistent structures, eg if a user has an address you'll get $user->address otherwise the address key just isn't there. Rather than put tons of issets in my templates I wrote this class...

class GracefulData
{
    private $_path;
    public function __construct($d=null,$p='')
    {
        $this->_path=$p;
        if($d){
            foreach(get_object_vars($d) as $property => $value) {
                if(is_object($d->$property)){
                    $this->$property = new GracefulData($d->$property,$this->_path . '->' . $property);
                }else{
                    $this->$property = $value;
                }
            }
        }
    }
    public function __get($property) {
        return new GracefulData(null,$this->_path . '->' . $property);
    }
    public function __toString() {
        Log::info('GracefulData: Invalid property accessed' . $this->_path);
        return '';
    }
}

and then instantiate it like so

$user = new GracefulData($response->body);

It will gracefully handle nested calls to existing and non existing properties. What it can't handle though is if you access a child of an existing non-object property eg

$user->firstName->something

Lots of good answers here, I consider @Luca 's answer as one of the best - I extended his a little so that I could pass in either an array or object and have it create an easy to use object. Here's mine:

<?php

namespace App\Libraries;

use ArrayObject;
use stdClass;

class SoftObject extends ArrayObject{
    private $obj;

    public function __construct($data) {
        if(is_object($data)){
            $this->obj = $data;
        }elseif(is_array($data)){
            // turn it into a multidimensional object
            $this->obj = json_decode(json_encode($data), false);
        }
    }

    public function __get($a) {
        if(isset($this->obj->$a)) {
            return $this->obj->$a;
        }else {
            // return an empty object in order to prevent errors with chain call
            $tmp = new stdClass();
            return new SoftObject($tmp);
        }
    }

    public function __set($key, $value) {
        $this->obj->$key = $value;
    }

    public function __call($method, $args) {
        call_user_func_array(Array($this->obj,$method),$args);
    }

    public function __toString() {
        return "";
    }
}

// attributions: https://stackoverflow.com/questions/18361594/how-to-solve-the-missing-object-properties-in-php | Luca Rainone

I have written a helper function for multilevel chaining, for example, let's say you want to do something like $obj1->obj2->obj3->obj4, and my helper will return empty string if one of the tiers is not defined or null

class MyUtils
{
    // for $obj1->obj2->obj3: MyUtils::nested($obj1, 'obj2', 'obj3')
    // returns '' if some of tiers is null
    public static function nested($obj1, ...$tiers)
    {
        if (!isset($obj1)) return '';
        $a = $obj1;
        for($i = 0; $i < count($tiers); $i++){
            if (isset($a->{$tiers[$i]})) {
                $a = $a->{$tiers[$i]};
            } else {
                return '';
            }
        }
        return $a;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top