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).
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]);
}