Stringable 接口
概述
Stringable 接口是 PHP 8.0 引入的新接口,用于标识一个类可以隐式转换为字符串。当一个类实现了 __toString() 魔术方法时,PHP 会自动实现 Stringable 接口,无需显式声明 implements Stringable。
该接口的核心价值在于类型系统:函数参数可以使用 string|Stringable 联合类型来接受字符串或任何可转换为字符串的对象。
版本说明
Stringable 接口在 PHP 8.0 中引入。PHP 8.0+ 中任何定义了 __toString() 方法的类都会自动实现该接口。PHP 7.4 及以下版本不存在此接口。
基础概念
接口定义
php
<?php
declare(strict_types=1);
// Stringable 接口定义
// interface Stringable
// {
// public function __toString(): string;
// }自动实现机制
在 PHP 8.0+ 中,只要类中定义了 __toString() 方法,PHP 引擎会自动让该类实现 Stringable 接口,即使没有显式声明。
php
<?php
declare(strict_types=1);
class Money
{
public function __construct(
private readonly int $amount,
private readonly string $currency = 'CNY'
) {}
public function __toString(): string
{
return sprintf('%s %.2f', $this->currency, $this->amount / 100);
}
}
$money = new Money(999, 'USD');
// 自动实现了 Stringable 接口
var_dump($money instanceof Stringable); // bool(true)
echo $money; // USD 9.99语法与代码
类型声明 string|Stringable
Stringable 的主要用途是在函数参数中进行类型约束,允许接受字符串或任何可字符串化的对象。
php
<?php
declare(strict_types=1);
function writeLog(string|Stringable $message): void
{
$text = (string) $message;
$timestamp = date('Y-m-d H:i:s');
echo "[{$timestamp}] {$text}\n";
}
class User
{
public function __construct(
private readonly string $name,
private readonly string $email
) {}
public function __toString(): string
{
return "{$this->name} <{$this->email}>";
}
}
writeLog('系统启动'); // 传入字符串
writeLog(new User('张三', 'zhangsan@mail.com')); // 传入 Stringable 对象显式实现 Stringable(PHP 8.0+)
php
<?php
declare(strict_types=1);
// 虽然会自动实现,但显式声明更清晰
class Uuid implements Stringable
{
private const PATTERN = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i';
private function __construct(
private readonly string $value
) {
if (!preg_match(self::PATTERN, $value)) {
throw new InvalidArgumentException("无效的 UUID 格式: {$value}");
}
}
public static function generate(): self
{
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return new self(sprintf(
'%s-%s-%s-%s-%s',
bin2hex(substr($data, 0, 4)),
bin2hex(substr($data, 4, 4)),
bin2hex(substr($data, 8, 4)),
bin2hex(substr($data, 12, 2)),
bin2hex(substr($data, 14, 2))
));
}
public function __toString(): string
{
return $this->value;
}
}
$id = Uuid::generate();
echo $id; // 例如: 550e8400-e29b-41d4-a716-446655440000__toString() 中的异常处理
php
<?php
declare(strict_types=1);
class HtmlElement implements Stringable
{
public function __construct(
private readonly string $tag,
private readonly string $content = '',
private readonly array $attributes = []
) {}
public function __toString(): string
{
$attrs = '';
foreach ($this->attributes as $name => $value) {
$escaped = htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
$attrs .= " {$name}=\"{$escaped}\"";
}
if (in_array($this->tag, ['br', 'hr', 'img', 'input'], true)) {
return "<{$this->tag}{$attrs} />";
}
return "<{$this->tag}{$attrs}>{$this->content}</{$this->tag}>";
}
}
$div = new HtmlElement('div', 'Hello World', ['class' => 'container', 'id' => 'main']);
echo $div; // <div class="container" id="main">Hello World</div>
$hr = new HtmlElement('hr', '', ['class' => 'divider']);
echo $hr; // <hr class="divider" />__toString() 中的异常
PHP 7.4 以前,__toString() 方法不允许抛出异常(如果抛出会导致致命错误)。PHP 7.4+ 允许在 __toString() 中抛出异常,但如果是通过字符串拼接等方式隐式调用,异常可能无法被捕获。PHP 8.0+ 已修复此限制,可以安全地在 __toString() 中抛出异常。
详细说明
Stringable 在模板引擎中的应用
php
<?php
declare(strict_types=1);
class SafeHtml implements Stringable
{
private string $html;
public function __construct(string $html)
{
// 构造时标记为安全(假定已做净化处理)
$this->html = $html;
}
public function __toString(): string
{
return $this->html;
}
public static function escape(string $raw): self
{
return new self(htmlspecialchars($raw, ENT_QUOTES, 'UTF-8'));
}
}
class TemplateRenderer
{
public function render(string|Stringable $title, string|Stringable $body): string
{
return <<<HTML
<!DOCTYPE html>
<html>
<head><title>{$title}</title></head>
<body>{$body}</body>
</html>
HTML;
}
}
$renderer = new TemplateRenderer();
$html = $renderer->render(
SafeHtml::escape('<script>alert("xss")</script>'),
SafeHtml::escape('<b>Welcome</b>')
);
echo $html;
// 标题和内容都会被转义与旧版 PHP 的兼容
php
<?php
declare(strict_types=1);
// PHP 7.4 及以下不存在 Stringable 接口
// 跨版本兼容方案
if (interface_exists('Stringable')) {
// PHP 8.0+
abstract class AbstractStringable implements Stringable
{
abstract public function __toString(): string;
}
} else {
// PHP 7.4 及以下
abstract class AbstractStringable
{
abstract public function __toString(): string;
}
}字符串插值中的 Stringable
php
<?php
declare(strict_types=1);
class IpAddress implements Stringable
{
public function __construct(private readonly string $address)
{
if (!filter_var($address, FILTER_VALIDATE_IP)) {
throw new InvalidArgumentException("无效的 IP 地址: {$address}");
}
}
public function __toString(): string
{
return $this->address;
}
public function isPrivate(): bool
{
return filter_var(
$this->address,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE
) === false;
}
}
$ip = new IpAddress('192.168.1.1');
echo "服务器 IP: {$ip}"; // 自动调用 __toString()
// 输出: 服务器 IP: 192.168.1.1
// 在 sprintf 中同样自动转换
printf("连接到 %s (私有: %s)", $ip, $ip->isPrivate() ? '是' : '否');实战示例
金额对象
php
<?php
declare(strict_types=1);
class Amount implements Stringable
{
private const CURRENCY_SYMBOLS = [
'CNY' => '¥',
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥',
];
public function __construct(
private readonly int $cents,
private readonly string $currency = 'CNY'
) {
if ($this->cents < 0) {
throw new InvalidArgumentException('金额不能为负数');
}
}
public static function fromYuan(float $yuan, string $currency = 'CNY'): self
{
return new self((int) round($yuan * 100), $currency);
}
public static function fromDollar(float $dollar): self
{
return self::fromYuan($dollar, 'USD');
}
public function cents(): int
{
return $this->cents;
}
public function currency(): string
{
return $this->currency;
}
public function __toString(): string
{
$symbol = self::CURRENCY_SYMBOLS[$this->currency] ?? $this->currency;
$formatted = number_format($this->cents / 100, 2, '.', ',');
return "{$symbol}{$formatted}";
}
public function add(Amount $other): self
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('不能对不同币种进行运算');
}
return new self($this->cents + $other->cents, $this->currency);
}
}
$price = Amount::fromYuan(99.9);
$shipping = Amount::fromYuan(10.0);
$total = $price->add($shipping);
echo "商品: {$price}, 运费: {$shipping}, 合计: {$total}";
// 商品: ¥99.90, 运费: ¥10.00, 合计: ¥109.90注意事项
__toString() 触发场景
php
<?php
declare(strict_types=1);
class Demo implements Stringable
{
public function __toString(): string
{
echo "__toString() 被调用\n";
return "Demo对象";
}
}
$obj = new Demo();
echo $obj; // 触发 __toString()
$str = (string) $obj; // 显式触发
"$obj"; // 字符串插值触发
sprintf("%s", $obj); // sprintf 触发
echo $obj . ' suffix'; // 字符串拼接触发
// 以下不触发
strlen($obj); // 不会自动转换,需要 strlen((string) $obj)
var_dump($obj); // 不会调用 __toString()不能在 __toString() 中抛出异常(PHP 7.4 限制)
php
<?php
declare(strict_types=1);
// PHP 7.4: __toString() 中抛出异常是致命错误
// PHP 8.0+: 允许在 __toString() 中抛出异常
class SafeStringable implements Stringable
{
public function __toString(): string
{
try {
$result = $this->doRiskyOperation();
return $result;
} catch (Throwable $e) {
// PHP 7.4 兼容方案:捕获异常并返回安全字符串
return '[错误: ' . $e->getMessage() . ']';
}
}
private function doRiskyOperation(): string
{
// 可能失败的操作
return '操作结果';
}
}最佳实践
- 显式声明 implements Stringable:虽然会自动实现,但显式声明代码更清晰,IDE 提示更好
- __toString() 返回有意义的表示:不应返回调试信息或空字符串,应返回面向用户的可读字符串
- 保持 __toString() 简单:避免在
__toString()中执行复杂逻辑或 I/O 操作 - 使用 string|Stringable 类型约束:让函数/方法同时接受字符串和可字符串化对象
- 注意 __toString() 的隐式调用:在字符串拼接和插值中会自动触发
php
<?php
declare(strict_types=1);
// 参数类型约束的最佳实践
function logMessage(string|Stringable $message, string $level = 'info'): void
{
$text = is_object($message) ? (string) $message : $message;
$line = sprintf(
"[%s] [%s] %s",
date('Y-m-d H:i:s'),
strtoupper($level),
$text
);
error_log($line);
}
// 返回值类型约束
function renderTitle(string|Stringable $raw): string
{
// 始终返回 string 类型
$str = (string) $raw;
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}