6

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?

Anton
  • 3,998
  • 25
  • 40
  • The difference between self and static is described in https://stackoverflow.com/questions/5197300/new-self-vs-new-static – Nigel Ren Jun 21 '18 at 14:55
  • Another problem is that `static::SOMETHING` cannot be checked by isset() (Fatal error: cannot use isset() on the result of an expression) – Dormilich Jun 21 '18 at 15:01
  • Why would you set SOMETHING to private, or a constant for that matter, if you want to be able to define in the child classes? Seems like a poor choice of definition. – Devon Bessemer Jun 21 '18 at 15:12
  • @Dormilich, that's what I originally thought but it works when the class constants have a consistent visibility. The docs say to use `defined` for constants, so I guess a follow up to my question is why does it work in any situation if it's not supposed to? – Anton Jun 21 '18 at 15:47
  • @Devon It's not good practice to do it this way but there's a performance consideration in my use case. Loading data (especially array data) from constants directly this way turned out to be significantly faster than "proper" inheritance with functions that override parent functions when needed in the subclass. – Anton Jun 21 '18 at 15:50
  • @Nigel Ren I don't see how the difference between self and static is related to my question, could you clarify? – Anton Jun 21 '18 at 15:52
  • static refers to the class being called - b in this case. When you comment out the constant in b, the constant in a cannot be used as it's private, so there is no value for SOMETHING in b. IF you changed the constant in a to protected, it then becomes visible in b and works. – Nigel Ren Jun 21 '18 at 16:02
  • @NigelRen Right, so what you're saying is that it's a constant visibility issue because b can't access `a::SOMETHING`, rather than the misuse of self/static -- I think that's what confused me about your response. – Anton Jun 21 '18 at 17:02
  • The difference is that self refers to the current class, static always refers to the class being called. So in your case self is pointing at class a, static is class b. – Nigel Ren Jun 21 '18 at 17:10
  • @NigelRen You already said that, but I'm not misusing `self` and `static`. I was either misusing the null coalescing operator or constant visibility (see my answer below). – Anton Jun 21 '18 at 17:30

3 Answers3

8

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();
Anton
  • 3,998
  • 25
  • 40
1

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?

B::SOMETHING is set. It's set because B extends A and you've defined SOMETHING as a constant of A. The problem isn't that it is not set, the problem is that you haven't granted B access to it, so it really doesn't fit into the null coalescing format.

This really comes down to the improper use of private visibility.

Marcin Orlowski
  • 72,056
  • 11
  • 123
  • 141
Devon Bessemer
  • 34,461
  • 9
  • 69
  • 95
1

As you admitted before :

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.


This mean that this operator works correct in your second code block because in class b const SOMETHING exist! but it unaccessible. You can't check existent nor 'NULL value state' of existed encapsulated fields with ?? or with isset().

I guess the check logic of encapsulated field works in manner of :
Exists? ->yes-> is Null? -> Error(Can't access it to check value)

Other code block block works as they should
Petya
  • 312
  • 2
  • 6