Aloiso Junior
Aloiso Junior

Reputation: 451

Null Safe Operator for PHP 7.x or Earlier

PHP 8 has introduced an amazing code fallback tool, Null Safe Operator, eg:

$country = $session?->user?->getAddress()?->country;

It prevents you create a lot of comparisons of all whole object tree, Null Coalesce Operator not plays well here (PHP 7.x or Earlier) beacuse above code has an method which will throw an exception because their main class is null. Here, Null Safe Operator prevents an exception.

Well, there are some hack method to emulate this behaviour into earlier versions of PHP (<= 7.X)?

Fallback to some generic class with magic methods where ever returns null can be handful.

Upvotes: 1

Views: 677

Answers (4)

Lajos Arpad
Lajos Arpad

Reputation: 76454

I don't know of such a tool in existence, but you can implement one yourself.

Like (untested)

function handleNullSafe($root, $chain, $default) {
    if (!$root) {
        return $default;
    } else {
        $element = $root;
        for ($index = 0; $index < count($chain); $index++) {
            $current = $chain[$index];
            //Data member
            if (!is_array($current)) {
                if ((!$element) || (!isset($element->{$chain[$index]}))) {
                    return $default;
                } else {
                    $element = $element->{$chain[$index]};
                }
            //Array or function
            } else {
                $current = $chain[$index][0]; //The name
                $type = $chain[$index][1]; //The type
                $params = $chain[$index][2]; //The params or indexes
                if ($type === 'array') {
                    //Get the member
                    $element = $element->{$current};
                    foreach ($params as $param) {
                        //exists
                        if (is_array($element) && isset($element[$param])) {
                            $element = $element[$param];
                        //does not exist
                        } else {
                            return $default;
                        }
                    }
                //function
                } else {
                    //null or not an object
                    if ((!$element) || (!is_object($element))) {
                        return $default;
                    } else {
                        $element = call_user_func_array(array($element, $current), $params);
                    }
                }
            }
        }
        return $element;
    }
}

The idea is that we have a root and a chain of items. In our chain we may have data members, arrays indexed or functions.

So handleNullSafe is to be called by passing whatever the $root may be, possibly passing something like $root ?? null if we are unsure about $root being a properly initialized value. $chain is an array of elements. If it's a simple data-member, then we pass its name, otherwise we pass an array for it, specifying the 1. name, 2. type (array or function), 3. parameters/indexes. So your

$country = $session?->user?->getAddress()?->country;

would become

$country = handleNullSafe(
    $session ?? null,
    'user'
    ['getAddress', 'function', [/*here you could pass the parameters, in this case it's empty, as you did not pass anything to getAddress*/]],
    'country'
);

I admit, I did not test this code, but I'm pretty sure that the idea is correct, please do let me know if there is anything wrong, typos, bugs, anything.

Upvotes: 0

rusanovski
rusanovski

Reputation: 11

You might be interested in Error Control Operator (@ at sign).

Your expression turns to:

$country = @$session->user->getAddress()->country;

If any property in path is null or is causing error, the result will remain null.
Exceptions will not be triggered when expression marked with @.
So be careful of using it because it might cover an important misbehavior.

Upvotes: 0

Maxim Krizhanovsky
Maxim Krizhanovsky

Reputation: 26699

To emulate null safe operator, you can take inspiration from the option type. The idea is simple - you wrap the value in an object, as you suggested, and have a magic method handling. Now, the magic method will either return $this - e.g. the same Option instance, if this is already a null, or call the method and wrap the result in an Option, to allow further chaining.

The challenge you face with PHP will be where to terminate, e.g. where to return the original value, and not the wrapper. If you can afford an explicit method call at the end of the chain, it becomes straightforward.

It would look something like (not tested, written for illustrative purposes)

class Option {
    protected $value;

    public function __construct($value)
    {
        $this->value = $value;
    }

    public function __call($methodName, $args) {
        if (is_null($this->value)) {
            return $this;
        }
        return new Option($this->value->$methodName($args));
    }

    public function __get($propertyName) {
        if (is_null($this->value)) {
            return $this;
        }
        return new Option($this->value->$propertyName);
    }

    public function get() {
        return $this->value;
    }
}

So you will do:

$country = new Option($session)->user->getAddress()->country->get();

Upvotes: 1

Aloiso Junior
Aloiso Junior

Reputation: 451

We can create a black holed class to instead of throw an exception return Null if we call an undefined method of a generic class which will acts as our fallback.

    <?php
    // _7 - for brevity write and 7 is closer to question mark ;)
    class _7  {
        public function __call($method, $args) return null;
    }


$myDate = (\DateTime::createFromFormat('d/m/Y','05/04/1989') ?: new _7)->format('Y-m-d') ?? 'Bad date';
//'1989-04-05' - because expected date format is provided to Datetime 

$myDate = (\DateTime::createFromFormat('d/m/Y','1989') ?: new _7)->format('Y-m-d') ?? 'Bad date';
//'Bad date' - Very cool! No exceptions here, successfull fallback;

Important! This approach only works with PHP >= 7.0, i will collect info to work with 5.x soon as possible.

Upvotes: 0

Related Questions