I'll Be Backend
Principles · PHP

Coupling: wrap a third-party library

Keep a vendor's library behind one class of yours, so a switch touches one class.

13 steps4 chapters~6 min at 1.25×#architecture#coupling#wrapper#adapter pattern
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the PHP video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–9

Payoff

Steps 10–11

Recap

Steps 12–13

The code: before and after

Before

final class BookingService
{
    private const RATES = ['Golf' => 40, 'Polo' => 35];

    public function __construct(
        private CardPayClient $pay = new CardPayClient('desk-key'),
    ) {}

    public function book(string $car, int $days): string
    {
        $total = self::RATES[$car] * $days;
        $r = $this->pay->charge($total * 100, 'USD');
        if ($r->getStatus() !== 'succeeded') {
            return 'declined';
        }
        return "$car booked, $total USD";
    }
}

final class ReturnDesk
{
    public function __construct(
        private CardPayClient $client = new CardPayClient('desk-key'),
    ) {}

    public function lateFee(string $car, int $hours): string
    {
        $fee = $hours * 15;
        $result = $this->client->charge($fee * 100, 'USD');
        if ($result->getStatus() !== 'succeeded') {
            return 'declined';
        }
        return "$car late fee, $fee USD";
    }
}

echo (new BookingService())->book('Golf', 3), "\n";
echo (new ReturnDesk())->lateFee('Golf', 2), "\n";

After

interface Payments
{
    public function charge(int $dollars): bool;
}

final class CardPayPayments implements Payments
{
    private readonly CardPayClient $pay;

    public function __construct(string $apiKey)
    {
        $this->pay = new CardPayClient($apiKey);
    }

    public function charge(int $dollars): bool
    {
        $r = $this->pay->charge($dollars * 100, 'USD');
        return $r->getStatus() === 'succeeded';
    }
}

final class PayLanePayments implements Payments
{
    private readonly PayLane $lane;

    public function __construct(string $account)
    {
        $this->lane = new PayLane($account);
    }

    public function charge(int $dollars): bool
    {
        try {
            $this->lane->send($dollars, 'USD');
        } catch (PayLaneException) {
            return false;
        }
        return true;
    }
}

final class BookingService
{
    private const RATES = ['Golf' => 40, 'Polo' => 35];

    public function __construct(private Payments $payments) {}

    public function book(string $car, int $days): string
    {
        $total = self::RATES[$car] * $days;
        if (!$this->payments->charge($total)) {
            return 'declined';
        }
        return "$car booked, $total USD";
    }
}

final class ReturnDesk
{
    public function __construct(private Payments $payments) {}

    public function lateFee(string $car, int $hours): string
    {
        $fee = $hours * 15;
        if (!$this->payments->charge($fee)) {
            return 'declined';
        }
        return "$car late fee, $fee USD";
    }
}

$payments = new PayLanePayments('desk-account');
echo (new BookingService($payments))->book('Golf', 3), "\n";
echo (new ReturnDesk($payments))->lateFee('Golf', 2), "\n";

Transcript, step by step

Problem

Step 1: Two classes take money Before

This is the same car rental desk from last time, and now it takes card payments. Booking Service prices a rental and charges the card, and Return Desk charges a late fee of fifteen dollars an hour. Both call Card Pay, a third-party library, which is code another company writes and ships, that we call but cannot change. Each class keeps a Card Pay client here, calls charge in cents here, and checks the status string here. The two echo lines at the bottom are the caller, and they print Golf booked for a hundred and twenty dollars, then a Golf late fee of thirty. The library's own guide shows exactly these lines, so pasting them wherever money is taken was the shortest path.

Step 2: Finance moves to PayLane Change request

Finance moves card payments to Pay Lane next month. The quick edit swaps the property in both classes for a Pay Lane client with the desk account. Pay Lane takes dollars instead of cents and says no by throwing an exception, so each charge becomes a send inside a try block. PHPStan passes, and the script prints the same two lines as before.

Step 3: Coupled to the vendor Code smell

Coupling is how much one part of the code must change when another part changes. Here our business classes are coupled to a third-party library, because they use its class, its account, its units and its way of saying no. You can see the property here, the send call here, and the catch here, once in each class. So a vendor switch edits every class that takes money, and here that's two classes, each with a new property, a new call, dollars in place of cents, and a try block in place of the status check.

Refactor

Step 4: Refactor first Back it out

So let's undo the edit, refactor first, and then switch. Both classes are back on Card Pay, and every step from here prints the same two lines.

Step 5: Name the charge Extract Method

First we extract a method in Booking Service called charge, which takes dollars and returns true when the card goes through. It holds the cents math, the call to the library, and the status check, and it stays private to this class for now. Book now asks charge and returns declined when the answer is no, so the script prints the same lines.

Step 6: The library gets a class Extract Class

Next we extract a class, Card Pay Payments, and move the client property and the charge method into it. Its constructor takes the API key, and charge becomes public, because other classes will call it now. Booking Service holds one with the desk key and calls its charge method. A wrapper is one small class of ours that is the only code that calls the library. Return Desk still calls Card Pay itself, so this class isn't the only caller yet.

Step 7: Return desk uses it too Replace Inline Code with Function Call

Return Desk drops its own Card Pay client and holds a Card Pay Payments instead. Late fee replaces the call and the status check with one call to charge, the same way book does. Card Pay Payments is now the wrapper, because it is the only class that calls the Card Pay library.

Step 8: Name what the desk needs Extract Interface

We extract an interface, Payments, with the one charge method our desk needs. Card Pay Payments implements it, and both properties are now typed Payments, so PHPStan checks every call against the interface. Nothing about cents or status strings is left in that interface, so another vendor can fit behind it.

Step 9: Pass the wrapper in Constructor Injection

Each service still built its own wrapper, so the choice of vendor sat inside two business classes. With constructor injection, a form of dependency injection, each class takes its Payments in the constructor instead. The script now builds one wrapper and passes it to both services. It still prints the same two lines, Golf booked and the Golf late fee.

Payoff

Step 10: Switch to PayLane again Payoff

Now we make the switch to Pay Lane again. We add Pay Lane Payments, which implements Payments and takes the desk account in its constructor. Its charge sends dollars to Pay Lane and returns false when Pay Lane throws, or true when the send goes through. The script passes the new wrapper, and that is the only line outside it that changed. Booking Service, Return Desk and the Payments interface did not change at all. The script prints the same two lines, now paid through Pay Lane.

Step 11: The vendor lives in one class Payoff

The Pay Lane property and its call and its catch used to sit in both business classes. Now you can see the property here, the send call here, and the catch here, once, inside the wrapper. This is the Adapter pattern, because each wrapper turns a vendor's interface into the one our code asked for. So a vendor switch now costs one new class and one line in the script.

Recap

Step 12: Where we started, where we ended Before / after

On the left, both business classes charged a Card Pay client in cents and checked its status string. On the right, Booking Service only knows the Payments interface and asks it to charge dollars. Card Pay Payments is the one class that calls Card Pay, with the cents math and the status check inside it. So the vendor's names and units live in one class instead of in every class that takes money.

Step 13: When to reach for it Takeaways

Reach for it when a vendor library is called from more than one business class, or when a switch of vendors is likely. Skip it for code that is stable and everywhere, like PHP's own array functions, because the extra class hides nothing worth hiding. You'll spot the need by vendor class names, keys, units or status strings showing up in more than one business class. PHP already does this with the PSR-3 logger interface, which puts one interface in front of Monolog and other logging libraries.