In PHP 8.5, what may appear on the right-hand side of a global const declaration, including the casts and closures added in 8.5?
answer
- constant expression, evaluated without running code
- new allowed since 8.1
- casts since 8.5
- closures must be static, no use()
- call it as (NAME)($x)
basics
~20 sA const value must be a constant expression: literals, arrays, operators, other constants, enum cases and, since 8.1, new. PHP 8.5 adds casts, static closures without use() and first-class callables; ordinary function calls and variables are still rejected.
solid answer
~40 sThe value of `const` must be a **constant expression**, something the engine can evaluate without executing arbitrary code. That covers literals, arrays, arithmetic, string and logical operators, the ternary, other constants, class constants and enum cases, and, for global constants since PHP 8.1, `new SomeClass(...)`. **PHP 8.5** adds three things: **casts** such as `(int)` or `(string)`, **closures**, which must be `static` and cannot `use` variables, and **first-class callables** such as `strtoupper(...)`. Ordinary function calls like `round(0.2049, 2)` and variables are still rejected with "Constant expression contains invalid operations". A constant holding a closure is called with parentheses around its name, `(ROUND_PAY)($amount)`, because `ROUND_PAY($amount)` would look for a function.
code
php · 14 lines<?php
declare(strict_types=1);
namespace App\Payroll;
const TAX_RATE = 0.2;
const TAX_CENTS = (int) (TAX_RATE * 100); // cast, 8.5
const ROUND_PAY = static fn (float $x): float => round($x, 2); // static closure, 8.5
const FORMAT_PAY = \number_format(...); // first-class callable, 8.5
$net = (ROUND_PAY)(2500.0 * (1 - TAX_RATE));
echo TAX_CENTS, ' ', (FORMAT_PAY)($net, 2), PHP_EOL; // 20 2,000.00
// const LOADED_AT = time(); // Constant expression contains invalid operationsgo deeper
Know that const needs a constant expression, so literals, arithmetic and other constants work but function calls do not.
Explain the additions by version, new in 8.1 and casts plus static closures and first-class callables in 8.5, and why closures must be static without use().
Judge when a closure constant is a sensible home for a pure payroll rule and when the value really belongs in injected configuration or a service.
Weigh richer constant expressions against readability and testability across a codebase, and set conventions on what may live in global constants.
## What a constant expression is A `const` declaration binds a name to a value that must be a **constant expression**. The compiler accepts only expressions it can evaluate without running arbitrary user code: no function calls, no variables, no I/O. A similar grammar governs parameter defaults, property defaults, class constants and attribute arguments, although some features, such as `new`, are accepted only in some of those places. (What a closure may hold as a class constant or attribute argument is covered with those features; this answer focuses on global `const`.) ## What is allowed on PHP 8.5 | Allowed | Example | Since | |---|---|---| | scalar and array literals | `const API_VERSION = 'v2';` | long-standing | | operators, ternary, `??` | `const MONTHLY_CAP = 12 * 1000;` | long-standing | | other constants and class constants | `const HALF_RATE = TAX_RATE / 2;` | long-standing | | enum cases | `const DEFAULT_FREQ = PayFrequency::Monthly;` | 8.1 | | `new` (global constants) | `const ZERO = new Money(0, 'EUR');` | 8.1 | | casts | `const CENTS = (int) (TAX_RATE * 100);` | **8.5** | | static closures, no `use` | `const ROUND_PAY = static fn (float $x): float => round($x, 2);` | **8.5** | | first-class callables | `const UPPER = strtoupper(...);` | **8.5** | Note the difference between the last row and a call: `strtoupper(...)` creates a `Closure` object pointing at the function, which is allowed; `strtoupper('eur')` calls it, which is not. ## What is still rejected - **Ordinary function calls**: `const LOADED = time();` or `const RATE = round(0.2049, 2);`. The compiler stops with **"Constant expression contains invalid operations"**. - **Variables**: `const RATE = $config['rate'];`. - **Non-static closures**: `function () {...}` without `static` fails with **"Closures in constant expressions must be static"**, because there is no `$this` to bind. - **Captured variables**: `static function () use ($rate) {...}` fails with **"Cannot use(...) variables in constant expression"**. - **Dynamic names**: an anonymous class, a dynamic class name, or a dynamic function name in a first-class callable are all compile errors. When you need any of these, the value is not constant: use `define()` with a runtime expression, or pass the value in as configuration. ## How the 8.5 cast rule behaves A cast in a constant expression follows the same conversion rules as at runtime, including the warnings. The php-src test for the feature shows `(int) 0.3` giving `0`, `(object) ['a' => 1]` giving a `stdClass`, and `(string) []` giving `"Array"` with an "Array to string conversion" warning. So a cast is a convenience for things like converting a percentage to cents at declaration time, not a way to sneak arbitrary logic in. ## Calling a closure stored in a constant A constant holding a closure is a `Closure` object, and calling it needs one detail: ```php const ROUND_PAY = static fn (float $amount): float => round($amount, 2); echo (ROUND_PAY)(1234.5678); // 1234.57 // ROUND_PAY(1234.5678); // Error: Call to undefined function ROUND_PAY() ``` Without the parentheses, `ROUND_PAY(...)` is parsed as a call to a **function** named `ROUND_PAY`, which does not exist. Wrapping the constant name in parentheses turns it into an expression whose value is then called. The same rule applies to a closure stored in a class constant. ## When to use this - **Good fits**: small pure helpers or default strategies that belong with the configuration they serve, such as a rounding rule for payroll amounts or a formatter keyed by API version. - **Poor fits**: anything that needs services, configuration or state. A static closure without `use` cannot capture anything, which is exactly the point of the restriction. Also remember the older rule that a `const` statement only appears at the top level of a file; the richer 8.5 expression grammar does not change where the statement may be written.
- Why must a closure in a PHP 8.5 constant expression be static and have no use() list?A constant is created once, outside any object and any function call, so there is no `$this` to bind and no local variables to capture. Requiring `static` and forbidding `use(...)` keeps the closure a pure function of its parameters, which is what a constant value is supposed to be. The compiler enforces both with dedicated errors.
- Is const PAY_FORMATTER = number_format(...); allowed in PHP 8.5?Yes. `number_format(...)` is first-class callable syntax: it creates a `Closure` for the function without calling it, and PHP 8.5 allows that in constant expressions. You call it as `(PAY_FORMATTER)(1234.5, 2)`. Writing `number_format(1234.5, 2)` there would still be rejected, because that is a real function call.
- What does const CENTS = (int) (0.2 * 100); do on PHP 8.4?It fails to compile. Casts in constant expressions were only added in PHP 8.5, so on 8.4 the declaration is rejected as an invalid operation. On 8.4 you would write `define('CENTS', (int) (0.2 * 100));` or compute the integer literal yourself.
saying these in an interview costs you the question
- Says const can hold the result of any pure function call, such as round().
- Believes a closure in a constant expression may capture variables with use().
- Calls a closure constant as ROUND_PAY($x) and expects it to work.
- Thinks casts in constant expressions have been allowed since PHP 7.
- Claims new is forbidden in every constant expression.