Constant Scalar Expressions In PHP

Constant Scalar Expressions In PHP

Constants are the immovable rocks of a PHP application. Once defined, they never change. Yet, since PHP 5.6, those rocks may be carved with expressions. This is the realm of constant scalar expressions, where PHP allows a little bit of calculation in a place that was, for a long time, strictly literal.

= 80000;

No variable in sight, and yet, some logic. Let us review what is allowed in there, what is forbidden, and then, one operator that everyone believes is available but is not, a subtle behavior that keeps surprising developers year after year, and an architecture that spread across the whole application code base.

The Raw Material: Which Values May Enter A Constant Expression

A constant expression accepts a whide range of ingredients. It starts with data itself, so literals are on the menu. And not just them, in fact.

  • Integers, in all their notations: decimal, hexadecimal, octal, binary, all the signs + and - you need, and even with the underscore separator such as 1_000_000.
  • Floats, with the scientific notation 1.2e3.
  • Strings, single or double quoted. Heredoc and nowdoc are also welcome, as long as no variable sneaks inside.
  • Booleans and null, the philosophers of the type system.
  • Arrays, with the short syntax [1, 2, 3], possibly nested, possibly with keys. The values inside must themselves be constant expressions. Turtles all the way down.
  • Other constants, global ones such as \A\B\C or class constants such as Foo::BAR. Constants love to stand on the shoulders of other constants.
  • Enum cases, since PHP 8.1, because an enum case is a constant in a fancy suit. It works with the whole enum, or only its value. Yet, why settle for the latter?
  • Magic constants such as __DIR__, __LINE__ or __CLASS__. Magic, yes, but well behaved magic.
  • Objects, since PHP 8.1, thanks to new in initializers. Class constants are still waiting at the door for that one.
  • Closures and first class callables, since PHP 8.5. Yes, you read that right: const C = foo(...); is now valid PHP, and so is const D = static function ($x) { return $x * 2; };. The closure must be static and must not capture anything with use. A constant that computes on demand: the snake bites its own tail, and seems to enjoy it.

What is missing from this list is just as important: variables, function calls, properties of a random object. PHP wants the value to be computable without running your code. A constant expression is a promise of purity. Well, it was, until closures moved in.

Backed Enums: The One Object That Shares Its Properties

Since PHP 8.2, a constant expression may read the value and name properties of an enum case.

value; // ‘H’
}

Enums are the only objects granted this privilege. Any other object, even one fully built with new in an initializer, keeps its properties for itself. Try it, and PHP answers with a very explicit Fetching properties on non-enums in constant expressions is not allowed. The engine trusts enum cases because they are immutable by construction. Your own objects, however well behaved, are presumed guilty of being dynamic.

x; // Fatal error. The property stays private property.

The Toolbox: Which Operators Are Allowed

The operators form a generous set. You may use:

  • Arithmetic: +, -, *, /, %, **. Including exponentiation. Your constants do not have to remember what is 2 ** 64.
  • String concatenation: ., the workhorse of message building.
  • Bitwise operators: &, |, ^, ~, <<, >>. Perfect for building flag systems, such as const ALL = self::READ | self::WRITE.
  • Comparison operators: ==, !=, ===, !==, <, >, <=, >=, and the spaceship <=>, which landed among constants like everywhere else.
  • Logical operators: &&, ||, !, plus their literary cousins and, or, xor.
  • The ternary operator, long form and short form ?:.
  • The null coalescing operator ??, mostly useful with array keys that may or may not exist.
  • Array dereferencing: FOO[1] or FOO['key'], to pick one value from a constant array.
  • Cast operators, since PHP 8.5. const A = (int) '42'; is finally legal. All the casts are welcome: (int), (float), (string), (bool), (array). All but one: (object)is refused, with the message Object casts are not supported in this context. PHP will happily give a constant an object born from new, but an object born from a cast, never. Every family has its favorite child. Before PHP 8.5, developers emulated casts with tricks such as '42' + 0 for integers or 42 . '' for strings: clever, ugly, and now obsolete. As for the new (void) cast of PHP 8.5, it only exists as a statement, so it has no seat at this table either.

The Forbidden Fruits

Now, the operators and constructs that shall never appear in a constant expression:

  • Assignment operators: =, +=, .= and friends. Assigning inside a constant definition would be a strange loop indeed.
  • Increment and decrement: ++ and the other one that goes down. A constant that increments itself is called a variable, and there is another keyword for that.
  • The error suppression operator @. Constants have nothing to hide.
  • Function calls, even the innocent looking ones such as strlen or time. Note the nuance with PHP 8.5: foo(...) is accepted because it creates a callable without calling anything, while foo() remains banished because it actually runs. Constants may hold the gun, but they shall not pull the trigger.
  • Variables, obviously. The moment a $ appears, the party is over.
  • match and other runtime constructs. They stayed at the runtime table.
  • instanceof. This one deserves its own section, because it is a magnificent trap.

instanceof: So Close, Yet So Forbidden

Here is a story of two operators that look identical and behave nothing alike. Since PHP 7.3, a constant is allowed on the left side of instanceof in regular code. And since PHP 8.1, a constant may hold an object. Combine the two, add a name collision, and you obtain this little theater piece:

Read it slowly. The left A is the constant, which holds an object of class A. The right A is the class. Same three letters, two different worlds, and PHP resolves each side without blinking. The constant A is indeed an instance of the class A, so the answer is true. Show this line in a job interview and watch the room divide into two camps.

Naturally, the next step is to store that beautiful result in a constant:

And there, PHP slams the door. instanceof is fine in an echo, in an if, in a return, anywhere code runs. Inside a constant expression, it is still on the blacklist, verified up to PHP 8.5.2. The operator got a visa for the runtime, never for the constant land. Maybe one day. Until then, A instanceof Aremains a party trick, not a constant.

Constants Standing On Constants: Order Matters

Constants may be built on top of other constants, and this is where architecture enters the scene.

Beautiful. Yet, this beauty comes with a contract: the ingredients must exist when the recipe is cooked. A constant that refers to Foo::BAR needs the class Foo to be loadable at the moment of evaluation. With autoloading, this usually works by itself, but a class defined later in the same file, or a constant defined in a file that was never included, will end the story with an Error.

Move the echo after the definition of B, and everything works. The dependency graph of your constants is real, even if invisible. Circular definitions, where A::X needs B::Y which needs A::X, are also detected and rejected. PHP is not fond of chickens and eggs.

The Lazy Secret: Constants Are Calculated On First Use

Here is the twist that saves the previous example and confuses code reviewers: a constant expression is not evaluated when it is declared. It is evaluated when the constant is used for the first time. Until then, it is just a recipe waiting in a drawer.

This has delightful consequences.

First, a constant with an invalid expression may sit quietly in your codebase for years. Division by zero, reference to a missing class, an impossible array key: none of it explodes at declaration. The bomb only goes off when someone finally reads the constant. Or never, if nobody does. Schrödinger would have loved PHP class constants: both valid and broken until observed.

Second, this laziness is also an optimization. Constants that are never used cost nothing to compute. Your two hundred unused constants are not slowing anything down. They are only slowing down the next developer reading the file: I'm looking at you, AI! Unused code is going to cost someone a lot of tokens.

Third, magic constants such as __LINE__ are resolved where they are written, not where they are used. The recipe is lazy, but the ingredients were picked at writing time.

The Self Referencing Constant: Abstract By Accident

PHP has abstract methods, abstract classes, and no abstract constants. Officially. Yet laziness, that same laziness we just described, opens a back door. Consider a constant defined as itself:

A constant that references itself is obviously invalid: PHP throws Cannot declare self-referencing constant self::NAME the moment it tries to evaluate it. But remember, evaluation only happens at first read. Declaration goes through without a whisper. So the trap is armed, and now everything depends on who steps on it.

Every child class that overrides the constant lives happily. Any code that dares to read the parent constant directly gets an immediate Error. This is exactly the contract of an abstract constant: you must redefine me, or you shall not use me. It is a trick, it smells like a trick, and it is a genuinely useful trick. The error message is even self documenting, which is more than most abstractions can claim. Static analysis tools will raise an eyebrow at it, so leave a comment for the next reader, who will raise the other eyebrow.

Global Constants And Class Constants Are Not Twins

They share a keyword, they share the laziness, and yet global constants and class constants do not enjoy the same rights. The most striking difference: since PHP 8.1, a global constant may hold an object, while a class constant may not.

Frustrating? Yes. Final? No. A class constant cannot birth an object, but nothing forbids it from adopting one. Assign the global constant to the class constant, and the object walks right in:

The order of definition matters, as always: the global constant must exist by the time App::LOGGER is read for the first time. Not when the class is declared. Read that again, because this nuance is about to pay off.

Since evaluation is lazy, the global constant may be created with define, at runtime, with values computed at runtime. The class constant will pick up whatever is there when it wakes up:

Let this sink in: a class constant, the epitome of static, immutable, carved in stone values, just received an object configured from the command line. The class is statically changed at execution time. This sentence should not exist, and yet the code runs. Use this power for dependency injection at the constant level, for test doubles, or simply to win arguments at conferences. And maybe do not use it in production without telling anyone, because the person debugging why App::LOGGER differs between two runs deserves at least a comment.

Conclusion

Constant scalar expressions give constants a small brain: literals, other constants, arrays, enum case values, and a rich set of read only operators, now including casts, minus the (object) one. Since PHP 8.5, they even hold closures and first class callables, as long as those stay static and capture nothing. No variables, no function calls, and no instanceof, whatever your intuition says. Build them on top of each other with care, since the referenced classes and constants must be available at evaluation time. And remember the golden rule: evaluation happens at first use, possibly never. That single rule powers everything strange in this article: the bombs that sleep for years, the self referencing constants that act abstract, and the class constants that receive runtime objects through a global constant back door. A syntax check will not catch any of it. A good static analysis pass, or an actual use of the constant, will.

Happy coding, and may your constants stay constant.Constant Scalar Expressions In PHP