PHP 中的迭代器(实用指南)
每次我看到这个
$users = [new User(), new User()];
我觉得错失了使用迭代器的机会。
为什么需要迭代器?
集合是组织之前未命名数组的绝佳方式。使用迭代器有几个原因。其中一个原因是行为控制,你可以为诸如 `next`、`current`、`valid` 等标准调用指定确切的行为。另一个原因是,你可能希望确保集合只包含特定类型的对象。
了解使用未知值类型数组的弊端。
在 PHP 世界中,数组非常普遍,用于存储各种类型的数据,支持多维嵌套结构。数组为开发者带来了无限的灵活性,但也正因如此,它们变得非常棘手。
例子:
- 您的函数(getUsers)返回一个 User 对象数组。
- 另一个函数(setUsersToActiveState)使用getUsers输出数组,并将所有用户的活动状态设置为true。
- setUsersToActiveState 循环遍历数组,并期望调用数组项上的特定方法。例如,该方法名为 getActiveStatus。
- 如果给定的数组是包含所需对象的数组,且这些对象都具有可调用的 getActiveStatus 方法,则一切正常。否则,将会抛出异常。
- 如何确保给定的数组始终是特定类型的对象数组?
public function getUsers(): array
{
/**
here happen something which gets users from database
....
**/
return $userArray;
}
public function setUsersToActiveState()
{
$users = $this->getUsers();
/** @var User $param */
foreach ($users as $user) {
if(!$user->getActiveStatus()) {
$user->setActiveStatus(true);
}
}
}
立即出现了两个问题。
- 一是类型问题。我们的 IDE 不知道 `$users` 数组里包含什么,所以无法告诉我们如何使用 `$user` 元素。(我在 `foreach` 循环上方添加了注释块 `/** @var User $param */`,它在 phpStorm 中有效,我猜在其他一些 IDE 中也有效。)
- 你的同事们!在没有任何提示的情况下,他们怎么可能知道数组里是什么呢?
- 附加问题:getUsers 可以返回任何数组,系统不会发出警告。
解决方案
// Create a collection which accepts only Users
class UsersCollection implements \IteratorAggregate
{
/** @var array */
private $users = [];
public function getIterator() : UserIterator
{
return new UserIterator($this);
}
public function getUser($position)
{
if (isset($this->users[$position])) {
return $this->users[$position];
}
return null;
}
public function count() : int
{
return count($this->users);
}
public function addUser(User $users)
{
$this->users[] = $users;
}
}
// Create an Iterator for User
class UserIterator implements \Iterator
{
/** @var int */
private $position = 0;
/** @var UsersCollection */
private $userCollection;
public function __construct(UsersCollection $userCollection)
{
$this->userCollection = $userCollection;
}
public function current() : User
{
return $this->userCollection->getUser($this->position);
}
public function next()
{
$this->position++;
}
public function key() : int
{
return $this->position;
}
public function valid() : bool
{
return !is_null($this->userCollection->getUser($this->position));
}
public function rewind()
{
$this->position = 0;
}
}
测试
当然,我们还需要编写测试来确保集合和迭代器运行良好。在这个例子中,我使用的是 PHPUnit 框架的语法。
class UsersCollectionTest extends TestCase
{
/**
* @covers UsersCollection
*/
public function testUsersCollectionShouldReturnNullForNotExistingUserPosition()
{
$usersCollection = new UsersCollection();
$this->assertEquals(null, $usersCollection->getUser(1));
}
/**
* @covers UsersCollection
*/
public function testEmptyUsersCollection()
{
$usersCollection = new UsersCollection();
$this->assertEquals(new UserIterator($usersCollection), $usersCollection->getIterator());
$this->assertEquals(0, $usersCollection->count());
}
/**
* @covers UsersCollection
*/
public function testUsersCollectionWithUserElements()
{
$usersCollection = new UsersCollection();
$usersCollection->addUser($this->getUserMock());
$usersCollection->addUser($this->getUserMock());
$this->assertEquals(new UserIterator($usersCollection), $usersCollection->getIterator());
$this->assertEquals($this->getUserMock(), $usersCollection->getUser(1));
$this->assertEquals(2, $usersCollection->count());
}
private function getUserMock()
{
// returns the mock of User class
}
}
class UserIteratorTest extends MockClass
{
/**
* @covers UserIterator
*/
public function testCurrent()
{
$iterator = $this->getIterator();
$current = $iterator->current();
$this->assertEquals($this->getUserMock(), $current);
}
/**
* @covers UserIterator
*/
public function testNext()
{
$iterator = $this->getIterator();
$iterator->next();
$this->assertEquals(1, $iterator->key());
}
/**
* @covers UserIterator
*/
public function testKey()
{
$iterator = $this->getIterator();
$iterator->next();
$iterator->next();
$this->assertEquals(2, $iterator->key());
}
/**
* @covers UserIterator
*/
public function testValidIfItemInvalid()
{
$iterator = $this->getIterator();
$iterator->next();
$iterator->next();
$iterator->next();
$this->assertEquals(false, $iterator->valid());
}
/**
* @covers UserIterator
*/
public function testValidIfItemIsValid()
{
$iterator = $this->getIterator();
$iterator->next();
$this->assertEquals(true, $iterator->valid());
}
/**
* @covers UserIterator
*/
public function testRewind()
{
$iterator = $this->getIterator();
$iterator->rewind();
$this->assertEquals(0, $iterator->key());
}
private function getIterator() : UserIterator
{
return new UserIterator($this->getCollection());
}
private function getCollection() : UsersCollection
{
$userItems[] = $this->getUserMock();
$userItems[] = $this->getUserMock();
$usersCollection = new UsersCollection();
foreach ($userItems as $user) {
$usersCollection->addUser($user);
}
return $usersCollection;
}
private function getUserMock()
{
// returns the mock of User class
}
}
用法
public function getUsers(): UsersCollection
{
$userCollection = new UsersCollection();
/**
here happen something which gets users from database
....
**/
foreach ($whatIGetFromDatabase as $user) {
$userCollection->addUser($user);
}
return $userCollection;
}
public fucntion setUsersToActiveState()
{
$users = $this->getUsers();
foreach ($users as $user) {
if(!$user->getActiveStatus()) {
$user->setActiveStatus(true);
}
}
}
如您所见,setUsersToActiveState 保持不变,我们只是不需要为我们的 IDE 或同事指定 $users 变量的类型。
扩展功能
信不信由你,你可以重用这两个对象,只需更改变量名即可满足大多数需求。但如果你需要更复杂的功能,可以将其添加到迭代器或集合中。
例 1
例如,假设 userCollection 只接受年龄大于 18 岁的用户。实现将在 UsersCollection 类的 addUser 方法中进行。
public function addUser(User $users)
{
if ($user->getAge() > 18) {
$this->users[] = $users;
}
}
例 2
您需要批量添加用户。然后,您可以使用额外的 addUsers 方法扩展您的 userCollection,代码可能如下所示。
public function addUsers(array $users)
{
foreach($users as $user) {
$this->addUser(User $users);
}
}
笔记
我找到了一篇很棒的文章,它解答了为什么通常不建议返回数组,我完全同意@aleksikauppila在这个问题上的观点。
文章来源:https://dev.to/damnjan/iterator-in-php-a-practical-guide-2k59