Implementing Visitor Pattern in PHP and Symfony

Stefano Alletti on 2022-11-15

Introduction

In my latest article I showed my implementation of the Pattern Builder with support of Symfony’s Service Tag. I love this Symfony service, and this time I would like to show you how to use it to implement another design pattern: The Visitor pattern, less known than the Builder pattern but also very nice and useful.

Source: Wikipedia

To do this we start from the excellent example of refactoring.guru, but going to modify it slightly to fit our purpose.

Problem

Imagine you have three entities: Company, Departmentand Employee. And for all of these entities we want to know the salary (for an employee) or the total wage bill for other entities. Each of these entities will have a method (getCost, or getSalary) that will do the job.

But to mutualize the code we want to create a service that has this responsibility. A SalaryReport class will have the task of carrying out the calculation in string format.

// App/Service/SalaryReport.php

final class SalaryReport
{
    public function printTotal($entity): string 
    {
        if ($entity instanceof Company) {
          $output = ... some logic here ...
  
          return $output;
        } 

        if ($entity instanceof Department) {
          $output = ... some logic here ...

          return $output;
        }

        if ($entity instanceof Employee) {
            $output = ... some logic ... here           

            return $output;
        }

        throw new \InvalidArgumentException('SalaryReport: Incorrect type.');
    }
}

Ok perfect this class works, but honestly there are several things that are not right. The first of these does not respect the SOLID principles.

Because there is no shared interface or abstraction between types, when adding on more entities, this type checking will pile on and can become quite cumbersome and unreadable. And we throw an exception when the entity type is unknown to avoid improper use. — https://doeken.org/blog/visitor-pattern

Solution

This is where the Visitor pattern can be very useful.

First let’s create an interface implemented by each of the entities:

// App/Visitor/VisitableInterface.php

interface VisitableInterface
{
    public function accept(VisitorInterface $visitor): string;
}

The accept method takes as parameter another interface (VisitorInterface) which is implemented by the SalaryReport class.

// App/Visitor/VisitorInterface.php

interface VisitorInterface
{
    public function visitCompany(Company $company): string;

    public function visitDepartment(Department $department): string;

    public function visitEmployee(Employee $employee): string;
}

Let’s look at the accept methods of entities now.

// App/Entity/Company.php

public function accept(VisitorInterface $visitor): string
{
   return $visitor->visitCompany($this);
}
// App/Entity/Department.php

public function accept(VisitorInterface $visitor): string
{
   return $visitor->visitDepartment($this);
}
// App/Entity/Employee.php

public function accept(VisitorInterface $visitor): string
{
   return $visitor->visitEmployee($this);
}

And here’s how the SalaryReport class changes now

// App/Service/SalaryReport.php

final class SalaryReport implements VisitorInterface
{
    public function visitCompany(Company $company): string
    {
        $output = ... some logic here ...
  
        return $output;
    }

    public function visitDepartment(Department $department): string
    {
        $output = ... some logic here ...
  
        return $output;
    }

    public function visitEmployee(Employee $employee): string
    {
        $output = ... some logic here ...
  
        return $output;
    }
}

Well, that’s better, methods are typed and each method has a single responsibility. But we can do even better. For example, we don’t want to modify the class every time we add an entity by adding a new method on SalaryReport class, because in this way we don’t respect the O of SOLID.

Why not “borrow” the Director concept from the Builder pattern and use the Symfony Service Tag?

// App/Service/SalaryReportDirectorVisitor.php

final class SalaryReportDirectorVisitor
{
  private iterable $visitors; 

  public function __construct(
    #[TaggedIterator('app.visitor')] iterable $visitors
  ) {
  }
  
  public function visit(VisitableInterface $entity): ?string
  {
    /** @var VisitorInterface $visitor */
    foreach ($this->visitors as $visitor) {
      if ($visitor->support($entity)) {
        return $visitor->visit($entity);
      }
    }
  
    return null;
  }
}

And of course now we’ll have three different classes implementing the same interface.

// App/Visitor/VisitorInterface.php

#[Autoconfigure(tags: ['app.visitor'])]
interface VisitorInterface
{
    public function support(VisitableInterface $entity): bool;
    
    public function visit(VisitableInterface $entity): ?string;
}
// App/Service/CompanySalaryReport.php

final class CompanySalaryReport implements VisitorInterface
{
    public function support(VisitableInterface $entity): bool
    {
      return $entity instanceof Company;
    }
    
    public function visit(VisitableInterface $entity): string
    {
        $output = ... some logic here ...
  
        return $output;
    }
}
// App/Service/DepertementSalaryReport.php

final class DepertementSalaryReport implements VisitorInterface
{
    public function support(VisitableInterface $entity): bool
    {
      return $entity instanceof Depertement;
    }
    
    public function visit(VisitableInterface $entity): string
    {
        $output = ... some logic here ...
  
        return $output;
    }
}
// App/Service/EmployeeSalaryReport.php

final class EmployeeSalaryReport implements VisitorInterface
{
    public function support(Visitable $entity): bool
    {
      return $entity instanceof Employee;
    }
    
    public function visit(Visitable $entity): string
    {
        $output = ... some logic here ...
  
        return $output;
    }
}

The interface implemented by the entities now changes like this

// App/Visitor/VisitableInterface.php

interface VisitableInterface
{
    public function accept(SalaryReportDirectorVisitor $directorVisitor): string;
}

And the accept method will be the same for all entities

// App/Entity/Company.php
// App/Entity/Department.php
// App/Entity/Employee.php

public function accept(SalaryReportDirectorVisitor $directorVisitor): string
{
   return $directorVisitor->visit($this);
}

Conclusion

Using the Symfony service tag we were able to eliminate the drawback of the visitor pattern. In fact we will no longer have to modify the VisitorInterface interface when we add a new Entity. And furthermore, since all entities use the same method, the code can be unified with a Trait or an abstract class.

References