发布于 2026-01-06 0 阅读
0

PHP 中的迭代器(实用指南)

PHP 中的迭代器(实用指南)

每次我看到这个

$users = [new User(), new User()];
Enter fullscreen mode Exit fullscreen mode

我觉得错失了使用迭代器的机会。

为什么需要迭代器?

集合是组织之前未命名数组的绝佳方式。使用迭代器有几个原因。其中一个原因是行为控制,你可以为诸如 `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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

立即出现了两个问题。

  1. 一是类型问题。我们的 IDE 不知道 `$users` 数组里包含什么,所以无法告诉我们如何使用 `$user` 元素。(我在 `foreach` 循环上方添加了注释块 `/** @var User $param */`,它在 phpStorm 中有效,我猜在其他一些 IDE 中也有效。)
  2. 你的同事们!在没有任何提示的情况下,他们怎么可能知道数组里是什么呢?
  3. 附加问题: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;
    }
}
Enter fullscreen mode Exit fullscreen mode

测试

当然,我们还需要编写测试来确保集合和迭代器运行良好。在这个例子中,我使用的是 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
    }
}

Enter fullscreen mode Exit fullscreen mode

用法

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

如您所见,setUsersToActiveState 保持不变,我们只是不需要为我们的 IDE 或同事指定 $users 变量的类型。

扩展功能

信不信由你,你可以重用这两个对象,只需更改变量名即可满足大多数需求。但如果你需要更复杂的功能,可以将其添加到迭代器或集合中。

例 1

例如,假设 userCollection 只接受年龄大于 18 岁的用户。实现将在 UsersCollection 类的 addUser 方法中进行。

    public function addUser(User $users)
    {
        if ($user->getAge() > 18) {
            $this->users[] = $users;
        }    
    }

Enter fullscreen mode Exit fullscreen mode

例 2

您需要批量添加用户。然后,您可以使用额外的 addUsers 方法扩展您的 userCollection,代码可能如下所示。


    public function addUsers(array $users)
    {
        foreach($users as $user) {
            $this->addUser(User $users);
        }
    }
Enter fullscreen mode Exit fullscreen mode

笔记

我找到了一篇很棒的文章,它解答了为什么通常不建议返回数组,我完全同意@aleksikauppila在这个问题上的观点。

文章来源:https://dev.to/damnjan/iterator-in-php-a-practical-guide-2k59