Design Pattern: Visitor Pattern in PHP

Visitor pattern moves the calculation/operation to a separate class. This way, we can define a new operation without changing the related classes.

This article demonstrates Visitor pattern implementations in PHP. Check the following examples.

Implementation

Check the following steps for Visitor pattern implementation in PHP-

  • Create an interface for the element/item classes. Declare a method that accepts a visitor object.
  • Create element/item classes and implement the item interface. There can be other properties and methods as per the requirement of that class.
  • Create visitor interface, and declare methods for adding/processing elements.
  • Create visitor class and implement the interface. Define methods declared in the interface. Use properties to handle the processing of the content of the elements.

Here is a simple Visitor pattern implementation-

<?php
// Visitor pattern implementation in PHP

interface ElementInterface {
    function visit(VisitorInterface $visitor): void;
}

class Element1 implements ElementInterface {
    public function __construct(private string $elementContent) {

    }

    public function getContent(): string{
        return $this->elementContent;
    }

    public function visit(VisitorInterface $visitor): void {
        $visitor->addContent($this);
    }
}

class Element2 implements ElementInterface {
    public function __construct(private string $elementContent, private bool $addMergin = false) {

    }

    public function getContent(): string{
        return $this->elementContent;
    }

    public function getMargin(): bool{
        return $this->addMergin;
    }

    public function visit(VisitorInterface $visitor): void {
        $visitor->addContentWithMargin($this);
    }
}


interface VisitorInterface {
    function addContent(Element1 $element): void;
    function addContentWithMargin(Element2 $element): void;
}

class Visitor implements VisitorInterface {
    public string $output = '';

    public function addContent(Element1 $element1): void {
        $this->output .= $element1->getContent();
    }

    public function addContentWithMargin(Element2 $element2): void {
        if ($element2->getMargin()) {
            $this->output .= '[margin]' . $element2->getContent() . '[/margin]';
        } else {
            $this->output .= $element2->getContent();
        }
    }    
}


// list of elements
$elements = [
    new Element1("Element1: first element"),
    new Element2("Element2: second element without margin"),
    new Element2("Element2: third element with margin", true),
    new Element1("Element1: last element"),
];

$visitior = new Visitor();

foreach ($elements as $element) {
    $element->visit($visitior);
}

echo $visitior->output;

Output will be as below-

Element1: first element
Element2: second element without margin
[margin]Element2: third element with margin[/margin]
Element1: last element

Examples

Check the following example of Visitor pattern-

Example #1: Hosting Cost Calculator

In this example we are calculating costing of a hosting provider. There are services like – compute, database, serverless, storage, etc. provided by the hosting.

We want to use the total cost of hosting based on the service used, by implementing Visitor pattern.

Service Interface

  • Create file “Service.php”.
  • Define interface “Service”.
  • Declare method “accept” which accepts a visitor object.
<?php
// Service.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

interface Service {
    public function accept(HostingCalculatorVisitor $hostingCalculatorVisitor): float;
}

Compute Service Class

  • Create file “ComputeService.php”.
  • Define class “ComputeService”.
  • Implement the “Service” interface.
  • Define properties “PRICE” and “$quantity”.
  • In the constructor accept and set “$quantity”.
  • Define getter methods for price and quantity.
  • Define “accept” method as part of the interface implementation. Call the processing method from the visitor object, and pass the current object($this).
<?php
// ComputeService.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

class ComputeService implements Service {
    private const PRICE = 10.50;
    private int $quantity;

    public function __construct(int $quantity) {
        $this->quantity = $quantity;
    }

    public function getPrice(): float {
        return self::PRICE;
    }

    public function getQuantity(): int {
        return $this->quantity;
    }

    public function accept(HostingCalculatorVisitor $hostingCalculatorVisitor): float {
        return $hostingCalculatorVisitor->visitComputeService($this);
    }
}

Database Service Class

  • Create file “DatabaseService.php”.
  • Define class “DatabaseService” and implement the “Service” interface.
  • Define properties and methods as per the class requirement.
<?php
// DatabaseService.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

class DatabaseService implements Service {
    private const PRICE = 100.00;
    private const BACKUP_PRICE = 30.00;
    private int $quantity;
    private bool $backupEnabled;

    public function __construct(int $quantity, bool $backupEnabled = false) {
        $this->quantity = $quantity;
        $this->backupEnabled = $backupEnabled;
    }

    public function getPrice(): float {
        return self::PRICE;
    }

    public function getQuantity(): int {
        return $this->quantity;
    }

    public function getBackupPrice(): float {
        return self::BACKUP_PRICE;
    }

    public function isBackupEnabled(): bool {
        return $this->backupEnabled;
    }

    public function accept(HostingCalculatorVisitor $hostingCalculatorVisitor): float {
        return $hostingCalculatorVisitor->visitDatabaseService($this);
    }
}

File Storage Service Class

  • Create file “FileStorageService.php”.
  • Define class “FileStorageService” and implement the “Service” interface.
  • Define properties and methods as per the class requirement.
<?php
// FileStorageService.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;


class FileStorageService implements Service {
    private const PRICE_PER_GB = 1.70;
    private int $quantity;

    public function __construct(int $quantity) {
        $this->quantity = $quantity;
    }

    public function getPricePerGB(): float {
        return self::PRICE_PER_GB;
    }

    public function getQuantity(): int {
        return $this->quantity;
    }

    public function accept(HostingCalculatorVisitor $hostingCalculatorVisitor): float {
        return $hostingCalculatorVisitor->visitFileStorageService($this);
    }
}

Serverless Service Class

  • Create file “ServerlessService.php”.
  • Define class “ServerlessService” and implement the “Service” interface.
  • Define properties and methods as per the class requirement.
<?php
// ServerlessService.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

class ServerlessService implements Service {
    private const HOURLY_PRICE = 0.32;
    private int $totalHours;

    public function __construct(int $totalHours) {
        $this->totalHours = $totalHours;
    }

    public function getHourlyPrice(): float {
        return self::HOURLY_PRICE;
    }

    public function getTotalHours(): int {
        return $this->totalHours;
    }

    public function accept(HostingCalculatorVisitor $hostingCalculatorVisitor): float {
        return $hostingCalculatorVisitor->visitServerlessService($this);
    }
}

Container Service Class

  • Create file “ContainerService.php”.
  • Define class “ContainerService” and implement the “Service” interface.
  • Define properties and methods as per the class requirement.
<?php
// ContainerService.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

class ContainerService implements Service {
    private const PRICE = 5.60;
    private int $quantity;

    public function __construct(int $quantity) {
        $this->quantity = $quantity;
    }

    public function getPrice(): float {
        return self::PRICE;
    }

    public function getQuantity(): int {
        return $this->quantity;
    }

    public function accept(HostingCalculatorVisitor $hostingCalculatorVisitor): float {
        return $hostingCalculatorVisitor->visitContainerService($this);
    }
}

Visitor Interface

  • Create file “HostingCalculatorVisitor.php”.
  • Define interface “HostingCalculatorVisitor”.
  • Declare methods for calculation of different services, like – “visitComputService”, “visitDatabaseService”, “visitFileStorageService” etc. These methods will be used to perform the main calculation for each service.
<?php
// HostingCalculatorVisitor.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

interface HostingCalculatorVisitor {
    public function visitComputeService(ComputeService $computeService): float;
    public function visitDatabaseService(DatabaseService $databaseService): float;
    public function visitFileStorageService(FileStorageService $fileStorageService): float;
    public function visitServerlessService(ServerlessService $serverlessService): float;
    public function visitContainerService(ContainerService $containerService): float;
}

Visitor Interface Implementation

  • Create file “HostingCalculatorVisitorImpl.php”.
  • Define class “HostingCalculatorVisitorImpl”.
  • Implement the visitor interface “HostingCalculatorVisitor” for the class.
  • In the method implementations calculate the prices for relevant services.
<?php
// HostingCalculatorVisitorImpml.php

namespace BigBoxCode\DesignPattern\Visitor\HostingCost;

class HostingCalculatorVisitorImpl implements HostingCalculatorVisitor {
    public function visitComputeService(ComputeService $computeService): float {
        return $computeService->getPrice() * $computeService->getQuantity();
    }

    public function visitDatabaseService(DatabaseService $databaseService): float {
        $serviceCost = $databaseService->getPrice() * $databaseService->getQuantity();
        $backupCost = 0;

        if ($databaseService->isBackupEnabled()) {
            $backupCost = $databaseService->getBackupPrice() * $databaseService->getQuantity();
        }

        return $serviceCost + $backupCost;
    }

    public function visitFileStorageService(FileStorageService $fileStorageService): float {
        return $fileStorageService->getPricePerGB() * $fileStorageService->getQuantity();
    }

    public function visitServerlessService(ServerlessService $serverlessService): float {
        return $serverlessService->getHourlyPrice() * $serverlessService->getTotalHours();
    }

    public function visitContainerService(ContainerService $containerService): float {
        return $containerService->getPrice() * $containerService->getQuantity();
    }
}

Demo

In the client, define which services we want to use, and add a service object to your list of services. Then call the “accept” method to obtain the calculated price.

<?php
// demo.php

require __DIR__ . '/../../vendor/autoload.php';

use BigBoxCode\DesignPattern\Visitor\HostingCost\ComputeService;
use BigBoxCode\DesignPattern\Visitor\HostingCost\ContainerService;
use BigBoxCode\DesignPattern\Visitor\HostingCost\DatabaseService;
use BigBoxCode\DesignPattern\Visitor\HostingCost\FileStorageService;
use BigBoxCode\DesignPattern\Visitor\HostingCost\HostingCalculatorVisitorImpl;
use BigBoxCode\DesignPattern\Visitor\HostingCost\ServerlessService;

// Utility function calculating the hosting cost
// This is not required for the Visitor pattern implementation
// This uses the visitor pattern implementation
function calculateHostingCost(array $services): float {
    $hostingCalculatorVisitorImpl = new HostingCalculatorVisitorImpl();

    $totalCost = 0;

    foreach ($services as $service) {
        $totalCost += $service->accept($hostingCalculatorVisitorImpl);
    }

    return $totalCost;
}


// Demo
$usedServices = [
    new ComputeService(3),
    new DatabaseService(3, true),
    new FileStorageService(120),
    new ServerlessService(720),
    new ContainerService(2),
];

$totalCost = calculateHostingCost($usedServices);

echo "Total cost of hosting is: " . $totalCost . "\n";

Output

Output of the total price of hosting will be as below-

Total cost of hosting is: 867.1

Example #1: UI Elements

In this example, we are considering a system for printing UI Elements.

UI Element Interface

  • Create file “UiElement.php”.
  • Define interface “UiElement”.
  • Declare method “appendElement” which accepts a visitor object.
<?php
// UiElement.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;

interface UIElement {
    public function appendElement(Visitor $visitor): void;
}

Text Element Class

  • Create file “TextElement.php”.
  • Define class “TextElement “.
  • Implement “UIElement” interface for the class. In the “appendElement” method implementation call the “appendContent” from the visitor object, and pass the current object($this).
<?php
// TextElement.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;

class TextElement implements UIElement {
    private string $text;

    public function __construct(string $text) {
        $this->text = $text;
    }

    public function getText(): string {
        return $this->text;
    }

    public function appendElement(Visitor $visitor): void {
        $visitor->appendContent($this);
    }
}

Wrap Element Class

  • Create file “WrapElement.php”.
  • Define class “WrapElement” and implement interface “UIElement” for the class.
<?php
// WrapElement.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;

class WrapElement implements UIElement {
    private string $text;
    private string $wrapper;

    public function __construct(string $text, string $wrapper) {
        $this->text = $text;
        $this->wrapper = $wrapper;
    }

    public function getText(): string {
        return $this->text;
    }

    public function getWrapper(): string {
        return $this->wrapper;
    }

    public function appendElement(Visitor $visitor): void {
        $visitor->appendContentWithWrapper($this);
    }
}

Head Element Class

  • Create file “HeadElement.php”.
  • Define class “HeadElement” and implement interface “UIElement” for the class.
<?php
// HeadElement.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;

class HeadElement implements UIElement {
    private string $text;
    private string $wrapper = 'h1';

    public function __construct(string $text) {
        $this->text = $text;
    }

    public function getText(): string {
        return $this->text;
    }

    public function getWrapper(): string {
        return $this->wrapper;
    }

    public function appendElement(Visitor $visitor): void {
        $visitor->appendContentWithWrapper($this);
    }
}

List Element Class

  • Create file “ListElement.php”.
  • Define class “ListElement” and implement interface “UIElement” for the class.
<?php
// ListElement.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;

class ListElement implements UIElement {
    private array $lines;

    public function __construct(array $lines) {
        $this->lines = $lines;
    }

    public function getListItems(): array {
        return $this->lines;
    }

    public function appendElement(Visitor $visitor): void {
        $visitor->appendList($this);
    }
}

Visitor Interface

  • Create file “Visitor.php”.
  • Define interface “Visitor”.
  • Declare methods for the content processing, like – “appendContent”, “appendContentWithWrapper”, “appendList”.
<?php
// Visitor.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;


interface Visitor {
    public function appendContent(TextElement $textElement): void;
    public function appendContentWithWrapper(WrapElement | HeadElement $wrapElement): void;
    public function appendList(ListElement $listElement): void;
}

Visitor Interface Implementation

  • Create file “ElementVisitor.php”.
  • Define class “ElementVisitor”.
  • Implement interface “Visitor”. In the method implementation write relevant code for content processing/structuring.
<?php
// ElementVisitor.php

namespace BigBoxCode\DesignPattern\Visitor\UiElement;

class ElementVisitor implements Visitor {
    public string $output = '';

    public function appendContent(TextElement $textElement): void {
        $this->output .= $textElement->getText();
    }

    public function appendContentWithWrapper(WrapElement | HeadElement $wrapElement): void {
        $this->output .= "[" . $wrapElement->getWrapper() . "] " . $wrapElement->getText() . " [/" . $wrapElement->getWrapper() . "]";
    }

    public function appendList(ListElement $listElement): void {
        $this->output .= '[ul]';

        foreach ($listElement->getListItems() as $listItem) {
            $this->output .= "[li] $listItem [/li]";
        }

        $this->output .= '[/ul]';
    }
}

Demo

The client can decide to use a bunch of elements, and we need to generate an object of those elements. Then we can call the “appendElement” method from those objects to to generate those UI elements

<?php
// demo.php

require __DIR__ . '/../../vendor/autoload.php';

use BigBoxCode\DesignPattern\Visitor\UiElement\ElementVisitor;
use BigBoxCode\DesignPattern\Visitor\UiElement\HeadElement;
use BigBoxCode\DesignPattern\Visitor\UiElement\ListElement;
use BigBoxCode\DesignPattern\Visitor\UiElement\TextElement;
use BigBoxCode\DesignPattern\Visitor\UiElement\WrapElement;

$uiElements = [
    new HeadElement('My Heading'),
    new TextElement('First line of text'),
    new ListElement(['abc', 'def', 'ghi', 'jkl']),
    new WrapElement('Content wrapped with div', 'div'),
    new TextElement('Last line of text'),
];


$visitor = new ElementVisitor();

foreach ($uiElements as $element) {
    $element->appendElement($visitor);
}

echo $visitor->output;

Output

Following output will be generated after processing those UI elements-

[h1] My Heading [/h1]

First line of text

[ul]
    [li] abc [/li]
    [li] def [/li]
    [li] ghi [/li]
    [li] jkl [/li]
[/ul]

[div] Content wrapped with div [/div]

Last line of text

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check Visitor pattern implementation in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.