0

PHP 8.3 — Typed class constants

Intermediate5 min read·php-09-019

Concept

PHP 8.3 introduced typed class constants, allowing you to declare the type of a class, interface, enum, or trait constant: const int MAX_SIZE = 100. Before PHP 8.3, constants were untyped — any value could be assigned regardless of intent, and static analyzers had to infer the type from the initializer. Typed constants make the contract explicit and enforceable.

The type annotation goes between const and the constant name and follows the same rules as property types: scalar types (int, float, string, bool), array, object, class and interface names, null, union types, and never. Intersection types and DNF types are not valid for constants because constants must be compile-time evaluable — their values cannot be objects constructed at runtime. This also means you cannot type a constant as a class that requires newconst Connection DB = new Connection() is still not valid in PHP 8.3; const values must be literal expressions.

Typed constants are particularly valuable in large codebases where constants evolve over time. Without types, a constant const TIMEOUT = 30 could silently be changed to const TIMEOUT = '30s' in a refactor, and callers depending on the integer would break at runtime rather than at analysis time. With const int TIMEOUT = 30, any initializer that is not an integer is a fatal compile error.

Interface constants can also be typed, which is significant for the behavioral contract they represent. An interface declaring const string DEFAULT_CHANNEL = 'email' ensures any implementing class that overrides the constant (PHP 8.1+ allows override via final removal) provides a string value. Enum constants benefit similarly.

FeatureBefore PHP 8.3PHP 8.3+
Constant type declarationNot supportedSupported
EnforcementRuntime value mismatch onlyCompile-time type mismatch
Static analysisInferred from valueExplicit and verified
Valid constant typesN/AScalars, array, null, union, class names
Objects as valuesNot allowedStill not allowed

Code Example

php
<?php
declare(strict_types=1);

class HttpClient
{
    public const int    DEFAULT_TIMEOUT     = 30;
    public const int    MAX_REDIRECTS       = 5;
    public const string DEFAULT_USER_AGENT  = 'ZiraHttpClient/1.0';
    public const bool   VERIFY_SSL          = true;
    public const array  ALLOWED_METHODS     = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
}

interface Cacheable
{
    public const int    TTL_FOREVER = 0;
    public const string CACHE_PREFIX = '';
}

// Typed constants in enums
enum HttpMethod: string
{
    case GET    = 'GET';
    case POST   = 'POST';
    case PUT    = 'PUT';
    case PATCH  = 'PATCH';
    case DELETE = 'DELETE';

    public const string SAFE_METHODS = 'GET,HEAD,OPTIONS';
}

// Typed constants in traits
trait HasTimestamps
{
    public const string CREATED_AT = 'created_at';
    public const string UPDATED_AT = 'updated_at';
}

// Union typed constants (PHP 8.3+)
class Config
{
    public const int|string DEFAULT_PORT = 8080;
    public const int|null   OPTIONAL_LIMIT = null;
}

// Type mismatch causes a compile-time fatal error:
// class Wrong {
//     public const int TIMEOUT = '30'; // Fatal: Cannot use string as int constant value
// }

// Demonstration: typed constants are enforced in child classes too
class SecureHttpClient extends HttpClient
{
    // Override must be same or narrower type
    public const bool VERIFY_SSL = true; // fine — same type, different value not possible here
                                          // Actually, overriding const values IS allowed:
    public const int  DEFAULT_TIMEOUT = 60; // int — matches parent's type, legal
}

// Practical usage in a service class
class RateLimiter
{
    public const int   WINDOW_SECONDS  = 60;
    public const int   MAX_ATTEMPTS    = 100;
    public const float BACKOFF_FACTOR  = 1.5;

    public function isAllowed(string $key, int $attempts): bool
    {
        return $attempts < self::MAX_ATTEMPTS;
    }
}

Interview Q&A

Q: What problem do typed class constants solve that was not addressed by PHPDoc annotations?

PHPDoc @var on constants is a convention honored by IDEs but ignored at runtime and not enforced by PHP itself. A developer could write /** @var int */ const TIMEOUT = 30 and then change the initializer to '30s' — no error, no warning, no analysis failure if the docblock isn't updated. Typed class constants make the type part of the actual syntax, enforced at compile time. If the initializer's type does not match the declared type, PHP raises a fatal error before any code executes. This closes the gap between documentation and reality, and it means refactoring tools can reliably verify constant types without parsing docblocks.


Q: Which types are valid for typed constants and which are excluded, and why?

Valid types are scalars (int, float, string, bool), array, null, union types of the above, and class/interface names when the value is a class constant reference (not an object instantiation). Intersection types, never, void, and mixed are not valid for constants. The core constraint is that constant initializers must be compile-time evaluable — PHP resolves them before any code runs. You cannot call functions, instantiate objects with new, or reference runtime state. This is why types that imply object instantiation (class names as types for new Class() values) are not meaningful — the engine cannot verify an object's interface compliance at compile time.


Q: Can interface and abstract class constants be typed, and can implementations override them?

Yes. An interface can declare public const string DEFAULT_CHANNEL = 'email', and a class implementing the interface may override it — as long as the overriding constant provides the same type or a compatible subtype. If the interface declares const int TTL = 3600 and an implementor tries const string TTL = 'never', PHP raises a fatal error. This makes typed interface constants a genuine contract: the type of the constant is part of the interface's public API, and violating it is caught at class-load time, not at runtime when the constant is first accessed.