A trait could carry methods, properties and abstract methods, and not constants, which meant a trait defining behaviour around a constant had to document rather than declare it.
trait HasRetries
{
public const MAX_ATTEMPTS = 3;
public function attemptsRemaining(int $done): int
{
return max(0, self::MAX_ATTEMPTS - $done);
}
}
// the conflict rule: two traits with the same constant
// name and DIFFERENT values is a fatal error.
// same name, same value: allowed.
// and a class constant always wins over a trait's.
Constants cannot be accessed through the trait name itself — HasRetries::MAX_ATTEMPTS is an error, and only the using class exposes it — which is consistent with how trait properties behave and surprises people who expected interface semantics. The conflict rule being value-sensitive rather than name-sensitive is unusual and means two traits agreeing by coincidence compose silently.