Skip to content

魔术常量

魔术常量是 PHP 中一类特殊的预定义常量,它们的值会根据在代码中的使用位置而动态变化。例如 __LINE__ 返回当前行号,__FILE__ 返回当前文件路径。所有魔术常量都在编译时解析,与运行时常量不同。

前置知识

基础概念

魔术常量之所以"魔术",是因为它们的值不是固定的,而是取决于它们在代码中出现的具体位置。它们在编译时被解析,因此性能优于运行时函数调用(如 debug_backtrace())。

PHP 魔术常量总览

常量说明示例值
__LINE__文件中的当前行号42
__FILE__文件的完整路径和文件名/app/src/Service.php
__DIR__文件所在的目录/app/src
__FUNCTION__当前函数名handleRequest
__CLASS__当前类名(含命名空间)App\Services\UserService
__TRAIT__当前 Trait 名(含命名空间)App\Traits\Loggable
__METHOD__当前方法名App\Services\UserService::getUser
__NAMESPACE__当前命名空间名App\Services
__PROPERTY__属性挂钩中的属性名(PHP 8.4+)属性名称字符串

编译时解析

所有魔术常量都在编译时解析,不是运行时。这意味着它们的值在代码编译完成后就已确定,与运行时的上下文无关。

文件相关魔术常量

__FILE____DIR__

php
<?php
declare(strict_types=1);

// __FILE__ 返回当前文件的完整路径
echo __FILE__ . "\n";
// 输出示例: /var/www/html/project/src/Service.php

// __DIR__ 返回文件所在目录(等价于 dirname(__FILE__))
echo __DIR__ . "\n";
// 输出示例: /var/www/html/project/src

// 实际应用:构建路径
$configPath = __DIR__ . "/../config/app.php";
$templatePath = __DIR__ . "/templates/layout.php";

echo "Config: " . realpath($configPath) . "\n";
echo "Template: " . realpath($templatePath) . "\n";

__DIR__dirname(__FILE__) 的区别

__DIR__ 等价于 dirname(__FILE__),但更简洁高效。注意:除非是根目录,否则 __DIR__ 不包含末尾的斜杠。

__LINE__

php
<?php
declare(strict_types=1);

// __LINE__ 返回当前行号
echo "Line " . __LINE__ . "\n"; // Line 6

$debugInfo = [
    "file" => __FILE__,
    "line" => __LINE__,  // 行号在编译时确定
    "timestamp" => date("Y-m-d H:i:s"),
];

// 调试输出
function logDebug(string $message, array $context = []): void
{
    $line = __LINE__;
    $logEntry = "[" . date("H:i:s") . "] [Line {$line}] {$message}";
    if (!empty($context)) {
        $logEntry .= " | " . json_encode($context, JSON_UNESCAPED_UNICODE);
    }
    echo $logEntry . "\n";
}

logDebug("User login attempt", ["ip" => "192.168.1.1"]);

函数和方法相关魔术常量

__FUNCTION____METHOD__

php
<?php
declare(strict_types=1);

class UserService
{
    /**
     * __FUNCTION__ 只返回函数/方法名
     * __METHOD__ 返回类名::方法名(含命名空间)
     */
    public function getUserById(int $id): ?array
    {
        echo "Function: " . __FUNCTION__ . "\n"; // getUserById
        echo "Method: " . __METHOD__ . "\n";     // App\Services\UserService::getUserById
        return null;
    }
}

$service = new UserService();
$service->getUserById(1);

__FUNCTION__ vs __METHOD__

  • __FUNCTION__:仅返回函数名或方法名
  • __METHOD__:返回完整的"类名::方法名"格式(包含命名空间)

在匿名函数中

php
<?php
declare(strict_types=1);

// 在匿名函数中,__FUNCTION__ 返回 {closure}
$handler = function (string $input): string {
    echo __FUNCTION__ . "\n"; // {closure}
    echo __LINE__ . "\n";      // 行号
    return strtoupper($input);
};

echo $handler("hello") . "\n";

// 在普通命名函数中
function processData(string $data): void
{
    echo __FUNCTION__ . "\n"; // processData
    echo __METHOD__ . "\n";   // processData(不在类中,没有类名前缀)
}

类和命名空间相关魔术常量

__CLASS____TRAIT____NAMESPACE__

php
<?php
declare(strict_types=1);

namespace App\Models;

use App\Traits\Timestampable;

/**
 * __CLASS__ 返回当前类的完整名称(含命名空间)
 */
trait Timestampable
{
    public function getTraitName(): string
    {
        // __TRAIT__ 返回 Trait 名称(含命名空间)
        return __TRAIT__; // App\Traits\Timestampable
    }
}

class User
{
    use Timestampable;

    public function getClassName(): string
    {
        // __NAMESPACE__ 返回当前命名空间
        echo "Namespace: " . __NAMESPACE__ . "\n"; // App\Models
        echo "Class: " . __CLASS__ . "\n";           // App\Models\User

        // ::class 魔术常量返回完全限定的类名
        echo "Class constant: " . User::class . "\n"; // App\Models\User

        return __CLASS__;
    }

    public function getMethodInfo(): string
    {
        echo "Method: " . __METHOD__ . "\n";
        // App\Models\User::getMethodInfo

        return __METHOD__;
    }
}

$user = new User();
$user->getClassName();
$user->getMethodInfo();
echo "Trait name: " . $user->getTraitName() . "\n";

__CLASS__ 在继承和 Trait 中的行为

__CLASS__ 始终指向代码所在类

__CLASS__ 返回的是代码编写所在的类名,而不是实际调用时的对象类名。在 Trait 方法中使用 __CLASS__ 时,返回的是使用该 Trait 的类名。

php
<?php
declare(strict_types=1);

namespace App\Base;

class BaseService
{
    public function showClass(): void
    {
        echo "Class in BaseService: " . __CLASS__ . "\n";
        // 输出: App\Base\BaseService(始终是代码所在类)
    }

    public function showDynamicClass(): void
    {
        echo "Actual class: " . get_class($this) . "\n";
        // 输出实际对象类名
    }
}

namespace App\Services;

use App\Base\BaseService;

class UserService extends BaseService
{
}

$service = new UserService();
$service->showClass();       // App\Base\BaseService(代码所在类)
$service->showDynamicClass(); // App\Services\UserService(实际对象类)

详细说明

__PROPERTY__(PHP 8.4+)

PHP 8.4 引入了属性挂钩(Property Hooks),__PROPERTY__ 魔术常量仅在属性挂钩内部有效,等同于被挂钩的属性名称:

php
<?php
declare(strict_types=1);

// PHP 8.4+ 属性挂钩
class Circle
{
    public float $radius {
        get => $this->radius;
        set(float $value) {
            if ($value < 0) {
                throw new InvalidArgumentException(
                    "Property '{__PROPERTY__}' must be non-negative"
                );
            }
            $this->radius = $value;
        }
    }
}

// __PROPERTY__ 在属性挂钩中返回 "radius"

ClassName::class 常量

::class 魔术常量

ClassName::class 返回类的完全限定名称(包含命名空间)。它在编译时解析,比运行时的 get_class()ReflectionClass 更高效。

php
<?php
declare(strict_types=1);

namespace App\Services;

class UserService
{
}

// ::class 返回完全限定类名
echo UserService::class . "\n";    // App\Services\UserService

// 用于类型检查
$className = UserService::class;
if ($service instanceof $className) {
    echo "Is UserService instance\n";
}

// 用于动态实例化
$service = new $className();

魔术常量在 include 文件中的行为

关键:魔术常量在定义位置解析

当函数参数使用魔术常量作为默认值时,值在函数定义位置解析,而不是在函数调用位置:

php
<?php
declare(strict_types=1);

// helpers.php
function showLocation(string $file = __FILE__, int $line = __LINE__): void
{
    echo "Defined in: {$file} (Line {$line})\n";
    echo "Local constants: " . __FILE__ . "; " . __LINE__ . "\n";
}

// main.php
require_once __DIR__ . "/helpers.php";

showLocation();                     // 参数使用默认值,值是 helpers.php 中的位置
showLocation(__FILE__, __LINE__);    // 参数显式传递,值是 main.php 中的位置

实战示例

日志记录工具

php
<?php
declare(strict_types=1);

namespace App\Utils;

class Logger
{
    private string $logFile;

    public function __construct(string $logDir)
    {
        $this->logFile = rtrim($logDir, "/") . "/app_" . date("Y-m-d") . ".log";
    }

    /**
     * 记录调试信息(自动附加文件名和行号)
     */
    public function debug(string $message, array $context = []): void
    {
        // 注意:这里的 __FILE__ 和 __LINE__ 是此方法定义的位置
        // 需要调用者传递其文件和行号
        $this->write("DEBUG", $message, $context);
    }

    /**
     * 记录信息(调用者需传递位置信息)
     */
    public function info(
        string $message,
        string $file = __FILE__,
        int $line = __LINE__,
        array $context = []
    ): void {
        $this->write("INFO", $message, $context, $file, $line);
    }

    private function write(
        string $level,
        string $message,
        array $context = [],
        string $file = "",
        int $line = 0
    ): void {
        $timestamp = date("Y-m-d H:i:s");
        $entry = "[{$timestamp}] [{$level}]";

        if ($file) {
            $shortFile = basename($file);
            $entry .= " [{$shortFile}:{$line}]";
        }

        $entry .= " {$message}";

        if (!empty($context)) {
            $entry .= " " . json_encode($context, JSON_UNESCAPED_UNICODE);
        }

        $entry .= "\n";
        file_put_contents($this->logFile, $entry, FILE_APPEND);
    }
}

自动加载路径构建

php
<?php
declare(strict_types=1);

// 使用 __DIR__ 构建项目路径
define("APP_ROOT", __DIR__ . "/..");
define("APP_PATH", APP_ROOT . "/app");
define("CONFIG_PATH", APP_ROOT . "/config");
define("STORAGE_PATH", APP_ROOT . "/storage");
define("VIEWS_PATH", APP_PATH . "/Views");

// 自动加载配置文件
$configFiles = glob(CONFIG_PATH . "/*.php");
foreach ($configFiles as $file) {
    $config = require $file;
    // 处理配置...
}

echo "App Root: " . APP_ROOT . "\n";
echo "Config Path: " . CONFIG_PATH . "\n";
echo "Storage Path: " . STORAGE_PATH . "\n";

异常类中使用魔术常量

php
<?php
declare(strict_types=1);

namespace App\Exceptions;

use RuntimeException;

class ValidationException extends RuntimeException
{
    private string $field;
    private mixed $value;

    public static function missingField(string $field): self
    {
        // __METHOD__ 包含完整的类和方法路径
        return new self(
            "Field '{$field}' is required",
            0,
            null,
            $field,
            null
        );
    }

    public static function invalidValue(string $field, mixed $value): self
    {
        return new self(
            "Invalid value for field '{$field}'",
            0,
            null,
            $field,
            $value
        );
    }

    public function __construct(
        string $message,
        int $code,
        ?\Throwable $previous,
        string $field,
        mixed $value
    ) {
        $this->field = $field;
        $this->value = $value;
        parent::__construct($message, $code, $previous);
    }

    public function getField(): string
    {
        return $this->field;
    }

    public function getValue(): mixed
    {
        return $this->value;
    }
}

// 使用
try {
    throw ValidationException::missingField("email");
} catch (ValidationException $e) {
    echo "Error: " . $e->getMessage() . "\n";
    echo "Field: " . $e->getField() . "\n";
}

注意事项

1. 魔术常量不区分大小写

php
<?php
declare(strict_types=1);

// 以下写法等价
echo __FILE__ . "\n";
echo __file__ . "\n";
echo __File__ . "\n";

// 推荐使用大写形式,更易识别

2. 不要在配置中硬编码路径

使用 __DIR__ 代替硬编码的路径,确保项目的可移植性:

php
<?php
declare(strict_types=1);

// 不推荐:硬编码路径
// require "/var/www/html/config/app.php";

// 推荐:使用 __DIR__
require __DIR__ . "/../config/app.php";

3. __DIR__$_SERVER['DOCUMENT_ROOT'] 的区别

  • __DIR__:PHP 文件在文件系统中的实际位置
  • $_SERVER['DOCUMENT_ROOT']:Web 服务器的文档根目录

两者可能不同,特别是在使用虚拟目录或 FPM 配置时。

最佳实践

  1. 使用 __DIR__ 构建路径:避免硬编码,保证可移植性
  2. 使用 ::class 获取类名:编译时解析,比 get_class() 高效
  3. __METHOD__ 做日志标记:包含完整的类和方法信息
  4. 大写书写魔术常量__FILE____file__ 更易识别
  5. 注意魔术常量在参数默认值中的位置:在定义位置解析
  6. 在 include 中正确使用__FILE__ 始终指向当前文件

下一节

学习了魔术常量之后,基础语法部分的学习即将完成。接下来我们将进入表达式和运算符的学习。首先了解什么是表达式以及表达式的求值规则。请阅读 表达式基础

参考链接