Using the Decorator pattern in Bitrix

It's good practice to use programming patterns. Unfortunately, there are rarely examples of use on CMS Bitrix projects.

In this article I will show with an example how you can use the Decorator pattern.

We’ll also look at this pattern as a whole: its common implementations in PHP, possible alternatives, and situations in which it’s best to avoid using it.

The Decorator pattern allows you to add new functionality to an object by wrapping it in other decorator objects. These decorators have the same interface as the original object and add new functionality by performing additional operations before or after calls to the original object's methods. Thus, you can create a chain of decorators, each of which adds new functionality to the original object.

An implementation of the Decorator pattern in PHP typically includes the following elements:

  1. Component Interface: defines an interface that all components and their decorators must implement.

  2. Specific Component: represents the main object to which new functions will be added. Implements the Component interface.

  3. Basic decorator: represents the base class for all decorators. Implements the Component interface, but also contains a reference to an object of the Component type to which the new functionality will be added.

  4. Specific decorators: extend the functionality of the basic decorator by adding new functions. Each specific decorator has a reference to an object of type Component and can perform additional logic before or after calling the object's methods.

Advantages and disadvantages

Advantages of using the Decorator template:

  • Allows you to add new functionality to objects without changing their code.

  • Allows you to add functions both before and after calling methods.

  • Flexibility to add and combine functionality.

Despite its many benefits, there are situations in which it is best to avoid using the Decorator pattern:

  • When you need to create a large number of nested decorators, which can lead to complex code and poor performance.

  • When you want to change the component class itself (the class to which functionality is added), instead of adding new functionality through decorators.

What does the Decorator pattern provide in Bitrix?

  1. Functionality extension: The Decorator pattern allows you to add new functionality to objects without changing their basic structure. In Bitrix, this can be useful, for example, for adding additional capabilities to users, infoblock elements and other entities.

  2. Division of Responsibilities: Using a decorator allows you to isolate individual functions into separate classes, which allows you to better structure your code and improve its understandability and maintainability.

  3. Flexibility and scalability: The Decorator pattern allows you to add and combine decorators in any order depending on the needs of the project. This allows you to quickly and flexibly change functionality without having to completely rewrite the code.

Usage example

Let's say we are implementing a custom Shopping Cart component. And we need to display the cost of the cart before applying the discount and a preliminary calculation of the cost of the cart with already applied discounts.

Implementing the Cart class using the Decorator pattern.

  1. Let's create an Interface BasketInterface, which will define the main methods for working with the cart:

interface BasketInterface {
   public function getTotalPrice();
   public function addItem(\Bitrix\Sale\BasketItem $item);
   public function removeItem(\Bitrix\Sale\BasketItem $item);
}
  1. Let's create a base class BaseBasket, which will implement the Recycle Bin interface:

class BaseBasket implements BasketInterface {
   protected $items = [];

   public function getTotalPrice() {
       $totalPrice = 0;
       foreach ($this->items as $item) {
           $totalPrice += $item->getPrice();
       }
       return $totalPrice;
   }

   public function addItem(\Bitrix\Sale\BasketItem $item) {
       $this->items[] = $item;
   }

   public function removeItem(\Bitrix\Sale\BasketItem $item) {
       foreach ($this->items as $key => $val) {
           if ($val == $item) {
               unset($this->items[$key]);
               break;
           }
       }
   }
}
  1. Let's create a decorator to calculate the discount DiscountBasket, which will extend the functionality of the BaseBasket base class:

class DiscountBasket implements BasketInterface
{

   protected $basket;
   protected $discount;

   public function __construct(BasketInterface $basket, $discount)
   {
       $this->basket = $basket;
       $this->discount = $discount;
   }

   public function getTotalPrice()
   {
       $totalPrice = $this->basket->getTotalPrice();
       $discountPrice = $totalPrice * (1 - ($this->discount / 100));

       return $discountPrice;
   }

   public function addItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->addItem($item);
   }

   public function removeItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->removeItem($item);
   }

}
  1. Let's implement our calculation:

$basket =Basket::loadItemsForFUser(Sale\Fuser::getId(), \Bitrix\Main\Context::getCurrent()->getSite());

$basketItems = $basket->getBasketItems();

$baseBasket = new BaseBasket();
$discountBasket = new DiscountBasket($baseBasket, 10); // Установим скидку 10%

foreach ($basketItems as $index => $basketItem) {
   $discountBasket->addItem($basketItem);
}

$totalPrice = $discountBasket->getTotalPrice(); // Расчет с учетом скидки


?>
<section>
   <p>
       Цена без скидки: <?
       \Bitrix\Main\Diag\Debug::dump($basket->getBasePrice());
       ?>
   </p>

   <p>
       Новая цена: <?
       \Bitrix\Main\Diag\Debug::dump($totalPrice);
       ?>
   </p>
</section>

Output result:

In this example, the class BaseBasket implements the basic functionality of the shopping cart – adding and removing products, as well as calculating the total cost.

Class DiscountBasket is a decorator and expands the functionality of the basic cart by adding the ability to set a discount on the total cost. This class also implements interface methods BasketInterface and uses the basic cart to perform basic operations.

When creating an instance of a class DiscountBasket You can pass an instance of the base cart and a discount value to the constructor.

So the methods addItem And removeItem called on an object DiscountBasket, but actually perform operations on the base cart. Method getTotalPrice also uses the basic cart functionality, but applies a discount to the resulting total cost and returns the discounted result.

You can rightly point out why you can’t just make a descendant from the base class? The Decorator pattern changes/adds the behavior of an object. You can wrap an object with one or more decorators. These decorators are responsible for adding or changing the functionality of the wrapped object without changing its interface. By layering multiple decorators, you can gradually extend the behavior of the original object.

It is also possible to use the Decorator for Final classes that are not open for extension.

  1. We will add shipping costs to our price.. To do this, we will create a decorator that adds shipping costs DelliveryBasket:

class DelliveryBasket implements BasketInterface
{

   protected $basket;
   protected $deliveryPrice;

   public function __construct(BasketInterface $basket, $deliveryPrice)
   {
       $this->basket = $basket;
       $this->deliveryPrice = $deliveryPrice;
   }

   public function getTotalPrice()
   {
       $totalPrice = $this->basket->getTotalPrice();
       $discountPrice = $totalPrice + $this->deliveryPrice;

       return $discountPrice;
   }

   public function addItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->addItem($item);
   }

   public function removeItem(\Bitrix\Sale\BasketItem $item)
   {
       $this->basket->removeItem($item);
   }
}
  1. Now our implementation will look like this:

$basket =Basket::loadItemsForFUser(Sale\Fuser::getId(), \Bitrix\Main\Context::getCurrent()->getSite());

$basketItems = $basket->getBasketItems();

$baseBasket = new BaseBasket();
$discountBasket = new DelliveryBasket( new DiscountBasket($baseBasket, 10), 500); // Установим скидку 10% и добавим доставку

foreach ($basketItems as $index => $basketItem) {
   $discountBasket->addItem($basketItem);
}

$totalPrice = $discountBasket->getTotalPrice(); // Расчет с учетом скидки


?>
<section>
   <p>
       Цена без скидки: <?
       \Bitrix\Main\Diag\Debug::dump($basket->getBasePrice());
       ?>
   </p>

   <p>
       Новая цена: <?
       \Bitrix\Main\Diag\Debug::dump($totalPrice);
       ?>
   </p>

</section>

Output result:

In the current implementation, the method getTotalPrice uses the basic cart functionality, applies the discount to the resulting total cost and returns the result with the discount, then adds the shipping cost to the resulting cost.

The decorator has an alternative name – wrapper. It more accurately describes the essence of the pattern: you wrap the target object in another wrapper object, which triggers the basic behavior of the object, and then adds something of its own to the result.

Both objects have a common interface, so it makes no difference for the user whether to work with a pure or wrapped object. You can use several different wrappers at the same time – the result will be the combined behavior of all wrappers at once.

In conclusion, the Decorator pattern in PHP provides a flexible and extensible alternative to subclassing for adding new functionality to objects. It follows the open-closed principle and allows changes to the functionality of objects at runtime. However, it is necessary to evaluate the situation and consider possible disadvantages when using this pattern.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *