Search code examples
phpconstantsvisibilityphp-7null-coalescing-operator

Why doesn't PHP's null coalescing operator (??) work on class constants with different visibilities?


Consider the example below. Class a has private const SOMETHING, but class b has protected const SOMETHING.

class a {
    private const SOMETHING = 'This is a!';

    public static function outputSomething() {
        return static::SOMETHING ?? self::SOMETHING;
    }
}

class b extends a {
    protected const SOMETHING = 'This is b!';
}

echo (new b())::outputSomething();

Output:

This is b!

But now if I comment out the definition for SOMETHING in class b, an error is thrown:

class a {
    private const SOMETHING = 'This is a!';

    public static function outputSomething() {
        return static::SOMETHING ?? self::SOMETHING;
    }
}

class b extends a {
    //protected const SOMETHING = 'This is b!';
}

echo (new b())::outputSomething();

Output:

Fatal error: Uncaught Error: Cannot access private const b::SOMETHING in {file}.php:7

However, changing the visibility from private const SOMETHING to protected const SOMETHING in class a fixes this.

class a {
    protected const SOMETHING = 'This is a!';

    public static function outputSomething() {
        return static::SOMETHING ?? self::SOMETHING;
    }
}

class b extends a {
    //protected const SOMETHING = 'This is b!';
}

echo (new b())::outputSomething();

Now the output is as expected:

This is a!

I don't understand why php is evaluating b::SOMETHING prior to applying the null coalescing operator, which according to the documentation:

The null coalescing operator (??) has been added as syntactic sugar for the common case of needing to use a ternary in conjunction with isset(). It returns its first operand if it exists and is not NULL; otherwise it returns its second operand.

Since b::SOMETHING is not set, why doesn't the first example work and a consistent visibility is required for the constant in the base class?


Solution

  • Thanks to @Devon and @Dormilich for their responses.

    TL;DR: You can't use the null coalescing operator (??) with constants. You have to use defined() instead.

    According to the documentation for the null coalescing operator (??):

    The null coalescing operator (??) has been added as syntactic sugar for the common case of needing to use a ternary in conjunction with isset(). It returns its first operand if it exists and is not NULL; otherwise it returns its second operand.

    Meaning that $x ?? $y is shorthand for isset($x) ? $x : $y. And this is where the problem lies, because the documentation for isset explicitly states:

    Warning: isset() only works with variables as passing anything else will result in a parse error. For checking if constants are set use the defined() function.

    That's what throws the fatal php error I describe in the question. Instead, a solution would be to do away with the null coalescing operator and replace it with defined():

    class a {
        private const SOMETHING = 'This is a!';
    
        public static function outputSomething() {
            return defined('static::SOMETHING') ? static::SOMETHING : self::SOMETHING;
        }
    }
    
    class b extends a {
        //protected const SOMETHING = 'This is b!';
    }
    
    echo (new b())::outputSomething();
    

    A second solution is to change how the code works in the first place. As @Devon correctly points out, the private visibility of a::SOMETHING prevents class b from seeing it, so b::SOMETHING is not defined. However, when the visibility of a::SOMETHING is changed to protected, class b can see it and b::SOMETHING references it. This code doesn't need the null coalescing operator at all, and can just use static::SOMETHING without any conditional:

    class a {
        protected const SOMETHING = 'This is a!';
    
        public static function outputSomething() {
            return static::SOMETHING;
        }
    }
    
    class b extends a {
        //protected const SOMETHING = 'This is b!';
    }
    
    echo (new b())::outputSomething();