Middleware in Business Logic?
Tags: php, middleware, architecture, webdev
Published: 2020-10-31 21:28:49


In case you are into web development, you might be familiar with the concept of HTTP middleware, which is common in microframeworks (Slim PHP, Silex), custom stack APIs and even some of the full stack frameworks (Laravel, CakePHP) and can be added to most other frameworks too.
But have you ever thought of using middleware in your business logic? Does it make sense? Let's find out.

---

Imagine a fairly simple calculation of price of an order in an e-commerce solution:
```php
function calculateOrderSum(Order $order)
{
    // calculate the sum of the item prices
    $subtotal = array_reduce(
        $order->items, 
        fn($carry, $item) => $carry + $item->price, 
        0
    );

    // calculate coupon discount
    $discount = CouponService::discountForCoupon($order->coupon);

    // calculate VAT
    $vat = $subtotal * 0.1;

    // calculate shipping cost
    $shipping = ShippingService::calculateShipping(
        $order->delivery->region
    );

    // calculate total
    return $subtotal + $discount + $vat + $shipping;
}
```

This is quite simple and looks OK.
Now imagine the same calculation, but for a multi-tenant app, or even a SAAS solution.

> Imagine a single code base deployed to the cloud serving multiple domains with different e-shops.

Each tenant can use certain addons or implement specific way to calculate the price:
```php
function calculateOrderSum(Order $order, Tenant $tenant)
{
    // calculate the sum of the items
    $subtotal = array_reduce(
        $order->items,
        fn($carry, $item) => $carry + $item->price,
        0
    );

    // apply coupon discount
    $discount = 0;
    if ($tenant->usesCoupons) {
        $discount += CouponService::discountForCoupon($order->coupon);
    }
    if ($tenant->usesPriceBasedDiscounts) {
        $discount += DiscountService::calculateForSubtotal($subtotal);
    }

    // apply VAT
    $vat = VatService::calculateVat($subtotal + $discount);

    // calculate shipping cost
    $shipping = 0;
    if ($subtotal < $tenant->shippingFreeMinimum) {
        $shipping = ShippingService::calculateShipping(
            $order->delivery->region
        );
    }

    // calculate total
    return $subtotal + $discount + $vat + $shipping;
}
```
Above, we only added 2 simple conditions and already the solution became less legible.
We needed to add the `Tenant` parameter to a calculation that should not care for tenants and such, it's _not its concern_ ✋.

In reality, tenants will offer loayalty discounts, bundle discounts and whatnot. Tenants in different regions will calculate VAT differently and may even include some other tax than VAT. That's a lot of ifs and thens!

What is more, this tangled mess becomes very hard to test.


## Decomposition

The above algorithm can be decomposed to several simple blocks:
```php
$subtotalCalc = function (Order $order, callable $next) {
    $price = array_reduce(
        $order->items,
        fn($carry, $item) => $carry + $item->price,
        0
    );
    return $next($order) + $price;
};
$couponDiscountCalc = function (Order $order, callable $next) {
    return $next($order) + CouponService::discountForCoupon(
            $order->coupon
        );
};
$discountCalc = function (Order $order, callable $next) {
    $subtotal = $next($order);
    return $subtotal + DiscountService::calculateForSubtotal($subtotal);
};
$shippingCalc = function (Order $order, callable $next) {
    return $next($order) + ShippingService::calculateShipping(
            $order->delivery->region
        );
};
// a parameterized factory:
$vatCalcFactory = function (float $vat): callable {
    return function (Order $order, callable $next) use ($vat) {
        return $next($order) * (1 + $vat / 100);
    };
};
```
Each of them is **independent** and can very simply be unit tested. What is more, we can create them on the fly and parameterize them as we need.
A huge benefit is that the **complexity** of the algorithm **does not increase** with added logic, since you are only adding independent blocks.

Blocks of code like these are called _middleware_. Observe the base structure of a middleware:
```php
function middleware(mixed $passable, callable $next): mixed {

    // 1. code that happens before the next middleware
    // Note: this code may alter the argument

    // 2. invoke the next middleware
    // Note: this step is optional too
    $result = $next($passable);

    // 3. code that happens after the next middleware returns
    // Note: this code may alter the result

    return $result;
}
```
When composed, a middleware stack may be perceived as layers of an onion added one on top of the other. Note that the last added (outer-most) layer is executed first.


## Composition

Now we can elegantly build different computation pipelines for each tenant.
```php
$bikeShop = Pipeline::onion([
    $subtotalCalc,
    $couponDiscountCalc,
    $vatCalcFactory(10.0),
    $shippingCalc,
]);
$europeanToolsShed = Pipeline::onion([
    $subtotalCalc,
    $couponDiscountCalc,
    $discountCalc,
    LoyaltyService::forCustomer($currentlyAuthenticatedUser),
    $vatCalcFactory(20.0),
    $shippingCalc,
]);
```
To calculate the order's total, we only need to invoke the proper pipeline, like so:
```php
$total = $bikeShop($order);
// or
$total = $europeanToolsShed($order);
```

I'm using the [`Pipeline`](https://github.com/dakujem/cumulus/blob/master/doc/pipeline.md) class from my own package [`dakujem/cumulus`](https://github.com/dakujem/cumulus), but there are other middleware/pipeline dispatchers out there. Just make sure not to confuse them with HTTP middleware dispatchers, they are different animals.


## Wrapping Up

Using middleware pattern and composition, it is possible to reduce complexity of a complex calculation resulting in easier to understand and easier to test codebase.
This pattern is especially useful when the actual steps of a calculation are not known beforehand (avoiding heaps of `if`s!).

If you are new to middleware, be sure to google some info on HTTP middleware and PSR-15.

To build and dispatch simple pipelines within your domain logic, check out the [`Pipeline`](https://github.com/dakujem/cumulus/blob/master/doc/pipeline.md) dispatcher.

{% github dakujem/cumulus no-readme %}