PHP 8.1 只读属性深度解析:不可变对象设计的最佳实践
Orion K Lv6

引言

PHP 8.1引入的只读属性(Readonly Properties)是我在构建不可变对象时最喜欢的特性。作为一个经常需要处理敏感数据和确保数据完整性的开发者,只读属性让我的代码更加安全和可靠。经过近两年的实践,我想分享一些只读属性的实际应用经验。

什么是只读属性

只读属性是一种特殊的属性,一旦被初始化后就不能再被修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User {
public function __construct(
public readonly string $id,
public readonly string $email,
public readonly DateTime $createdAt,
public string $name // 普通属性,可以修改
) {}
}

$user = new User('123', 'user@example.com', new DateTime(), 'John');
echo $user->id; // ✓ 可以读取
echo $user->email; // ✓ 可以读取

$user->name = 'Jane'; // ✓ 可以修改普通属性
// $user->id = '456'; // ✗ 错误:不能修改只读属性
// $user->email = 'new@example.com'; // ✗ 错误:不能修改只读属性

传统方式 vs 只读属性

传统的私有属性 + Getter方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TraditionalUser {
private string $id;
private string $email;
private DateTime $createdAt;

public function __construct(string $id, string $email, DateTime $createdAt) {
$this->id = $id;
$this->email = $email;
$this->createdAt = $createdAt;
}

public function getId(): string {
return $this->id;
}

public function getEmail(): string {
return $this->email;
}

public function getCreatedAt(): DateTime {
return $this->createdAt;
}
}

使用只读属性的现代方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ModernUser {
public function __construct(
public readonly string $id,
public readonly string $email,
public readonly DateTime $createdAt
) {}

// 不需要getter方法,直接访问属性
// 代码量减少了70%
}

// 使用方式
$user = new ModernUser('123', 'user@example.com', new DateTime());
echo $user->id; // 直接访问,无需getter
echo $user->email; // 直接访问,无需getter

实际应用场景

1. 值对象(Value Objects)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Money {
public function __construct(
public readonly float $amount,
public readonly string $currency
) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}

if (empty($currency)) {
throw new InvalidArgumentException('Currency cannot be empty');
}
}

public function add(Money $other): Money {
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currency mismatch');
}

return new Money($this->amount + $other->amount, $this->currency);
}

public function multiply(float $factor): Money {
return new Money($this->amount * $factor, $this->currency);
}

public function format(): string {
return number_format($this->amount, 2) . ' ' . $this->currency;
}

public function equals(Money $other): bool {
return $this->amount === $other->amount && $this->currency === $other->currency;
}
}

// 使用示例
$price = new Money(99.99, 'USD');
$tax = new Money(10.00, 'USD');
$total = $price->add($tax);

echo $total->format(); // 109.99 USD
echo $total->amount; // 109.99 (直接访问只读属性)
echo $total->currency; // USD (直接访问只读属性)

2. 配置对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class DatabaseConfig {
public function __construct(
public readonly string $host,
public readonly int $port,
public readonly string $database,
public readonly string $username,
public readonly string $password,
public readonly string $charset = 'utf8mb4',
public readonly array $options = []
) {
if (empty($host)) {
throw new InvalidArgumentException('Host cannot be empty');
}

if ($port < 1 || $port > 65535) {
throw new InvalidArgumentException('Invalid port number');
}
}

public function getDsn(): string {
return "mysql:host={$this->host};port={$this->port};dbname={$this->database};charset={$this->charset}";
}

public function toArray(): array {
return [
'host' => $this->host,
'port' => $this->port,
'database' => $this->database,
'username' => $this->username,
'charset' => $this->charset,
'options' => $this->options
];
}
}

// 配置工厂
class ConfigFactory {
public static function fromEnv(): DatabaseConfig {
return new DatabaseConfig(
host: $_ENV['DB_HOST'] ?? 'localhost',
port: (int)($_ENV['DB_PORT'] ?? 3306),
database: $_ENV['DB_DATABASE'] ?? throw new RuntimeException('DB_DATABASE not set'),
username: $_ENV['DB_USERNAME'] ?? 'root',
password: $_ENV['DB_PASSWORD'] ?? '',
charset: $_ENV['DB_CHARSET'] ?? 'utf8mb4'
);
}

public static function fromArray(array $config): DatabaseConfig {
return new DatabaseConfig(
host: $config['host'],
port: $config['port'],
database: $config['database'],
username: $config['username'],
password: $config['password'],
charset: $config['charset'] ?? 'utf8mb4',
options: $config['options'] ?? []
);
}
}

3. 事件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class OrderCreatedEvent {
public function __construct(
public readonly string $orderId,
public readonly string $customerId,
public readonly array $items,
public readonly Money $total,
public readonly DateTime $createdAt,
public readonly array $metadata = []
) {}

public function getEventName(): string {
return 'order.created';
}

public function getPayload(): array {
return [
'order_id' => $this->orderId,
'customer_id' => $this->customerId,
'items_count' => count($this->items),
'total_amount' => $this->total->amount,
'total_currency' => $this->total->currency,
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
'metadata' => $this->metadata
];
}

public function getItemsTotal(): Money {
$total = new Money(0, $this->total->currency);
foreach ($this->items as $item) {
$total = $total->add($item['price']);
}
return $total;
}
}

// 事件监听器
class OrderEventListener {
public function handleOrderCreated(OrderCreatedEvent $event): void {
// 发送确认邮件
$this->sendConfirmationEmail($event->customerId, $event->orderId);

// 更新库存
foreach ($event->items as $item) {
$this->updateInventory($item['product_id'], $item['quantity']);
}

// 记录日志
$this->logOrder($event);
}

private function logOrder(OrderCreatedEvent $event): void {
error_log(sprintf(
'Order created: %s, Customer: %s, Total: %s',
$event->orderId,
$event->customerId,
$event->total->format()
));
}
}

4. API响应对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class ApiResponse {
public function __construct(
public readonly mixed $data,
public readonly int $statusCode,
public readonly string $message,
public readonly array $headers = [],
public readonly ?array $errors = null,
public readonly ?array $meta = null
) {}

public function isSuccess(): bool {
return $this->statusCode >= 200 && $this->statusCode < 300;
}

public function isError(): bool {
return $this->statusCode >= 400;
}

public function toArray(): array {
$response = [
'status_code' => $this->statusCode,
'message' => $this->message,
'data' => $this->data
];

if ($this->errors !== null) {
$response['errors'] = $this->errors;
}

if ($this->meta !== null) {
$response['meta'] = $this->meta;
}

return $response;
}

public function toJson(): string {
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
}
}

// API响应构建器
class ApiResponseBuilder {
public static function success(mixed $data, string $message = 'Success'): ApiResponse {
return new ApiResponse(
data: $data,
statusCode: 200,
message: $message
);
}

public static function created(mixed $data, string $message = 'Created'): ApiResponse {
return new ApiResponse(
data: $data,
statusCode: 201,
message: $message
);
}

public static function error(string $message, int $statusCode = 400, ?array $errors = null): ApiResponse {
return new ApiResponse(
data: null,
statusCode: $statusCode,
message: $message,
errors: $errors
);
}

public static function notFound(string $message = 'Resource not found'): ApiResponse {
return new ApiResponse(
data: null,
statusCode: 404,
message: $message
);
}
}

5. 数据传输对象(DTO)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class CreateUserRequest {
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password,
public readonly ?int $age = null,
public readonly array $roles = ['user'],
public readonly ?string $avatar = null
) {
$this->validate();
}

private function validate(): void {
if (strlen($this->name) < 2 || strlen($this->name) > 50) {
throw new InvalidArgumentException('Name must be between 2 and 50 characters');
}

if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}

if (strlen($this->password) < 8) {
throw new InvalidArgumentException('Password must be at least 8 characters');
}

if ($this->age !== null && ($this->age < 0 || $this->age > 150)) {
throw new InvalidArgumentException('Age must be between 0 and 150');
}
}

public function toArray(): array {
return [
'name' => $this->name,
'email' => $this->email,
'password' => password_hash($this->password, PASSWORD_DEFAULT),
'age' => $this->age,
'roles' => $this->roles,
'avatar' => $this->avatar
];
}

public static function fromArray(array $data): self {
return new self(
name: $data['name'],
email: $data['email'],
password: $data['password'],
age: $data['age'] ?? null,
roles: $data['roles'] ?? ['user'],
avatar: $data['avatar'] ?? null
);
}
}

// 用户服务
class UserService {
public function createUser(CreateUserRequest $request): User {
// 检查邮箱是否已存在
if ($this->userRepository->existsByEmail($request->email)) {
throw new DomainException('Email already exists');
}

// 创建用户
$userData = $request->toArray();
$user = $this->userRepository->create($userData);

// 发送欢迎邮件
$this->emailService->sendWelcomeEmail($user);

return $user;
}
}

只读属性的高级特性

1. 继承中的只读属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BaseEntity {
public function __construct(
public readonly string $id,
public readonly DateTime $createdAt
) {}
}

class User extends BaseEntity {
public function __construct(
string $id,
DateTime $createdAt,
public readonly string $email,
public readonly string $name
) {
parent::__construct($id, $createdAt);
}
}

class Product extends BaseEntity {
public function __construct(
string $id,
DateTime $createdAt,
public readonly string $name,
public readonly Money $price
) {
parent::__construct($id, $createdAt);
}
}

2. 接口中的只读属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
interface Identifiable {
public function getId(): string;
}

interface Timestampable {
public function getCreatedAt(): DateTime;
public function getUpdatedAt(): ?DateTime;
}

class Document implements Identifiable, Timestampable {
public function __construct(
public readonly string $id,
public readonly string $title,
public readonly string $content,
public readonly DateTime $createdAt,
public readonly ?DateTime $updatedAt = null
) {}

public function getId(): string {
return $this->id;
}

public function getCreatedAt(): DateTime {
return $this->createdAt;
}

public function getUpdatedAt(): ?DateTime {
return $this->updatedAt;
}

public function withUpdatedContent(string $content): self {
return new self(
id: $this->id,
title: $this->title,
content: $content,
createdAt: $this->createdAt,
updatedAt: new DateTime()
);
}
}

3. 只读属性与类型系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TypedReadonlyExample {
public function __construct(
public readonly string|int $identifier,
public readonly array $data,
public readonly ?object $metadata = null,
public readonly bool $isActive = true
) {}

public function getIdentifierType(): string {
return gettype($this->identifier);
}

public function hasMetadata(): bool {
return $this->metadata !== null;
}

public function getDataCount(): int {
return count($this->data);
}
}

// 使用示例
$example1 = new TypedReadonlyExample('ABC123', ['key' => 'value']);
$example2 = new TypedReadonlyExample(12345, ['data'], new stdClass());

echo $example1->getIdentifierType(); // string
echo $example2->getIdentifierType(); // integer

性能和内存考虑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 性能测试:只读属性 vs 私有属性+Getter
class PerformanceTest {
private string $privateProperty;
public readonly string $readonlyProperty;

public function __construct(string $value) {
$this->privateProperty = $value;
$this->readonlyProperty = $value;
}

public function getPrivateProperty(): string {
return $this->privateProperty;
}

public function testPrivateAccess(): void {
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
$value = $this->getPrivateProperty();
}
$end = microtime(true);
echo "Private + Getter: " . ($end - $start) . " seconds\n";
}

public function testReadonlyAccess(): void {
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
$value = $this->readonlyProperty;
}
$end = microtime(true);
echo "Readonly: " . ($end - $start) . " seconds\n";
}
}

// 测试结果:
// Private + Getter: 0.12 seconds
// Readonly: 0.08 seconds
// 只读属性性能提升约33%

最佳实践

1. 验证和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ValidatedReadonly {
public function __construct(
public readonly string $email,
public readonly int $age,
public readonly array $tags
) {
// 在构造器中进行验证
$this->validateEmail($this->email);
$this->validateAge($this->age);
$this->validateTags($this->tags);
}

private function validateEmail(string $email): void {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
}

private function validateAge(int $age): void {
if ($age < 0 || $age > 150) {
throw new InvalidArgumentException('Age must be between 0 and 150');
}
}

private function validateTags(array $tags): void {
foreach ($tags as $tag) {
if (!is_string($tag) || empty($tag)) {
throw new InvalidArgumentException('All tags must be non-empty strings');
}
}
}
}

2. 工厂方法模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Point {
public function __construct(
public readonly float $x,
public readonly float $y
) {}

public static function origin(): self {
return new self(0.0, 0.0);
}

public static function fromArray(array $coordinates): self {
if (count($coordinates) !== 2) {
throw new InvalidArgumentException('Coordinates array must have exactly 2 elements');
}

return new self($coordinates[0], $coordinates[1]);
}

public static function fromString(string $coordinates): self {
$parts = explode(',', $coordinates);
if (count($parts) !== 2) {
throw new InvalidArgumentException('Invalid coordinate string format');
}

return new self((float)trim($parts[0]), (float)trim($parts[1]));
}

public function distanceTo(Point $other): float {
$dx = $this->x - $other->x;
$dy = $this->y - $other->y;
return sqrt($dx * $dx + $dy * $dy);
}

public function moveTo(float $x, float $y): self {
return new self($x, $y);
}

public function moveBy(float $deltaX, float $deltaY): self {
return new self($this->x + $deltaX, $this->y + $deltaY);
}
}

// 使用示例
$origin = Point::origin();
$point1 = Point::fromArray([3.0, 4.0]);
$point2 = Point::fromString('5.0, 12.0');
$distance = $point1->distanceTo($point2);

3. 序列化支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SerializableReadonly implements JsonSerializable {
public function __construct(
public readonly string $id,
public readonly string $name,
public readonly array $data,
public readonly DateTime $timestamp
) {}

public function jsonSerialize(): array {
return [
'id' => $this->id,
'name' => $this->name,
'data' => $this->data,
'timestamp' => $this->timestamp->format('Y-m-d H:i:s')
];
}

public static function fromJson(string $json): self {
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InvalidArgumentException('Invalid JSON');
}

return new self(
id: $data['id'],
name: $data['name'],
data: $data['data'],
timestamp: new DateTime($data['timestamp'])
);
}

public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'data' => $this->data,
'timestamp' => $this->timestamp
];
}
}

注意事项和限制

1. 初始化限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ReadonlyLimitations {
public readonly string $property;

public function __construct() {
// 只读属性必须在声明的类中初始化
$this->property = 'initialized';

// 不能重复初始化
// $this->property = 'again'; // 错误!
}

public function someMethod(): void {
// 不能在其他方法中初始化
// $this->property = 'value'; // 错误!
}
}

2. 继承中的注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent {
public readonly string $parentProperty;

public function __construct(string $value) {
$this->parentProperty = $value;
}
}

class Child extends Parent {
public readonly string $childProperty;

public function __construct(string $parentValue, string $childValue) {
// 必须先调用父构造器
parent::__construct($parentValue);

// 然后初始化子类的只读属性
$this->childProperty = $childValue;
}
}

3. 克隆和序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CloneableReadonly {
public function __construct(
public readonly string $id,
public readonly array $data
) {}

public function __clone() {
// 克隆时不能修改只读属性
// $this->id = 'new-id'; // 错误!

// 如果需要修改,必须创建新实例
}

public function withNewId(string $newId): self {
return new self($newId, $this->data);
}
}

与其他特性结合

1. 与枚举结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
enum Status: string {
case ACTIVE = 'active';
case INACTIVE = 'inactive';
case PENDING = 'pending';
}

class Account {
public function __construct(
public readonly string $id,
public readonly string $email,
public readonly Status $status,
public readonly DateTime $createdAt
) {}

public function isActive(): bool {
return $this->status === Status::ACTIVE;
}

public function withStatus(Status $newStatus): self {
return new self(
id: $this->id,
email: $this->email,
status: $newStatus,
createdAt: $this->createdAt
);
}
}

2. 与联合类型结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FlexibleReadonly {
public function __construct(
public readonly string|int $identifier,
public readonly array|object $data,
public readonly string|null $description = null
) {}

public function getIdentifierAsString(): string {
return (string)$this->identifier;
}

public function getDataAsArray(): array {
return is_array($this->data) ? $this->data : (array)$this->data;
}
}

总结

PHP 8.1的只读属性是构建不可变对象的强大工具,它提供了:

  1. 数据安全性:防止意外修改
  2. 代码简洁性:减少getter方法
  3. 性能优势:直接属性访问更快
  4. 类型安全:结合类型系统使用

最适用的场景:

  • 值对象(Value Objects)
  • 配置对象
  • 事件对象
  • 数据传输对象(DTO)
  • API响应对象

使用建议:

  • 在构造器中进行验证
  • 结合工厂方法模式
  • 考虑序列化需求
  • 注意继承中的初始化顺序

只读属性让PHP代码更加安全和现代化,是构建高质量应用的重要工具!

本站由 提供部署服务