---
title: "Lazy class constants in PHP"
url: https://www.exakat.io/lazy-class-constants-in-php/
date: 2026-09-22
modified: 2026-09-22
lang: en
author: "dams"
description: "Lazy class constants in PHP: fast by design, sometimes surprising PHP has class constant, which are named literals. It also supports static constant expressions, where the value is not always..."
categories:
  - "Technology"
tags:
  - "audit"
  - "constant"
  - "php"
image: https://www.exakat.io/wp-content/uploads/2026/09/lazy.320.jpg
word_count: 1575
---

# Lazy class constants in PHP

# Lazy class constants in PHP: fast by design, sometimes surprising

PHP has class constant, which are named literals. It also supports static constant expressions, where the value is not always a literal. Rather, it's an expression that PHP doesn't evaluate immediately. PHP evaluates it the first time the code needs it. This is the concept of lazy execution. It is good for performance, and it also means some bugs only show up at runtime, in the one request that touches the broken constant.

So, power and macros come at a risk of eventual error. Let's see how this works.

## How it works

When PHP compiles a class, it tries to fold each constant expression into a plain value. `const A = 2;` is an obvious case. `const A = 2 * 21;` becomes `42` at compile time. And then, some expressions can't be folded, because they depend on something that doesn't exist yet, now or ever:

- global constants: `const PATH = APP_ROOT . '/var';`
- constants from other classes: `const B = Dep::VALUE;` They must be loaded first.
- enum cases: `const DEFAULT = Status::On;`. They must be accessed first.
- expressions that would throw: `const X = 1 % 0;`. This fails, but it the exception is defered to execution phase.

For these, PHP stores the expression as an AST, the famous syntax tree, and marks the constant as "not updated yet". The first time the constant is read, PHP evaluates the tree, replaces it with the result and uses the stored value after that.

```
compiled and running
fine
Error: Undefined constant "UNDEFINED_THING"
?>
[/php]

```

The class compiles, `php -l` reports no syntax errors, and `Config::OK` works. The error only appears when something reads `Config::BROKEN`.

## Why it helps performance

Evaluating constants lazily saves work in three ways.

**1. You don't pay for constants you never read**. A class may define dozens of constants while a given request uses two of them. PHP doesn't resolve the rest. It does some compilation work and allocate memory for the constant, its value and the AST. But these are usually cheap, in terms of memory and execution time.

**2. Dependencies aren't autoloaded early**. A constant that points to another class only triggers that class's autoloader when it's read. This is the same problem with entity hydration or deep cloning, where one element depends on n elements, which dependes on even more elements and dependencies...

```
Svc::A = 1
Svc::B = [autoload(Dep)] 42
Svc::B again = 42
?>
[/php]

```

Reading `Svc::A` doesn't load `Dep`. Only `Svc::B` does, and only once. In a large codebase, constants refer to other classes all the time. Resolving them eagerly would pull in large parts of the class graph on every request.

**3. Declaration order doesn't matter**. A constant can refer to a global constant defined later during bootstrap. It works as long as the value exists when the constant is first read.

With OPcache, classes are cached in shared memory with their unresolved expressions. Each request resolves only the constants it uses.

## Bugs waiting for the right request

Lazy evaluation delays value building, so it also delays errors. A mistake in a constant expression doesn't fail when the file is loaded. It fails when some code first reads that constant, which may be rare or only happen in production.

### Typos and missing dependencies

This file loads without complaint. Reading `Status::On` works. Reading `Status::Off` works. Reading `Status::DEFAULT` fails with:

```
Undefined constant self::Of
?>
[/php]

```

A missing class behaves the same way: `const B = Missing::VALUE;` raises `Class "Missing" not found`, but only when it's evaluated.

### Math that fails at runtime

PHP doesn't detect it at compile time when an expression would throw an exception. It saves it for later:

Now, real code rarely divides by a literal zero. It does divide by other constants, which can be zero in some configuration or an empty string. Dependencies may hide some errors under several layers of camouflage.

### Typed constants checked late

PHP 8.3 added typed class constants. When the value comes from an expression, PHP can only check the type once it computes the value:

Note the stange situation where PHP report the assignation of a value to a constant.

### new evaluates every constant

Reading one constant evaluates only that one. Calling a static method also works. And then, instantiating the class, or reading a static property, evaluates all of the class's constants:

A broken constant that nobody reads can still break `new Foo` in code that has nothing to do with that constant. Calling a static method works, then reading a static property on the same class fails. And the stack trace points to where the class was used, not where the mistake is.

### Results that depend on execution order

A failed evaluation isn't cached as a failure. If the global constant gets defined later, the next read succeeds:

So the outcome depends on execution order. The code works through the web front controller, where bootstrap has run, and fails in a CLI command, a queue worker or a unit test that skips bootstrap. It's the kind of bug that gets reported as "only happens sometimes".

## Recommendations

**1. Keep constant expressions self-contained**. Prefer literals and constants within the same class, or to classes that are always available. Every reference to a global constant or another class is a dependency that is only checked at runtime.

**2. Don't use constants for environment values**. Constants built from `APP_ROOT`, `define()`d settings or config values create the timing problem above. Put that data in a config object, or in a static method that reads configuration explicitly and fails with a clear message.

**3. Evaluate every constant in your test suite**. `ReflectionClass::getConstants()` forces all of a class's constants to be evaluated, so one test can check the whole codebase:

This moves errors from "the first request that happens to read the constant" to CI. Run it after your bootstrap, and a second time without it, to catch constants that depend on bootstrap.

**4. Use static analysis**. PHPStan, Psalm and Exakat resolve constant expressions without running the code. They report undefined constants and classes, missing enum cases and type mismatches that `php -l` doesn't catch. At high enough strictness levels, most of the problems above get flagged before anything runs.

**5. Use typed constants**. A typed constant can't silently turn into the wrong type. Static analyzers can check the type ahead of time, and at runtime a mismatch throws a `TypeError` where the constant is declared, not further down the line.

**6. Remember that `new` checks every constant**. When `new SomeClass` throws an error that mentions a class or constant you've never heard of, look at that class's constants first. Many "impossible" instantiation errors come from there.

**7. Preload in production for an early warning**. With `opcache.preload`, classes are compiled when the server starts, and constants that can be resolved at that point are resolved then. It isn't a complete check, because anything that depends on runtime state stays unresolved. It does move some failures from the first unlucky request to startup.

## Summary

Lazy class constants are offer the same advantages and drawbacks than lazy objects. They are a good trade-off: requests don't pay for constants they don't use or for class graphs they don't need. The cost is that constants are no longer validated when the file compiles. An error in a constant expression shows up only when the right code path reads it, or when `new` runs on its class. Check your constants in CI with reflection and static analysis so those errors don't reach production.