Scaling PHP Codebases: Why Safety and Strictness Come First

When people talk about scale, many think they mean 10000 requests per second. They however forget that scale also means the ease of maintaining the code and adding new features. How quickly can we add a new city? How quickly can we onboard new engineers?

Published 26 July 20266 min read
Scaling PHP Codebases: Why Safety and Strictness Come First

# The big problem with PHP

PHP does not have a scaling issue. It has a strictness issue. I could say this for every language but today is PHP day. So what does this mean? Before I answer this, let me first define what I mean by scale.

When people say language x does not scale they mean different things. Most of the time they mean the throughput. The number of requests per second. Well, they are right but that is not the whole story. The other side of the story is the Developer experience. I want you to look at this graph that every PHP codebase goes through.

1785035403315 php dx curve 24f4d6c1

If you have worked on a dynamic language for more than a year, you have probably seen the above phenomenon. In the first days you ship fast. Then feature requests pile up, more team members join and suddenly there are parts of the codebase that no one wants to touch. You have probably seen stuff like;

<?php

/**
 * ⚠️ DO NOT DELETE THIS BLOCK ⚠️
 * I don’t know what this does, but when I remove it, production goes down.
 * - Added by @former_dev (3 years ago)
 */
register_shutdown_function(static function (): void {
    if (connection_status() !== CONNECTION_NORMAL) {
        // Keeping the daemon's heartbeat alive. Touch at your own peril.
        @file_get_contents('http://127.0.0.1:9090/keepalive?state=' . (int) (memory_get_usage(true) > 0));
    }
});

Now this, is what I mean by scalability. Over time, adding new features to the codebase takes time, the code quality worsens and everyone in the team starts hating their job. The new guy in the office will be pitching a Rust rewrite three days in.

# How to make it scale.

Now that we have established what I mean by scale, Let us look at ways to avoid this because it is inevitable.

SDEs hate paying for things upfront and they end up paying for them three years down the line. It is a vicious cycle and before you run to Twitter to curse PHP, try the following.

# Make your code strict.

PHP is loved because it is dynamically typed but this is the kind of thing that will haunt you in future. You move fast in the early days but soon you’ll find yourself writing tests to assert data types. You will have more tests than code. All this because you wanted to ship quick at the beginning.

Look at this example:

<?php

// No type hints on parameters or return value
function calculateDiscountedPrice($price, $discount, $format = false) {
    // Relying on loose equality and dynamic coercion
    if ($discount == "NONE" || !$discount) {
        $finalPrice = $price;
    } else {
        // Danger: Passing '15%' as a string forces silent float conversion
        $finalPrice = $price - ($price * ($discount / 100));
    }

    // Inconsistent return type: string vs. float
    if ($format == "true" || $format === true) {
        return "$" . number_format($finalPrice, 2);
    }

    return $finalPrice;
}

// -----------------------------------------------------------------------------
// Usage examples (showing how fragile this gets in production):
// -----------------------------------------------------------------------------

// Works, but returns a float (85.0)
$total1 = calculateDiscountedPrice(100, 15);

// String type juggling works by accident, but returns a string ("$85.00")
$total2 = calculateDiscountedPrice("100", "15%", true);

// Unexpected edge case: passing a bad type silently returns wrong math
$total3 = calculateDiscountedPrice(100, "20 dollars"); // Treats discount as 20 -> 80

// Calling code expecting a float might break if $format was dynamically set to true
// $grandTotal = $total1 + $total2; // Fatal error or garbage results in strict contexts

The above snippet looks trivial. In fact it is and for a small project, fixing bugs is easy. However, we know how it goes in the real world. Snippets like this will grow, a function in this file will have countless callers, business rules will change and updating that will be very risky. You don’t know what you might break. The IDE will do little to support you and you’ll end up hating PHP. It is not the tools bro, it you. You are just a shitty programmer.

Anyway, let us try and fix this mess. This is how I’d improve it. A common pitfall, is passing wrong parameters or depending on the LSP to tell you what the first parameter is. For this, I’ll use a modern PHP feature called named parameters. Yeah lean into modern PHP.

The second thing I’ll do is to ensure the parameters are typed. Good thing, named parameters give you this for free. Here is what the improved snippet looks like

<?php

declare(strict_types=1);

namespace App\Pricing;

use InvalidArgumentException;

/**
 * Calculates the discounted price with strict type constraints.
 * * @throws InvalidArgumentException If price or discount percentage is negative.
 */
function calculateDiscountedPrice(
    float $price,
    ?float $discountPercentage = null,
): float {
    if ($price < 0.0) {
        throw new InvalidArgumentException('Price cannot be negative.');
    }

    if ($discountPercentage === null) {
        return $price;
    }

    if ($discountPercentage < 0.0 || $discountPercentage > 100.0) {
        throw new InvalidArgumentException('Discount percentage must be between 0 and 100.');
    }

    return $price - ($price * ($discountPercentage / 100.0));
}

/**
 * Formats a monetary amount explicitly (separated from calculation logic).
 */
function formatCurrency(
    float $amount,
    string $symbol = '$',
    int $decimals = 2,
): string {
    return sprintf('%s%s', $symbol, number_format($amount, $decimals));
}

// -----------------------------------------------------------------------------
// Usage examples with Named Parameters:
// -----------------------------------------------------------------------------

// 1. Clear, self-documenting calls using named arguments
$discountedTotal = calculateDiscountedPrice(
    price: 100.0,
    discountPercentage: 15.0,
);

// 2. Formatting is explicitly decoupled, allowing optional defaults or named overrides
$formattedPrice = formatCurrency(
    amount: $discountedTotal,
    symbol: '$',
); // "$85.00"

// 3. Skipping optional parameters smoothly via named arguments
$customFormat = formatCurrency(
    amount: 2500.50,
    decimals: 0, // Symbol defaults to '$'
); // "$2,501"

Looks cool right?

# It is so much to remember!

Of course it is and the above example is far from comprehensive. Your work as an SDE is to ship features and the above gymnastics will only make things worse. Good thing, is that you do not have to remember all this. The SDE landscape is awesome because there are people dedicated in building tools for devs so that you can focus on maximising shareholder value. For this, you can lean to tools like PHPstan and Mago. Mago is my personal favourite even though PHPstan has been with me in dark days. Let me do a quick overview of the two.

# PHPstan

PHPstan is focused on static analysis. It is written PHP and the speed is pretty much the same as PHP. I’ve used it extensively in my editor because it also works as an LSP. It will not let me write dynamic PHP. I still recommend it because it is widely used and your next PHP role might include it. Here is an example of it shouting at me.

1785038985085 screenshot 2026 07 26 at 07 09 28 e67bd681 That is pretty much it. By default, it will ensure that you are writing proper documentation for your code, you are typing your parameters etc. You will be able to catch a lot of issues without writing tests. It is a must have because trust me, you will inherit a PHP codebase and you’ll need it.

# Mago

Mago is the new kid in the block. Written in Rust but that is not the whole story. Someone looked at Rust’s Clippy and said, I want that in PHP. Mago is more than a static analyser. It is also a linter and can modernise your codebase. Remember when I told you to lean to modern PHP earlier? Well, that could be very hard with a large codebase but Mago can instantly migrate your codebase to php 8.5+. You should of course do a lot of QA after this. Below is an example of what Mago has done in the screenshot above.

1785039803301 screenshot 2026 07 26 at 07 22 39 40a126c8

The #[\SensitiveParameter] is part of the new PHP decorators. This particular one will hide the values from logs. Laravel makes a very good use of them to reduce boilerplate.

# Now Mago + PHPstan

It is a good idea to combine both. PHPstan is good at enforcing safety rules. While Mago will help you write the latest PHP. Both are important.

# Now the other scalability types.

When NodeJs came out, it made many devs in the industry realise that the new internet needs a better way to run applications. If you were still blocking IO, you were NGMI. PHP on the other hand still relied in the 90s model. Wake up a thread per request. NodeJs was a clear winner because it could handle way more requests for the same amount of hardware. It even scaled better when paired with Nginx. Meanwhile PHP behind Apache would frequently hit you with 503 errors. The servers were running out of threads.

1785040650525 nodejs ee182e74

Good thing we’re talking in past tense. PHP has since caught up and there are a couple of options for running your PHP code in a non blocking mode. One of them is OpenSwoole and the other is FrankenPHP.

# Parting shot

With these tips, you can keep your PHP codebase without the maintenance burden. Your 2015 codebase can still serve your customers in the foreseeable future. If the scale gets insane, you can try the HackVM. Anyway, if you are contemplating Hack, you are probably too big for your stack.

Do you have PHP codebase you’d like some help with? Hit me up on [email protected] or click the contact button below.

Happy coding.