3.13. Visitor

3.13.1. Purpose

The Visitor Pattern lets you outsource operations on objects to other objects. The main reason to do this is to keep a separation of concerns. But classes have to define a contract to allow visitors (the Role::accept method in the example).

The contract is an abstract class but you can have also a clean interface. In that case, each Visitor has to choose itself which method to invoke on the visitor.

3.13.2. UML Diagram

Alt Visitor UML Diagram

3.13.3. Code

You can also find this code on GitHub

RoleVisitor.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7/**
 8 * Note: the visitor must not choose itself which method to
 9 * invoke, it is the visited object that makes this decision
10 */
11interface RoleVisitor
12{
13    public function visitUser(User $role);
14
15    public function visitGroup(Group $role);
16}

RecordingVisitor.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7class RecordingVisitor implements RoleVisitor
 8{
 9    /**
10     * @var Role[]
11     */
12    private array $visited = [];
13
14    public function visitGroup(Group $role)
15    {
16        $this->visited[] = $role;
17    }
18
19    public function visitUser(User $role)
20    {
21        $this->visited[] = $role;
22    }
23
24    /**
25     * @return Role[]
26     */
27    public function getVisited(): array
28    {
29        return $this->visited;
30    }
31}

Role.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7interface Role
 8{
 9    public function accept(RoleVisitor $visitor);
10}

User.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7class User implements Role
 8{
 9    public function __construct(private string $name)
10    {
11    }
12
13    public function getName(): string
14    {
15        return sprintf('User %s', $this->name);
16    }
17
18    public function accept(RoleVisitor $visitor)
19    {
20        $visitor->visitUser($this);
21    }
22}

Group.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Visitor;
 6
 7class Group implements Role
 8{
 9    public function __construct(private string $name)
10    {
11    }
12
13    public function getName(): string
14    {
15        return sprintf('Group: %s', $this->name);
16    }
17
18    public function accept(RoleVisitor $visitor)
19    {
20        $visitor->visitGroup($this);
21    }
22}

3.13.4. Test

Tests/VisitorTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Tests\Visitor\Tests;
 6
 7use DesignPatterns\Behavioral\Visitor\RecordingVisitor;
 8use DesignPatterns\Behavioral\Visitor\User;
 9use DesignPatterns\Behavioral\Visitor\Group;
10use DesignPatterns\Behavioral\Visitor\Role;
11use DesignPatterns\Behavioral\Visitor;
12use PHPUnit\Framework\TestCase;
13
14class VisitorTest extends TestCase
15{
16    private RecordingVisitor $visitor;
17
18    protected function setUp(): void
19    {
20        $this->visitor = new RecordingVisitor();
21    }
22
23    public function provideRoles()
24    {
25        return [
26            [new User('Dominik')],
27            [new Group('Administrators')],
28        ];
29    }
30
31    /**
32     * @dataProvider provideRoles
33     */
34    public function testVisitSomeRole(Role $role)
35    {
36        $role->accept($this->visitor);
37        $this->assertSame($role, $this->visitor->getVisited()[0]);
38    }
39}