When upgrading legacy code to PHP 8.5, what happens when a script reads an undefined constant or declares a constant a second time?
answer
- bare word no longer becomes a string
- Error: Undefined constant
- unquoted array keys like $row[name]
- redeclare: warning, first value wins
- "will be an error in PHP 9"
basics
~20 sReading an undefined constant throws an Error since PHP 8.0; PHP 7 turned the bare word into a string. Redeclaring one keeps the first value, warns that PHP 9 will make it an error, and makes define() return false.
solid answer
~40 sTwo old habits break on an upgrade. First, **undefined constants**: before PHP 8.0 an unknown bare name such as `API_VERSION` or the unquoted key in `$row[name]` was treated as the string `'API_VERSION'` with a warning; since 8.0 it throws `Error: Undefined constant "API_VERSION"`, which ends the request unless caught. Second, **redeclaration**: declaring the same constant twice, for example a config file included twice, has never changed the value; the first one wins. PHP 8.5 still emits an `E_WARNING`, now reading "Constant TAX_RATE already defined, this will be an error in PHP 9", and lists constant redeclaration as deprecated; `define()` returns `false`. The fixes are quoting array keys, using `include_once`/`require_once` or a `defined()` guard for shared config, and running static analysis before the upgrade to find both patterns.
code
php · 17 lines<?php
declare(strict_types=1);
// config/payroll.php may be reached from several entry points
if (!defined('TAX_RATE')) {
define('TAX_RATE', 0.2);
}
$ok = @define('TAX_RATE', 0.25); // unguarded second attempt
var_dump($ok); // bool(false)
var_dump(TAX_RATE); // float(0.2): first value wins
try {
echo PAYROLL_API_VERSION; // never defined
} catch (\Error $e) {
echo $e->getMessage(), PHP_EOL; // Undefined constant "PAYROLL_API_VERSION"
}go deeper
Remember that using an undefined constant throws an Error in PHP 8 and that constants cannot be redefined.
Explain the PHP 7 bare-word fallback that 8.0 removed, and what happens on redeclaration: first value kept, a warning, and define() returning false.
Show how you would find unquoted keys, unguarded define() calls and double includes before an upgrade, and why include-order-dependent constants are a correctness bug.
Plan the upgrade path to PHP 9 for constants: single ownership, namespaced const over global define(), and warnings treated as failures in CI.
## Two behaviours that changed under legacy code Old PHP code often relied on the engine being forgiving about constants. Two of those forgiving behaviours are gone or on their way out, and both show up in upgrade projects: 1. reading a constant that was never defined; 2. defining a constant that already exists. ## Undefined constants: from string to Error Before PHP 8.0, an unknown bare identifier was treated as a **string with its own name**. `echo API_VERSION;` without a definition printed `API_VERSION` and raised a notice (a warning since 7.2). The most common victim was the unquoted array key: ```php echo $employee[name]; // PHP 7: reads $employee['name'] with a warning // PHP 8: Error: Undefined constant "name" ``` Since **PHP 8.0** that fallback is removed: an undefined constant throws an **`Error`** with the message `Undefined constant "name"`. Because `Error` is not an `Exception`, a legacy `catch (Exception $e)` does not stop it, and the request ends with a fatal "Uncaught Error". Qualified names such as `\App\Payroll\API_VERSION` never had the string fallback; they always threw. What to look for when upgrading: - unquoted string keys in array access, including inside old templates; - typos in constant names that were silently "working" as strings; - constants defined in a bootstrap that some entry points (a cron script, a CLI command) never load. ## Redeclared constants: first value wins, with a warning A constant can never be changed or undefined once it exists. When code tries to declare it again, whether with `const` or `define()`: - the **original value stays**; - an **`E_WARNING`** is raised; on PHP 8.5 the message reads `Constant TAX_RATE already defined, this will be an error in PHP 9`; - `define()` returns **`false`** instead of `true`. PHP 8.5's release notes list constant redeclaration as **deprecated**. In the 8.5.11 source the diagnostic is still raised at `E_WARNING` level, with the message announcing that PHP 9 will make it an error. The direction is clear: code that "gets away with" declaring twice today will stop working. The typical causes in a payroll codebase: | Cause | Symptom | Fix | |---|---|---| | `include 'config/payroll.php'` reached twice | warning on every request, value from the first include | `require_once`, or load config once in the bootstrap | | two modules each `define('TAX_RATE', ...)` | second module silently gets the first module's rate | one owner per constant, or namespaced `const` per module | | test suite re-running a bootstrap | warnings in test output, stale values across tests | guard with `if (!defined('TAX_RATE'))` or avoid global constants for test-varied values | The second row is the dangerous one: the warning is easy to ignore, and the module that loads later computes pay with the other module's rate. ## Finding both problems before the upgrade 1. Run a static analyser over the codebase; it reports reads of constants that are never defined. 2. Turn warnings into failures in the test suite, so a redeclaration warning cannot pass unnoticed. 3. Grep for `define(` calls not wrapped in a `defined()` guard, and for includes of config files that are not `_once`. 4. Search templates and old views for array access with an unquoted bare-word key, such as `$row[name]`. ## A worked upgrade example Consider a payroll script that ran for years on PHP 7: 1. `config.php` defines `TAX_RATE` and is included by both the web front controller and a nightly batch script, which also includes it indirectly through a helper, so it loads twice. 2. A template prints `$payslip[employee]` without quotes. 3. The batch script reads `PAYROLL_API_VERSION`, which only the web bootstrap defines. On PHP 7 all three "worked": the double include produced a warning nobody read, the template read the right key with a warning, and the batch script sent the literal string `PAYROLL_API_VERSION` as its version. On PHP 8.5, item 1 still warns (and will fail on PHP 9), while items 2 and 3 now throw `Error` and stop the script. The third case is the instructive one: the old behaviour was silently wrong, and the new one merely makes the bug visible. ## Why interviewers ask this The question separates people who have only read about constants from people who have upgraded a real application. The strong answer names the exact exception class (`Error`, not `Exception`), the version (8.0 for undefined constants; 8.5 for the PHP 9 warning), and the one outcome that is easy to miss: on redeclaration the **first** value silently stays.
- Why does catch (Exception $e) not catch an undefined-constant failure on PHP 8?The engine throws `Error`, and `Error` and `Exception` are separate branches under `Throwable`. A handler for `Exception` never sees an `Error`, so the request dies with "Uncaught Error: Undefined constant". Catch `\Error` or `\Throwable` at the boundary, but the real fix is defining or quoting the name.
- If two files both call define('TAX_RATE', ...) with different values, which value does the application use?The value from whichever file runs first. The second `define()` does not overwrite it: it returns `false` and raises a warning, which on PHP 8.5 says the redeclaration will be an error in PHP 9. That makes the effective rate depend on include order, which is why each constant should have exactly one owner.
saying these in an interview costs you the question
- Says the second define() overwrites the constant with the new value.
- Believes an undefined constant still evaluates to its own name as a string on PHP 8.
- Claims catch (Exception $e) handles an undefined-constant failure.
- Thinks redeclaring a constant already throws an Error in PHP 8.5.
- Says $row[name] and $row['name'] are equivalent in PHP 8.