Method Chaining Made Easy in PHP

Method chaining can be a nice way to reduce the amount of code your write.  It does so without sacrificing readability. In a basic form, it’s the process of calling multiple methods on an object sequentially, without referencing the object before each call.

 

Class without method chaining

We’ll demonstrate the usage of method chaining here. We’ll walk through using a simple PHP purchase order class.  The class will contain methods that add items to a cart, apply coupons for discounts and set the tax rate to be applied to the final purchase price.

The Order class will also contain methods to complete the purchase, send a notification email to the customer, and return a summary of the details.  First we’ll take a look at the simple Order class before applying any method chaining:

class Order {
    protected $purchasePrice = 0;
    protected $cart = [];
    protected $coupons = [];
    protected $taxRate = 0;
    protected $purchaseTime;

    // Add item to cart
    public function addItem($item, $price) {
        $this->cart[] = [
            'item' => $item,
            'price' => floatval($price),
        ];
    }

    // Add a coupon to the order
    public function addCoupon($code, $value) {
        $this->coupons[] = [
            'code' => $code,
            'value' => floatval($value),
        ];
    }

    // Apply tax to price
    public function tax($rate) {
        $this->taxRate = $rate;
    }

    // Finish the order
    public function purchase() {
        // Get total for items, minus coupon discounts, plus tax
        $this->purchasePrice = $this->subtotal() + $this->taxAmount();

        // Process credit card, etc here...
        // ...

        $this->purchaseTime = time();
    }

    // Send email notification
    public function notify($emailAddress) {
        // Logic to email order details here...
        // ...
    }

    // Get the subtotal (item costs, minus coupon values)
    protected function subtotal() {
        return $this->debitTotal() - $this->creditTotal();
    }

    // Get the tax amount (subtotal plus applicable tax)
    protected function taxAmount() {
        return $this->subtotal() * $this->taxRate;
    }

    // Get total for all items in the cart
    protected function debitTotal() {
        $total = 0;

        // Add up item prices
        foreach($this->cart as $i) {
            $total += $i['price'];
        }

        return $total;
    }

    // Get total for all coupons
    protected function creditTotal() {
        $total = 0;

        // Add up coupon values
        foreach($this->coupons as $i) {
            $total += $i['value'];
        }

        return $total;
    }

    // Get details for the order
    public function summary() {
        return [
            'purchase_price' => $this->purchasePrice,
            'subtotal' => $this->subtotal(),
            'tax_amount' => $this->taxAmount(),
            'items' => $this->cart,
            'coupons' => $this->coupons,
            'purchased_at' => $this->purchaseTime,
        ];
    }
}

Basic usage of the Order class looks something like this:

// Create the new order
$order = new Order();

// Add items
$order->addItem('pizza', 15);
$order->addItem('soda', 2.5);
$order->addItem('cookie', 1);

// Apply a discount
$order->addCoupon('5-OFF', 5);

// Set 6% sales tax
$order->tax(.06);

// Purchase items
$order->purchase();

// Send email notification
$order->notify('customer@example.com');

// Get order summary
$summary = $order->summary();

echo '<pre>'.print_r($summary, true).'</pre>';

Running the above code will give the following summary for the $order object:

Array
(
    [purchase_price] => 14.31
    [subtotal] => 13.5
    [tax_amount] => 0.81
    [items] => Array
        (
            [0] => Array
                (
                    [item] => pizza
                    [price] => 15
                )

            [1] => Array
                (
                    [item] => soda
                    [price] => 2.5
                )

            [2] => Array
                (
                    [item] => cookie
                    [price] => 1
                )

        )

    [coupons] => Array
        (
            [0] => Array
                (
                    [code] => 5-OFF
                    [value] => 5
                )

        )

    [purchased_at] => 1537319212
)

Apply method chaining to the order class

There is nothing wrong with the way we are instantiating the Order class. Working with the $order object line-by-line is fine too.  However, if we apply a little method chaining magic, we can get a much more concise version of the code that will accomplish the same thing.  Let’s take a look at the modified order class that will allow us to apply method chaining:

class Order {
    protected $purchasePrice = 0;
    protected $cart = [];
    protected $coupons = [];
    protected $taxRate = 0;
    protected $purchaseTime;

    // Add item to cart
    public function addItem($item, $price) {
        $this->cart[] = [
            'item' => $item,
            'price' => floatval($price),
        ];
        return $this;
    }

    // Add a coupon to the order
    public function addCoupon($code, $value) {
        $this->coupons[] = [
            'code' => $code,
            'value' => floatval($value),
        ];
        return $this;
    }

    // Apply tax to price
    public function tax($rate) {
        $this->taxRate = $rate;
        return $this;
    }

    // Finish the order
    public function purchase() {
        // Get total for items, minus coupon discounts, plus tax
        $this->purchasePrice = $this->subtotal() + $this->taxAmount();

        // Process credit card, etc here...
        // ...

        $this->purchaseTime = time();
        return $this;
    }

    // Send email notification
    public function notify($emailAddress) {
        // Logic to email order details here...
        // ...
        return $this;
    }

    // Get the subtotal (item costs, minus coupon values)
    protected function subtotal() {
        return $this->debitTotal() - $this->creditTotal();
    }

    // Get the tax amount (subtotal plus applicable tax)
    protected function taxAmount() {
        return $this->subtotal() * $this->taxRate;
    }

    // Get total for all items in the cart
    protected function debitTotal() {
        $total = 0;

        // Add up item prices
        foreach($this->cart as $i) {
            $total += $i['price'];
        }

        return $total;
    }

    // Get total for all coupons
    protected function creditTotal() {
        $total = 0;

        // Add up coupon values
        foreach($this->coupons as $i) {
            $total += $i['value'];
        }

        return $total;
    }

    // Get details for the order
    public function summary() {
        return [
            'purchase_price' => $this->purchasePrice,
            'subtotal' => $this->subtotal(),
            'tax_amount' => $this->taxAmount(),
            'items' => $this->cart,
            'coupons' => $this->coupons,
            'purchased_at' => $this->purchaseTime,
        ];
    }
}

All we’ve done here is place return $this; into some of our public methods.  It’s a simple change, but it will allow us to use the Order class with method chaining like this:

// Create the new order
$order = new Order();

$summary = $order->addItem('pizza', 15) // Add items
    ->addItem('soda', 2.5)
    ->addItem('cookie', 1)
    ->addCoupon('5-OFF', 5)             // Apply a discount
    ->tax(.06)                          // Set 6% sales tax
    ->purchase()                        // Purchase items
    ->notify('customer@example.com')    // Send email notification
    ->summary();                        // Get order summary

echo '<pre>'.print_r($summary, true).'</pre>';

This code gives the exact same summary output as the first version.  Here we were able to reduce our code by only writing a reference to our $order object once.  As you can see each method is “chained” onto the one before it.  Ultimately the last summary method returns the order data.

But wait, there’s more…

Lets see if we can reduce it just a bit further.  Let’s add a new method to the order class which will give us back a newly instantiated object:

public static function instance() {
    return new static();
}

This added method will allow us to create and process a new order all together.  We can drop the $order = new Order(); line and use this code instead:

// Create and process the new order all at once
$summary = Order::instance()->addItem('pizza', 15)  // Add items
    ->addItem('soda', 2.5)
    ->addItem('cookie', 1)
    ->addCoupon('5-OFF', 5)             // Apply a discount
    ->tax(.06)                                 // Set 6% sales tax
    ->purchase()                                    // Purchase items
    ->notify('customer@example.com')    // Send email notification
    ->summary();                                    // Get order summary

echo '<pre>'.print_r($summary, true).'</pre>';

This further reduced block of code will still produce the same summary results, but with less verbosity.  Essentially the instance method turns the Order class into a factory for itself.  Here is the full Order class in it’s final form:

class Order {
    protected $purchasePrice = 0;
    protected $cart = [];
    protected $coupons = [];
    protected $taxRate = 0;
    protected $purchaseTime;

    // Get a new instance
    public static function instance() {
        return new static();
    }

    // Add item to cart
    public function addItem($item, $price) {
        $this->cart[] = [
            'item' => $item,
            'price' => floatval($price),
        ];
        return $this;
    }

    // Add a coupon to the order
    public function addCoupon($code, $value) {
        $this->coupons[] = [
            'code' => $code,
            'value' => floatval($value),
        ];
        return $this;
    }

    // Apply tax to price
    public function tax($rate) {
        $this->taxRate = $rate;
        return $this;
    }

    // Finish the order
    public function purchase() {
        // Get total for items, minus coupon discounts, plus tax
        $this->purchasePrice = $this->subtotal() + $this->taxAmount();

        // Process credit card, etc here...
        // ...

        $this->purchaseTime = time();
        return $this;
    }

    // Send email notification
    public function notify($emailAddress) {
        // Logic to email order details here...
        // ...
        return $this;
    }

    // Get the subtotal (item costs, minus coupon values)
    protected function subtotal() {
        return $this->debitTotal() - $this->creditTotal();
    }

    // Get the tax amount (subtotal plus applicable tax)
    protected function taxAmount() {
        return $this->subtotal() * $this->taxRate;
    }

    // Get total for all items in the cart
    protected function debitTotal() {
        $total = 0;

        // Add up item prices
        foreach($this->cart as $i) {
            $total += $i['price'];
        }

        return $total;
    }

    // Get total for all coupons
    protected function creditTotal() {
        $total = 0;

        // Add up coupon values
        foreach($this->coupons as $i) {
            $total += $i['value'];
        }

        return $total;
    }

    // Get details for the order
    public function summary() {
        return [
            'purchase_price' => $this->purchasePrice,
            'subtotal' => $this->subtotal(),
            'tax_amount' => $this->taxAmount(),
            'items' => $this->cart,
            'coupons' => $this->coupons,
            'purchased_at' => $this->purchaseTime,
        ];
    }
}

Grab the code…

All the code above is available in a GitLab repository here:

https://gitlab.com/totaldev-instructables/php-method-chaining.git

Leave a Reply

avatar
  Subscribe  
Notify of