PHP: Strategy

Strategy

Strategy is a behavioral design pattern that turns a set of behaviors into objects and makes them interchangeable inside original context object.

The original object, called context, holds a reference to a strategy object and delegates it executing the behavior. In order to change the way the context performs its work, other objects may replace currently linked strategy object with another one.

More about Strategy

Application of the pattern in PHP

Complexity:

Popularity:

Usage examples: The Strategy pattern is often used in PHP code, especially when it’s necessary to switch algorithms at runtime. However, the pattern has a strong competitor represented by anonymous functions, supported in PHP since 2009.

Example: Structure of the Pattern

This example illustrates the structure of the Strategy design pattern and focuses on following questions:

  • What classes does it consists of?
  • What roles do these classes play?
  • In what way the elements of the pattern are related?

After learning about the pattern's structure it will be easier for you to grasp the following example, based on a real world PHP use case.

StrategyStructural.php: Structural Example

<?php

namespace RefactoringGuru\Strategy\Structural;

/**
 * The Context defines the interface of interest to clients.
 */
class Context
{
    /**
     * @var Strategy The Context maintains a reference to one of the Strategy
     * objects. The Context does not know the concrete class of a strategy. It
     * should work with all strategies via the Strategy interface.
     */
    private $strategy;

    /**
     * Usually, the Context accepts a strategy through the constructor, but also
     * provides a setter to change it at runtime.
     */
    public function __constructor(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
     * Usually, the Context allows replacing a Strategy object at runtime.
     */
    public function setStrategy(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
     * The Context delegates some work to the Strategy object instead of
     * implementing multiple versions of the algorithm on its own.
     */
    public function doSomeBusinessLogic()
    {
        // ...

        print("Context: Sorting data using the strategy (not sure how it'll do it)\n");
        $result = $this->strategy->doAlgorithm(["a", "b", "c", "d", "e"]);
        print(implode(",", $result)."\n");

        // ...
    }
}

/**
 * The Strategy interface declares operations common to all supported versions
 * of some algorithm.
 *
 * The Context uses this interface to call the algorithm defined by Concrete
 * Strategies.
 */
interface Strategy
{
    public function doAlgorithm($data);
}

/**
 * Concrete Strategies implement the algorithm while following the base Strategy
 * interface. The interface makes them interchangeable in the Context.
 */
class ConcreteStrategyA implements Strategy
{
    public function doAlgorithm($data)
    {
        sort($data);

        return $data;
    }
}

class ConcreteStrategyB implements Strategy
{
    public function doAlgorithm($data)
    {
        rsort($data);

        return $data;
    }
}

/**
 * The client code picks a concrete strategy and passes it to the context. The
 * client should be aware of the differences between strategies in order to make
 * the right choice.
 */
$context = new Context();

print("Client: Strategy is set to normal sorting.\n");
$context->setStrategy(new ConcreteStrategyA());
$context->doSomeBusinessLogic();
print("\n");

print("Client: Strategy is set to reverse sorting.\n");
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();

Output.txt: Output

Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

Example: Real World Example

In this example, the Strategy pattern is used to represent payment methods in an e-commerce application.

Each payment method can display a payment form to collect proper payment details from a user and send it to the payment processing company. Then, after the payment processing company redirects the user back to our website, the payment method validates the return parameters and helps to decide whether the order was completed.

StrategyRealWorld.php: Real world example

<?php

namespace RefactoringGuru\Strategy\RealWorld;

/**
 * Strategy Design Pattern
 *
 * Intent: Define a family of algorithms, encapsulate each one, and make them
 * interchangeable. Strategy lets the algorithm vary independently from clients
 * that use it.
 *
 * Example: In this example, the Strategy pattern is used to represent payment
 * methods in an e-commerce application.
 *
 * Each payment method can display a payment form to collect proper payment
 * details from a user and send it to the payment processing company. Then,
 * after the payment processing company redirects the user back to our website,
 * the payment method validates the return parameters and helps to decide
 * whether the order was completed.
 */

/**
 * This is the router and controller of our application. Upon receiving a
 * request, this class decides what behavior should be executed. When the app
 * receives a payment request, the OrderController class also decides which
 * payment method it should use to process the request. Thus, the class acts as
 * the Context and the Client at the same time.
 */
class OrderController
{
    /**
     * Handle POST requests.
     *
     * @param $url
     * @param $data
     * @throws \Exception
     */
    public function post($url, $data)
    {
        print("Controller: POST request to $url with ".json_encode($data)."\n");

        $path = parse_url($url, PHP_URL_PATH);

        if (preg_match('#^/orders?$#', $path, $matches)) {

            $this->postNewOrder($data);

        } else {
            print("Controller: 404 page\n");
        }
    }

    /**
     * Handle GET requests.
     *
     * @param $url
     * @throws \Exception
     */
    public function get($url)
    {
        print("Controller: GET request to $url\n");

        $path = parse_url($url, PHP_URL_PATH);
        $query = parse_url($url, PHP_URL_QUERY);
        parse_str($query, $data);

        if (preg_match('#^/orders?$#', $path, $matches)) {

            $this->getAllOrders();

        } elseif (preg_match('#^/order/([0-9]+?)/payment/([a-z]+?)(/return)?$#', $path, $matches)) {

            $order = Order::get($matches[1]);

            // The payment method (strategy) is selected according to the value
            // passed along with the request.
            $paymentMethod = PaymentFactory::getPaymentMethod($matches[2]);

            if (! isset($matches[3])) {
                $this->getPayment($paymentMethod, $order, $data);
            } else {
                $this->getPaymentReturn($paymentMethod, $order, $data);
            }

        } else {
            print("Controller: 404 page\n");
        }
    }

    /**
     * POST /order {data}
     */
    public function postNewOrder(array $data)
    {
        $order = new Order($data);
        print("Controller: Created the order #{$order->id}.\n");
    }

    /**
     * GET /orders
     */
    public function getAllOrders()
    {
        print("Controller: Here's all orders:\n");
        foreach (Order::get() as $order) {
            print(json_encode($order, JSON_PRETTY_PRINT)."\n");
        }
    }

    /**
     * GET /order/123/payment/XX
     */
    public function getPayment(PaymentMethod $method, Order $order, array $data)
    {
        // The actual work is delegated to the payment method object.
        $form = $method->getPaymentForm($order);
        print("Controller: here's the payment form:\n");
        print($form."\n");
    }

    /**
     * GET /order/123/payment/XXX/return?key=AJHKSJHJ3423&success=true
     */
    public function getPaymentReturn(PaymentMethod $method, Order $order, array $data)
    {
        try {
            // Another type of work delegated to the payment method.
            if ($method->validateReturn($order, $data)) {
                print("Controller: Thanks for your order!\n");
                $order->complete();
            }
        } catch (\Exception $e) {
            print("Controller: got an exception (".$e->getMessage().")\n");
        }
    }
}

/**
 * A simplified representation of the Order class.
 */
class Order
{
    /**
     * For the sake of simplicity, we'll store all created orders here...
     *
     * @var array
     */
    private static $orders = [];

    /**
     * ...and access them from here.
     *
     * @param int $orderId
     * @return mixed
     */
    public static function get(int $orderId = null)
    {
        if ($orderId === null) {
            return static::$orders;
        } else {
            return static::$orders[$orderId];
        }
    }

    /**
     * The Order constructor assigns the values of the order's fields. To keep
     * things simple, there is no validation whatsoever.
     *
     * @param array $attributes
     */
    public function __construct(array $attributes)
    {
        $this->id = count(static::$orders);
        $this->status = "new";
        foreach ($attributes as $key => $value) {
            $this->{$key} = $value;
        }
        static::$orders[$this->id] = $this;
    }

    /**
     * The method to call when an order gets paid.
     */
    public function complete()
    {
        $this->status = "completed";
        print("Order: #{$this->id} is now {$this->status}.");
    }
}

/**
 * This class helps to produce a proper strategy object for handling a payment.
 */
class PaymentFactory
{
    /**
     * Get a payment method by its ID.
     *
     * @param $id
     * @return PaymentMethod
     * @throws \Exception
     */
    public static function getPaymentMethod($id): PaymentMethod
    {
        switch ($id) {
            case "cc":
                return new CreditCardPayment();
            case "paypal":
                return new PayPalPayment();
            default:
                throw new \Exception("Unknown Payment Method");
        }
    }
}

/**
 * The Strategy interface describes how a client can use various Concrete
 * Strategies.
 *
 * Note that in most examples you can find on the Web, strategies tend to do
 * some tiny thing within one method. However, in reality, your strategies can
 * be much more robust (by having several methods, for example).
 */
interface PaymentMethod
{
    public function getPaymentForm(Order $order): string;
    
    public function validateReturn(Order $order, $data): bool;
}

/**
 * This Concrete Strategy provides a payment form and validates returns for
 * credit card payments.
 */
class CreditCardPayment implements PaymentMethod
{
    static private $store_secret_key = "swordfish";

    public function getPaymentForm(Order $order): string
    {
        $returnURL = "https://our-website.com/".
            "order/{$order->id}/payment/cc/return";

        return <<<FORM
<form action="https://my-credit-card-processor.com/charge" method="POST">
    <input type="hidden" id="email" value="{$order->email}">
    <input type="hidden" id="total" value="{$order->total}">
    <input type="hidden" id="returnURL" value="$returnURL">
    <input type="text" id="cardholder-name">
    <input type="text" id="credit-card">
    <input type="text" id="expiration-date">
    <input type="text" id="ccv-number">
    <input type="submit" value="Pay">
</form>
FORM;
    }

    public function validateReturn(Order $order, $data): bool
    {
        print("CreditCardPayment: ...validating... ");

        if ($data['key'] != md5($order->id.static::$store_secret_key)) {
            throw new \Exception("Payment key is wrong.");
        }

        if (! isset($data['success']) || ! $data['success'] || $data['success'] == 'false') {
            throw new \Exception("Payment failed.");
        }

        // ...

        if (floatval($data['total']) < $order->total) {
            throw new \Exception("Payment amount is wrong.");
        }

        print("Done!\n");

        return true;
    }
}

/**
 * This Concrete Strategy provides a payment form and validates returns for
 * PayPal payments.
 */
class PayPalPayment implements PaymentMethod
{
    public function getPaymentForm(Order $order): string
    {
        $returnURL = "https://our-website.com/".
            "order/{$order->id}/payment/paypal/return";

        return <<<FORM
<form action="https://paypal.com/payment" method="POST">
    <input type="hidden" id="email" value="{$order->email}">
    <input type="hidden" id="total" value="{$order->total}">
    <input type="hidden" id="returnURL" value="$returnURL">
    <input type="submit" value="Pay on PayPal">
</form>
FORM;
    }

    public function validateReturn(Order $order, $data): bool
    {
        print("PayPalPayment: ...validating... ");

        // ...

        print("Done!\n");

        return true;
    }
}

/**
 * The client code.
 */

$controller = new OrderController();

print("Client: Let's create some orders\n");

$controller->post("/orders", [
    "email" => "me@example.com",
    "product" => "ABC Cat food (XL)",
    "total" => 9.95,
]);

$controller->post("/orders", [
    "email" => "me@example.com",
    "product" => "XYZ Cat litter (XXL)",
    "total" => 19.95,
]);

print("\nClient: List my orders, please\n");

$controller->get("/orders");

print("\nClient: I'd like to pay for the second, show me the payment form\n");

$controller->get("/order/1/payment/paypal");

print("\nClient: ...pushes the Pay button...\n");
print("\nClient: Oh, I'm redirected to the PayPal.\n");
print("\nClient: ...pays on the PayPal...\n");
print("\nClient: Alright, I'm back with you, guys.\n");

$controller->get("/order/1/payment/paypal/return".
    "?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95");

Output.txt: Output

Client: Let's create some orders
Controller: POST request to /orders with {"email":"me@example.com","product":"ABC Cat food (XL)","total":9.95}
Controller: Created the order #0.
Controller: POST request to /orders with {"email":"me@example.com","product":"XYZ Cat litter (XXL)","total":19.95}
Controller: Created the order #1.

Client: List my orders, please
Controller: GET request to /orders
Controller: Here's all orders:
{
    "id": 0,
    "status": "new",
    "email": "me@example.com",
    "product": "ABC Cat food (XL)",
    "total": 9.95
}
{
    "id": 1,
    "status": "new",
    "email": "me@example.com",
    "product": "XYZ Cat litter (XXL)",
    "total": 19.95
}

Client: I'd like to pay for the second, show me the payment form
Controller: GET request to /order/1/payment/paypal
Controller: here's the payment form:
<form action="https://paypal.com/payment" method="POST">
    <input type="hidden" id="email" value="me@example.com">
    <input type="hidden" id="total" value="19.95">
    <input type="hidden" id="returnURL" value="https://our-website.com/order/1/payment/paypal/return">
    <input type="submit" value="Pay on PayPal">
</form>

Client: ...pushes the Pay button...

Client: Oh, I'm redirected to the PayPal.

Client: ...pays on the PayPal...

Client: Alright, I'm back with you, guys.
Controller: GET request to /order/1/payment/paypal/return?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95
PayPalPayment: ...validating... Done!
Controller: Thanks for your order!
Order: #1 is now completed.