学完这篇,你能分清 define、const、enum 各自在什么时机生效、能写在哪儿、不能写在哪儿,并且知道站点配置该用哪一种方式来存。
第一步:先搞清楚三者的「生效时机」
这是所有坑的根源,先记住这张对照:
| 方式 | 生效时机 | 能否写在 if / 函数里 | 最低版本 |
|---|
define() | 运行时(执行到那一行才成立) | 可以 | 一直有 |
const | 编译期(文件解析时就固定) | 顶层不行,类里可以 | 一直有 |
enum | 编译期 | 只能在顶层或命名空间下 | PHP 8.1 |
一句话:define 是函数调用,const 是语法结构。语法结构不能被条件包住,函数调用可以。
if (!defined('APP_ENV')) {
define('APP_ENV', 'production'); // 合法:运行时才执行
}
// if (!defined('APP_ENV')) {
// const APP_ENV = 'production'; // 语法错误:const 不能写在 if 里
// }
注意:PHP 8.0 起,define() 的第三个参数(不区分大小写)已被移除,写 define('FOO', 1, true) 会直接报错。老代码迁移时优先搜这个。
第二步:define 的适用场景就两个
define 真正的价值是「名字可以是变量」和「可以在运行时决定要不要定义」:
$key = 'CACHE_PREFIX_' . strtoupper($site);
define($key, 'bbs_'); // 动态常量名
以及库文件里防止重复定义的守卫写法(上面那段 if (!defined(...)))。
其余场景,能用 const 就用 const:它不需要函数调用开销,OPcache 也更好处理。另外 const 在顶层同样支持数组:
const DB_OPTIONS = ['charset' => 'utf8mb4', 'strict' => true];
define('DB_OPTIONS', [...]) 从 PHP 7.0 起也支持数组,但可读性不如前者。
注意:const 里的值必须是常量表达式——不能写函数调用、不能写 new、不能写变量。time()、getenv() 这类都不行。需要动态值,只能走 define 或配置文件。
第三步:类常量才是配置的主力
如果你的配置项有归属(比如都属于「上传限制」),别散在全局,塞进类里:
final class Upload
{
public const MAX_SIZE = 5 * 1024 * 1024;
public const ALLOW_EXT = ['jpg', 'png', 'gif', 'webp'];
private const TMP = '/tmp/upload';
}
要点:
- PHP 7.1+ 类常量支持
public / protected / private,不写默认 public。
- PHP 8.1+ 支持
final 常量,防止子类覆盖。
- PHP 8.3+ 支持带类型的类常量,例如
public const string NAME = 'x';。
- 类常量天然带命名空间,不会污染全局,也不会撞名。
注意:self:: 是「定义时常量所在的那个类」,static:: 是「调用时的类」。如果你打算让子类覆盖常量,就必须用 static::,否则永远读到父类的值。
第四步:enum 的边界——什么时候必须用它,什么时候用不了
enum 解决的是「一组固定的、互斥的取值」。典型场景是状态机:
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
case Hidden = 'hidden';
public function label(): string
{
return match ($this) {
self::Draft => '草稿',
self::Published => '已发布',
self::Hidden => '已隐藏',
};
}
}
对比老的写法 const STATUS_DRAFT = 'draft';,枚举的优势是能当类型用:
function publish(Post $post, PostStatus $status) { /* ... */ }
传错值直接抛 TypeError,不用在函数体里手写一堆 in_array 校验。取值用 PostStatus::from('draft'),安全版用 tryFrom() 返回 null。
但枚举有硬边界,踩了就报错:
- 枚举不能
new,不能有属性(public $x),不能继承别的类。
- 枚举可以有常量、方法、实现接口,可以用
PostStatus::cases() 拿全集。
- 枚举不能写在
if 里,因为它和 const 一样是编译期结构。
注意:枚举是 PHP 8.1 才有的语法。如果你的程序要兼容 PHP 7.4,核心代码里就不能出现 enum——像 Clara BBS 这类标称支持 PHP 7.4–8.5 的系统,官方代码必须避开枚举,改用类常量加一个 isValid() 静态方法做等价校验。你自己的项目也一样:先确认最低支持版本,再决定用不用枚举。
第五步:配置管理到底选哪个
按「变化频率」分三层,别混用:
- 永不变化的结构性常量 →
const / 类常量。比如表前缀命名规范、目录名、权限位。
- 随环境变化的配置(数据库账号、Redis 地址、开关) → 不要用常量,用「配置文件返回数组」,在入口处读一次:
// config/database.php
return [
'host' => getenv('DB_HOST') ?: '127.0.0.1',
'name' => 'bbs',
];
常量一旦定义就不能改、不能删,测试里想切换环境会非常难受;数组配置可以整体替换,好测也好覆盖。
- 一组互斥状态 →
enum(PHP 8.1+)或类常量(7.4 兼容)。
注意:判断常量是否存在只能用 defined('X'),不能写 isset(X) 或 empty(X)——isset 对常量永远返回 false,这是新手最常见的静默 bug。
小结
define 是运行时函数,能进 if、能拼动态名;const 是编译期语法,不能进 if,但更快更清晰。
- 有归属的配置放进类常量,配合可见性、
final(8.1+)、类型(8.3+)使用。
- 注意
self:: 与 static:: 的区别,涉及继承就别用 self::。
- 一组互斥取值用
enum,能获得类型检查;但它是 PHP 8.1 语法,7.4 兼容项目必须回避。
- 随环境变化的配置用「返回数组的配置文件」,别塞进常量。
- 检测常量只认
defined()。