数组不解释其内容
本机数组可以表示列表、映射或键和值的混合。变量名本身并不是契约。
一个 列表 使用从零开始的连续整数键。一个 地图 将键与值关联起来。 PHP 使用相同的 array 两者的类型,所以这个参数声明说得很少:
<?php
declare(strict_types=1);
final class User
{
public function __construct(
public readonly string $email,
) {}
}
function sendWelcomeEmails(array $users): void
{
foreach ($users as $user) {
echo $user->email, PHP_EOL;
}
}
sendWelcomeEmails([new User('owner@example.com')]);读者必须推断 $users 应该是一个列表 User 对象。 IDE 或静态分析器可以从附近的代码推断出一些值,但是当数组跨越方法、服务或外部边界时,该信息变得不太可靠。
首先选择最简单的工具
仅当类添加代码实际需要的契约或行为时才有用。
注释本地列表
使用 list<User> 或 array<int, User> 对于一个短暂的数组。 IDE 和分析器无需新的运行时对象即可理解预期值。
描述可重用的泛型
PHPStan 和 Psalm 了解模板注释,例如 @template T of object。这可以在分析过程中跨多种对象类型扩展一个集合实现。
强制执行域边界
创建 UserCollection 当值跨越边界时,错误的值必须在运行时失败,或者列表有其自己的有用行为。
PHP 8.1 不提供本机用户态泛型。模板注解是开发工具的契约; PHP 本身并不强制执行它们。下面的集合添加了运行时检查。
包裹数组并保持合约较小
使用标准接口,以便对象支持 foreach、索引访问,以及 count() 无需手动管理迭代器位置。
IteratorAggregate返回内部列表的可遍历视图。没有需要维护的可变迭代器游标。
ArrayAccess支持 $users[0], 替换, 附加为 $users[]和移除。
Countable让我们 count($users) 返回存储的对象的数量。
list<User>该类验证每个插入并在删除后保留连续的整数键。
方法签名必须与 PHP 的接口匹配:偏移量和插入的值作为 mixed。实现在使用前检查它们。仅当以下情况时才会发生追加 $offset === null,所以索引 0 永远不会被误认为是空值。
像使用焦点列表一样使用集合
构造、追加、索引访问、迭代和计数仍然是熟悉的。
<?php
$users = new UserCollection([
new User('Ada', 'ada@example.com'),
new User('Linus', 'linus@example.com'),
]);
$users[] = new User('Grace', 'grace@example.com');
$users[0] = new User('Ada Lovelace', 'ada@example.com');
echo $users[0]->name, PHP_EOL;
echo count($users), PHP_EOL;
foreach ($users as $user) {
echo $user->email, PHP_EOL;
}因为 offsetGet() 回报 User 和 getIterator() 被记录为 Traversable<int, User>,许多 IDE 可以提供成员完成功能 $users[0]->name 和 $user->email。确切的行为取决于 IDE 及其分析设置。
当重用证明合理时添加静态分析模板
混凝土 UserCollection 最容易阅读。仅当多个集合共享相同的机制时,通用基础才变得有用。
PHPStan 和 Psalm 可以使用注释对基本集合进行建模,例如 @template T of object, @implements IteratorAggregate<int, T>, 和 @implements ArrayAccess<int, T>。然后绑定一个子类 T 到 User.
这些注释不会创建运行时泛型。如果在 PHP 执行时必须强制执行值,那么可重用的基础仍然需要可靠的运行时验证器(例如子类传递的类字符串)。对于不运行静态分析器的团队,首先保留具体实现。
类型安全有什么作用,没有什么作用
元素类型合约可以防止一类错误。它不是一般的安全边界。
一种方法接受 UserCollection 表明它需要一组有序的用户。
尝试插入另一个对象会立即失败 InvalidArgumentException.
返回和迭代器注释为 IDE 和静态分析器提供有关每个元素的更多信息。
有效的 User 对象仍然可以包含不安全的名称、电子邮件或其他值。
知道一个对象是一个 User 并不能证明当前请求者可以查看或更改它。
包装数组会添加方法调用和验证。在做出性能声明之前对您的实际工作负载进行基准测试。
当列表中有作业时使用集合
当数组是本地临时数组时,首选普通类型参数加上 PHPDoc。
这些值跨越控制器、服务、存储库或 API 边界;运行时拒绝很重要;或者列表拥有诸如查找活跃用户或强制唯一性等行为。
该列表存在于一个小函数内,不需要行为,并且已经由配置的静态分析器检查。
避免让集合模仿每个数组函数。添加域方法,使调用代码更清晰。如果对象变成了排序、过滤、持久性和表示行为的杂货包,则拆分这些职责。
完整的 PHP 8.1 示例
这个独立的脚本测试构造、附加、索引零处的替换、迭代、计数、无效类型拒绝和超出范围的访问。
<?php
declare(strict_types=1);
final class User
{
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
/**
* @implements IteratorAggregate<int, User>
* @implements ArrayAccess<int, User>
*/
final class UserCollection implements IteratorAggregate, ArrayAccess, Countable
{
/** @var list<User> */
private array $items = [];
/** @param iterable<User> $users */
public function __construct(iterable $users = [])
{
foreach ($users as $user) {
$this[] = $user;
}
}
/** @return Traversable<int, User> */
public function getIterator(): Traversable
{
yield from $this->items;
}
public function count(): int
{
return count($this->items);
}
public function offsetExists(mixed $offset): bool
{
return is_int($offset)
&& array_key_exists($offset, $this->items);
}
public function offsetGet(mixed $offset): User
{
if (!$this->offsetExists($offset)) {
throw new OutOfBoundsException('Unknown user index.');
}
return $this->items[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
if (!$value instanceof User) {
throw new InvalidArgumentException(
'UserCollection accepts only User objects.'
);
}
if ($offset === null) {
$this->items[] = $value;
return;
}
if (!is_int($offset) || $offset < 0 || $offset > count($this->items)) {
throw new OutOfBoundsException('Invalid user index.');
}
$this->items[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
if (!$this->offsetExists($offset)) {
throw new OutOfBoundsException('Unknown user index.');
}
array_splice($this->items, $offset, 1);
}
}
$check = static function (bool $condition, string $message): void {
if (!$condition) {
throw new RuntimeException($message);
}
};
$users = new UserCollection([
new User('Ada', 'ada@example.com'),
new User('Linus', 'linus@example.com'),
]);
$users[] = new User('Grace', 'grace@example.com');
$check(count($users) === 3, 'Append or count failed.');
$users[0] = new User('Ada Lovelace', 'ada@example.com');
$check($users[0]->name === 'Ada Lovelace', 'Index zero failed.');
$names = [];
foreach ($users as $user) {
$names[] = $user->name;
}
$check($names === ['Ada Lovelace', 'Linus', 'Grace'], 'Iteration failed.');
$wrongTypeRejected = false;
try {
$users[] = new stdClass();
} catch (InvalidArgumentException) {
$wrongTypeRejected = true;
}
$check($wrongTypeRejected, 'Invalid object type was accepted.');
$outOfRangeRejected = false;
try {
$users[99];
} catch (OutOfBoundsException) {
$outOfRangeRejected = true;
}
$check($outOfRangeRejected, 'Out-of-range access was accepted.');
echo "All UserCollection checks passed.", PHP_EOL;将解码后的块保存为 user-collection.php,然后运行 php user-collection.php。它应该打印“所有 UserCollection 检查已通过”。
参考文献
- PHP 手册:ArrayAccess
所需的签名和数组样式的对象访问。
- PHP 手册:IteratorAggregate
外部迭代通过
getIterator(). - PHPStan PHPDoc 类型 和 诗篇数组类型
工具级列表、数组形状和通用类型语法。


