Constants — define() vs const, class constants, interface constants
Concept
Constants in PHP are named values that cannot be changed after they are defined. Unlike variables, they have no sigil ($) and are global by default when defined with define(). PHP has two mechanisms for defining constants: the define() function and the const keyword — and understanding their differences is critical for writing maintainable code.
define() is evaluated at runtime: it can appear inside conditionals, loops, or functions, and its first argument can be a dynamic expression. const is a language construct evaluated at compile time (parse time). This means const cannot be inside an if block or a function body (except in class context), but it can be used in namespace scope and is the only way to define class/interface/trait constants. Since PHP 5.6, const values can be constant expressions (arithmetic, string concatenation, other constants), but not arbitrary runtime values.
Class constants, interface constants, and trait constants are defined with const inside the type body. They are accessed via the scope resolution operator :: — either ClassName::CONSTANT, self::CONSTANT, static::CONSTANT (late static binding), or parent::CONSTANT. PHP 8.3 added typed class constants (const string VERSION = '1.0'), which enforces type checking on the constant value — particularly useful for interface constants that implementing classes must not change the type of.
Interface constants are a powerful contract mechanism: an interface can declare constants, and implementing classes inherit them but historically could override them. PHP 8.1 changed this: interface constants became final by default in implementing classes — they can no longer be overridden by a class that implements the interface, only by a child interface that extends it. This enforces true immutability for protocol-level constants.
The PHP_INT_MAX, PHP_INT_MIN, PHP_FLOAT_EPSILON, M_PI, DIRECTORY_SEPARATOR and other predefined constants come from PHP core and extensions. Namespaced constants follow the same use const import syntax as functions. A common gotcha: constants defined with define() are always global regardless of namespace context, while const respects the current namespace.
| Feature | define() | const |
|---|---|---|
| Evaluated | Runtime | Compile time |
| Inside conditionals | Yes | No |
| In class body | No | Yes |
| Namespaced | Global (unless prefixed) | Respects namespace |
| Constant expressions | Any expression | Compile-time expressions only |
| PHP 8.3 typed | No | Yes (class constants) |
Code Example
<?php
declare(strict_types=1);
// define() — runtime, global
define('APP_ENV', 'production');
define('MAX_RETRIES', 3);
// Conditional define (not possible with const)
if (PHP_OS_FAMILY === 'Windows') {
define('PATH_SEP', ';');
} else {
define('PATH_SEP', ':');
}
// const — compile time, namespace-aware
namespace App\Config;
const DB_DRIVER = 'mysql';
const SUPPORTED_LOCALES = ['en', 'fr', 'de']; // arrays allowed since PHP 5.6
// Class constants
class HttpStatus
{
const OK = 200;
const NOT_FOUND = 404;
const SERVER_ERROR = 500;
// PHP 8.3: typed class constant
const string VERSION = '1.0';
// Constant expressions
const REDIRECT_BASE = self::OK + 100; // 300
public static function label(int $code): string
{
return match($code) {
self::OK => 'OK',
self::NOT_FOUND => '404 Not Found',
default => 'Unknown',
};
}
}
echo HttpStatus::NOT_FOUND; // 404
echo HttpStatus::label(200); // OK
// Interface constants — final in implementing classes (PHP 8.1+)
interface HasVersion
{
const string MIN_VERSION = '8.1';
}
class Service implements HasVersion
{
public function getMin(): string
{
return self::MIN_VERSION; // '8.1'
}
// const MIN_VERSION = '7.4'; // Fatal error: Cannot override interface constant
}
// Enum constants (PHP 8.1+ — enums can also have class constants)
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
const DEFAULT = self::Hearts; // constant on an enum
}
echo Suit::DEFAULT->value; // 'H'
// late static binding with static::
class Base
{
const TYPE = 'base';
public static function type(): string
{
return static::TYPE; // resolved at call site
}
}
class Child extends Base
{
const TYPE = 'child';
}
echo Base::type(); // 'base'
echo Child::type(); // 'child'
// use const import
use const App\Config\DB_DRIVER;
echo DB_DRIVER; // 'mysql'Interview Q&A
Q: What are the key differences between define() and const in PHP, and when would you choose one over the other?
The essential difference is when each is evaluated: const is a compile-time construct and define() is runtime. This has three practical consequences. First, const cannot appear inside a conditional block or function, so define() is required when the constant value depends on runtime information (environment, OS, feature flags). Second, const naturally respects namespace scope — a const in namespace App\Http is accessible as App\Http\CONSTANT, whereas define('CONSTANT', 1) is always global unless you manually prefix the name. Third, in class, interface, and trait bodies, only const is allowed. In modern codebases the consensus is: use const everywhere at the file/namespace scope and in types, and only reach for define() when you genuinely need runtime evaluation.
Q: PHP 8.3 introduced typed class constants. Why is this important for interface constants specifically?
Before PHP 8.3, an interface could declare const TIMEOUT = 30 but nothing prevented an implementing class from overriding it with const TIMEOUT = 'thirty', silently changing the type. Typed constants (const int TIMEOUT = 30) enforce that any override must supply a value of the same type — the engine validates this at class-load time. For interface constants (which PHP 8.1 made non-overridable in direct implementors anyway) the benefit is clarity in type analysis tools: Psalm and PHPStan can confidently narrow the type of SomeInterface::TIMEOUT to int and warn if any code tries to use it as a string. This closes a long-standing gap where constants were the only typed language construct without explicit type enforcement.
Q: What is the difference between self::CONSTANT, static::CONSTANT, and parent::CONSTANT in a class hierarchy?
self:: is resolved at definition time — it always refers to the class in which the code is physically written, regardless of which class the object is an instance of. static:: uses late static binding and is resolved at call time — it refers to the class on which the static method was actually called or the actual class of the object. parent:: refers to the direct parent class. The difference matters when a child class overrides a constant: self::TYPE in a base class method always returns the base constant even when called on a child instance, while static::TYPE returns the child's override. This is the same distinction as self::method() vs static::method() for static methods, and it is the mechanism Laravel uses in Model::query() to return the correct model class's query builder.