![Strategia](/images/patterns/cards/strategy-mini.png?id=d38abee4fb6f2aed909d262bdadca936)
Strategia w języku PHP
Strategia to behawioralny wzorzec projektowy zakładający przekształcenie zestawu zachowań w obiekty, które można stosować zamiennie w pierwotnym obiekcie.
Pierwotny obiekt, zwany kontekstem, przechowuje odniesienie do obiektu-strategii i deleguje mu działania związane z danym zachowaniem. Aby zmienić sposób, w jaki kontekst wykonuje swą pracę, należy zamienić bieżąco przypisany obiekt strategii na inny.
Złożoność:
Popularność:
Przykłady użycia: Wzorzec Strategia jest często stosowany w kodzie PHP, szczególnie gdy istnieje potrzeba zamiany algorytmu w trakcie działania programu. Wzorzec ten jednak konkuruje z funkcjami anonimowymi, obsługiwanymi przez PHP od 2009 roku.
Identyfikacja: Wzorzec Strategia można rozpoznać po obecności metody pozwalającej wykonywać faktyczną pracę zagnieżdżonemu obiektowi oraz po obecności settera umożliwiającego wymianę tego obiektu na inny.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Strategia ze szczególnym naciskiem na następujące kwestie:
- Z jakich składa się klas?
- Jakie role pełnią te klasy?
- W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć następujący przykład, oparty na prawdziwym przypadku użycia PHP.
index.php: Przykład koncepcyjny
<?php
namespace RefactoringGuru\Strategy\Conceptual;
/**
* 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 __construct(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(): void
{
// ...
echo "Context: Sorting data using the strategy (not sure how it'll do it)\n";
$result = $this->strategy->doAlgorithm(["a", "b", "c", "d", "e"]);
echo 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(array $data): array;
}
/**
* 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(array $data): array
{
sort($data);
return $data;
}
}
class ConcreteStrategyB implements Strategy
{
public function doAlgorithm(array $data): array
{
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(new ConcreteStrategyA());
echo "Client: Strategy is set to normal sorting.\n";
$context->doSomeBusinessLogic();
echo "\n";
echo "Client: Strategy is set to reverse sorting.\n";
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();
Output.txt: Wynik działania
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
Przykład z prawdziwego życia
W tym przykładzie, wzorzec Strategia reprezentuje metody płatności w aplikacji e-commerce.
Każda z metod płatności może wyświetlać formularz służący zebraniu od użytkownika szczegółów płatności i przesłaniu ich bramce płatniczej. Następnie, po przekierowaniu nas z powrotem na naszą stronę, metoda płatności waliduje zwrócone przez bramkę parametry i pomaga określić czy udało się zrealizować zamówienie.
index.php: Przykład z prawdziwego życia
<?php
namespace RefactoringGuru\Strategy\RealWorld;
/**
* 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(string $url, array $data)
{
echo "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 {
echo "Controller: 404 page\n";
}
}
/**
* Handle GET requests.
*
* @param $url
* @throws \Exception
*/
public function get(string $url): void
{
echo "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 {
echo "Controller: 404 page\n";
}
}
/**
* POST /order {data}
*/
public function postNewOrder(array $data): void
{
$order = new Order($data);
echo "Controller: Created the order #{$order->id}.\n";
}
/**
* GET /orders
*/
public function getAllOrders(): void
{
echo "Controller: Here's all orders:\n";
foreach (Order::get() as $order) {
echo json_encode($order, JSON_PRETTY_PRINT) . "\n";
}
}
/**
* GET /order/123/payment/XX
*/
public function getPayment(PaymentMethod $method, Order $order, array $data): void
{
// The actual work is delegated to the payment method object.
$form = $method->getPaymentForm($order);
echo "Controller: here's the payment form:\n";
echo $form . "\n";
}
/**
* GET /order/123/payment/XXX/return?key=AJHKSJHJ3423&success=true
*/
public function getPaymentReturn(PaymentMethod $method, Order $order, array $data): void
{
try {
// Another type of work delegated to the payment method.
if ($method->validateReturn($order, $data)) {
echo "Controller: Thanks for your order!\n";
$order->complete();
}
} catch (\Exception $e) {
echo "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(): void
{
$this->status = "completed";
echo "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(string $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, array $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, array $data): bool
{
echo "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.");
}
echo "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, array $data): bool
{
echo "PayPalPayment: ...validating... ";
// ...
echo "Done!\n";
return true;
}
}
/**
* The client code.
*/
$controller = new OrderController();
echo "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,
]);
echo "\nClient: List my orders, please\n";
$controller->get("/orders");
echo "\nClient: I'd like to pay for the second, show me the payment form\n";
$controller->get("/order/1/payment/paypal");
echo "\nClient: ...pushes the Pay button...\n";
echo "\nClient: Oh, I'm redirected to the PayPal.\n";
echo "\nClient: ...pays on the PayPal...\n";
echo "\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: Wynik działania
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.