How to solve the missing object properties in PHP?
Asked Answered
V

11

14

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?

Vivianviviana answered 21/8, 2013 at 15:29 Comment(5)
"However, one gets tired very soon when wrapping every accessor into this. Especially when chaining the objects, such as $data->people[0]->birthday->year." I don't understand. You can safely do isset($data->people[0]->birthday->year) even if $data is null without any side effects.Disraeli
This is a very good point, I was not aware of this. Thanks! (And still, is there any shorter version of $y = isset($x) ? $x : null;Vivianviviana
There isn't. If you look at other languages, PHP actually has the shortest form. In C per example, you'd have to check each sub-element for null manually. In Python, you'd have to surround it with a try..except block. If you really want shorter, you can always write a small wrapper function.Disraeli
Well, wrapping to try..except would help. The problem is not writing a single isset() but repeating it over and over, 1000x in my/your code.Vivianviviana
Have you ever considered the Reflection class?, I mean you can define a class as template and then use "reflect" to check your object.Roadway
N
6

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 :)

Necessary answered 31/8, 2013 at 7:41 Comment(0)
T
4

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.

Tanatanach answered 4/9, 2013 at 12:57 Comment(0)
D
3

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.

Dairyman answered 21/8, 2013 at 17:10 Comment(1)
Thanks for this. And +1 for default values and templates. I will be happy to hear other approaches as well.Vivianviviana
W
2

in PHP version 8

you can use Nullsafe operator as follow:

$res = $data?->people[0]?->birthday?->year;
Womenfolk answered 3/7, 2021 at 8:49 Comment(0)
A
1

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
Arian answered 3/9, 2013 at 15:33 Comment(0)
I
1

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;
Ison answered 4/9, 2013 at 15:7 Comment(2)
The problem here is nested objects. __get() should check if ($this->_obj->$key instanceof StdClass) and return return new self($this->_obj->$key); if true.Elouise
Also, returning null will cause $obj->user->birthday to fail if user doesn't exist. You can't, however, return a ProxyObj in its place because then $obj->user->birthdayBlah would return an object. __toString could help here, but wouldn't totally solve the problem.Elouise
D
1

You can decode the JSON object to an array:

$data = '{"name": "Pavel", "job": "programmer"}';
$jsonarray = json_decode($data, true);
$b = $jsonarray["birthday"];    // NULL
Duhamel answered 20/10, 2017 at 1:4 Comment(2)
Sidenote, My opinion is that this a great, simple answer when dealing with external data, but can be troublesome when dealing with internal data - particularly for nested objects. For example: Say you have a stdObject that has another stdObject as a property. You can safely decode both. But if that property was an instance of a class you couldn't decode without side effects of changing that instance. You may be missing data in your array or throw errors. However if you make an external request decoding to an array is safe because it was originally just serialized data to begin with.Doughman
As a real world example, this is something that can happen in WooCommerce. Accessing the cart will return an array of cart_items. Each cart_item is itself an array, with one of its fields being an instance of the WC_Product_Simple Object class. So changing the format of the cart or cart items will also change the format of the WC_Product_Simple Object which can cause issues....Doughman
S
0
function check($temp=null) {
 if(isset($temp))
 return $temp;

else
return null;
}

$b = check($data->birthday);
Stockpile answered 4/9, 2013 at 13:5 Comment(2)
Be careful about returning null - it can create side effects and is recommended to avoid by Uncle Bob.Necessary
This won't work because the error will raise before the function called.Lens
S
0

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

Stonefly answered 29/9, 2016 at 13:6 Comment(0)
R
0

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://mcmap.net/q/814288/-how-to-solve-the-missing-object-properties-in-php | Luca Rainone
Roberson answered 14/1, 2020 at 17:3 Comment(0)
J
0

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;
    }
}
Jiminez answered 19/12, 2020 at 7:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.