Visitor Pattern

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.


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.

An approach to add objects (known as roles) to other objects (known as visitors). The visitor object visits a role object and in doing so, the role becomes attached to the visitor (allowing it to access its methods and properties).

UML


Code

Role


namespace DesignPatterns\Behavioral\Visitor;

interface Role
{
    public function accept(RoleVisitor $visitor);
}

Group


namespace DesignPatterns\Behavioral\Visitor;

class Group implements Role
{
    public function __construct(private string $name)
    {
    }

    public function getName(): string
    {
        return sprintf('Group: %s', $this->name);
    }

    public function accept(RoleVisitor $visitor)
    {
        $visitor->visitGroup($this);
    }
}

User


namespace DesignPatterns\Behavioral\Visitor;

class User implements Role
{
    public function __construct(private string $name)
    {
    }

    public function getName(): string
    {
        return sprintf('User %s', $this->name);
    }

    public function accept(RoleVisitor $visitor)
    {
        $visitor->visitUser($this);
    }
}

RoleVisitor


namespace DesignPatterns\Behavioral\Visitor;

/**
* Note: the visitor must not choose itself which method to
* invoke, it is the visited object that makes this decision
*/
interface RoleVisitor
{
    public function visitUser(User $role);
    public function visitGroup(Group $role);
}

RecordingVisitor


namespace DesignPatterns\Behavioral\Visitor;

class RecordingVisitor implements RoleVisitor
{
   /**
    * @var Role[]
    */
    private array $visited = [];

    public function visitGroup(Group $role)
    {
        $this->visited[] = $role;
    }

    public function visitUser(User $role)
    {
        $this->visited[] = $role;
    }

    /**
     * @return Role[]
     */
    public function getVisited(): array
    {
        return $this->visited;
    }
}

Tests


private RecordingVisitor $visitor;

protected function setUp(): void
{
    $this->visitor = new RecordingVisitor();
}

public function provideRoles()
{
    return [
        [new User('Dominik')],
        [new Group('Administrators')],
    ];
}

/**
* @dataProvider provideRoles
*/
public function testVisitSomeRole(Role $role)
{ 
    $role->accept($this->visitor);
    $this->assertSame($role, $this->visitor->getVisited()[0]);
}