A public constant is the hardest thing in a PHP library to take back. You can deprecate a method, a class constant, an enum case — all with a single attribute. But a plain const at the top of a file got a doc comment and a prayer, and nobody reads doc comments. PHP 8.5 closes that gap.
What PHP 8.5 attributes on constants actually changed#
Be precise about the scope here, because a lot of the write-ups blur it. Attributes have targeted class constants since PHP 8.0 via Attribute::TARGET_CLASS_CONSTANT, and PHP 8.4 taught the built-in #[\Deprecated] attribute to work on them alongside functions, methods and enum cases — which I covered when PHP 8.4 shipped the #[\Deprecated] attribute.
What PHP 8.5 adds is the one target that was still missing: compile-time, non-class constants. That means constants declared with const, at file or namespace scope:
<?php
#[\Deprecated(message: 'Use Billing::LEGACY_MODE instead', since: '3.2')]
const BILLING_LEGACY_MODE = true;
Two new pieces come with it. Attribute::TARGET_CONSTANT lets your own attribute classes opt in, and Attribute::TARGET_ALL now includes constants. ReflectionConstant::getAttributes() exposes them at runtime.
Constants created with define() are not covered. The RFC lists that as future scope, and the reasoning is mechanical: attributes are attached at compile time, and define() is a runtime function call. If you want an attribute on it, it has to be a const.
Deprecating a constant with #[\Deprecated]#
The attribute takes the same two optional named arguments it has taken since 8.4 — message and since — and the engine assembles the notice for you:
<?php
#[\Deprecated(message: 'Use Billing::LEGACY_MODE instead', since: '3.2')]
const BILLING_LEGACY_MODE = true;
if (BILLING_LEGACY_MODE) {
// Deprecated: Constant BILLING_LEGACY_MODE is deprecated since 3.2,
// Use Billing::LEGACY_MODE instead in /app/bootstrap.php on line 7
}
The notice fires on read, not on declaration. Loading the file that declares the constant is silent; touching the constant is what warns. That is the behaviour you want — the file that owns the deprecation does not spam its own log.
For a class constant the mechanism is identical, and has been available a version longer:
<?php
final class Billing
{
#[\Deprecated(message: 'Use Billing::MODE_STANDARD', since: '3.2')]
public const string MODE_LEGACY = 'legacy';
public const string MODE_STANDARD = 'standard';
}
echo Billing::MODE_LEGACY;
// Deprecated: Constant Billing::MODE_LEGACY is deprecated since 3.2, ...
I reach for the class constant form nine times out of ten. Global constants are mostly a legacy shape in modern PHP — but "mostly" is the point of this feature. The constants you most want to kill are exactly the ones defined in a 2016 constants.php that half your consumers still include.
Deprecating a trait#
The other half of the 8.5 story is a separate RFC that landed in the same release: #[\Deprecated] on traits. Attributes could always target a trait — Attribute::TARGET_CLASS covers traits, interfaces and enums — but the built-in #[\Deprecated] rejected them until now.
<?php
#[\Deprecated(message: 'Use the HasBilling contract instead', since: '4.0')]
trait InteractsWithBilling
{
public function chargeCard(): void {}
}
final class Subscription
{
use InteractsWithBilling;
}
// Deprecated: Trait InteractsWithBilling used by Subscription is deprecated
// since 4.0, Use the HasBilling contract instead in ... on line ...
One detail worth internalising: the warning fires only on a direct use. A class that extends Subscription inherits the trait's methods but reports nothing, because it never used the trait itself. That keeps the signal pointed at the file you actually need to edit, which is the right call — though it does mean a grep for the deprecation in your logs undercounts how much of your codebase depends on it.
The notice is a normal E_DEPRECATED, so a set_error_handler that throws will turn it into a fatal. Handy in CI, painful in production if you have not audited first.
Reading constant attributes with Reflection#
The reflection side is where this gets useful beyond deprecation. Declare an attribute class with the new target, and you can annotate constants with real, typed metadata:
<?php
#[\Attribute(\Attribute::TARGET_CONSTANT | \Attribute::TARGET_CLASS_CONSTANT)]
final class ConfigKey
{
public function __construct(
public readonly string $envVar,
public readonly bool $secret = false,
) {}
}
#[ConfigKey(envVar: 'BILLING_WEBHOOK_SECRET', secret: true)]
const BILLING_WEBHOOK_SECRET_KEY = 'billing.webhook_secret';
Reading it back uses ReflectionConstant, the class PHP 8.4 introduced for non-class constants, with the getAttributes() method that is new in 8.5:
<?php
$constant = new ReflectionConstant('BILLING_WEBHOOK_SECRET_KEY');
foreach ($constant->getAttributes(ConfigKey::class) as $attribute) {
$configKey = $attribute->newInstance();
echo $configKey->envVar; // BILLING_WEBHOOK_SECRET
var_dump($configKey->secret); // bool(true)
}
var_dump($constant->isDeprecated()); // bool(false)
For class constants the equivalent is ReflectionClassConstant::getAttributes(), which has worked since 8.0:
<?php
$classConstant = new ReflectionClassConstant(Billing::class, 'MODE_LEGACY');
var_dump($classConstant->isDeprecated()); // bool(true)
If you are building tooling on top of this — a config auditor, a doc generator, a migration report — the patterns for instantiating and caching attribute instances are the same ones I walked through in PHP custom attributes beyond Laravel's built-ins. Nothing about constants changes the ergonomics.
Where PHP 8.5 attributes on constants bite#
The list of sharp edges is short but every one of them will cost you an afternoon.
Grouped declarations are a compile error. The RFC deliberately forbids applying an attribute to a multi-constant statement, because it would be ambiguous which constant it belonged to:
<?php
// Fine
#[\Deprecated]
const LEGACY_MODE = true;
// Fatal error: Cannot apply attribute to a group of constants
#[\Deprecated]
const LEGACY_MODE = true,
LEGACY_TIMEOUT = 30;
Split the statement. It is a one-line fix, but it means a bulk deprecation pass across an old constants.php is a bigger diff than you expect.
This is not backwards compatible source. Before 8.5, an attribute on a global constant was a compile error, not a silently-ignored comment. A library that adds #[\Deprecated] above a const will fail to parse on PHP 8.4 and below. If your composer.json still allows ^8.3, you cannot ship this yet — put the deprecation in a doc block until you can raise the floor. Class constants are safe here, since attributes have parsed there since 8.0.
isDeprecated() can now return true where it never did. ReflectionConstant::isDeprecated() previously always returned false for userland constants. Any tooling that hardcoded that assumption needs revisiting.
Static analysers lag the engine. PHPStan and Psalm each needed a release to stop reporting attributes on constants as syntax errors. Check your versions before you blame your code — the same shakeout happened with PHP 8.5's #[\NoDiscard] attribute.
Watch the constant-expression interaction. A deprecated constant referenced inside another constant expression — a default parameter value, another const, or a closure in a constant expression — surfaces its notice when that expression is evaluated, which is not always where you wrote it. Test the deprecation path, do not assume it.
Wrapping Up#
If you maintain a package, do the audit now: find your public const declarations, split any grouped statements, and put #[\Deprecated(message:, since:)] on the ones you want gone in the next major — but only once your minimum PHP version is 8.5. For applications, the win is smaller but immediate: your constants.php can finally tell callers off itself instead of relying on a code review to catch it.
Before you bump, run through the PHP 8.5 deprecations cheat sheet so you are fixing your own deprecations rather than tripping over the engine's. And if the deprecation notices are landing somewhere you cannot see them, PHP 8.5's fatal error backtraces will tell you which call path actually reached the constant.
FAQ#
Can PHP attributes target constants?
Yes, as of PHP 8.5. Class constants have accepted attributes since PHP 8.0 through Attribute::TARGET_CLASS_CONSTANT. PHP 8.5 adds Attribute::TARGET_CONSTANT for compile-time, non-class constants declared with const, and Attribute::TARGET_ALL now includes them. Constants created with define() are still not supported.
How do I deprecate a class constant in PHP 8.5?
Put #[\Deprecated] directly above the constant declaration, optionally passing the message and since named arguments. PHP emits an E_DEPRECATED notice every time the constant is read, formatted as "Constant Billing::MODE_LEGACY is deprecated since 3.2". This has worked on class constants since PHP 8.4, so you do not need to wait for 8.5 unless the constant is a global one.
Does #[Deprecated] work on constants and traits?
Both, in PHP 8.5. Global constants came from the attributes-on-constants RFC and traits from a separate RFC in the same release. A deprecated trait reports "Trait InteractsWithBilling used by Subscription is deprecated" when a class directly uses it. Classes that inherit the trait through a parent do not report anything, so the notice points at the file you need to change.
How do I read attributes on a constant with Reflection?
For a global constant, instantiate ReflectionConstant with the constant's name and call getAttributes(), which is new in PHP 8.5. It takes the same optional name filter and flags as every other getAttributes() method, and each returned ReflectionAttribute gives you newInstance(). For class constants, use ReflectionClassConstant::getAttributes() instead — that has been available since PHP 8.0.
What PHP version added attributes on constants?
PHP 8.5, released on 20 November 2025. The RFC passed 26–0 in January 2025 and was implemented for that release. Attributes on class constants are older, arriving with the original attributes feature in PHP 8.0, and the #[\Deprecated] attribute itself was introduced in PHP 8.4.