Skip to Content
Course content

180: Upgrading PHP 7 Code to PHP 8

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've got a legacy project on my desk—a user permission system written back in the PHP 7.2 era—and I'm tasked with moving it to PHP 8.2. On paper, it looks fine. No deprecated functions are screaming at me in the logs yet. But as any seasoned dev knows, the "silent" changes are the ones that actually break your production environment.

The Comparison Trap

I'm looking at a piece of logic that determines if a user has a "guest" status. In PHP 7, the developer wrote this:

$status = "guest_user"; 
if ($status == 0) {
    echo "This is a guest!";
}

When I run this on my PHP 7.4 environment, it actually prints "This is a guest!". Why? Because PHP 7's loose comparison (==) would cast the string "guest_user" to an integer to compare it to 0. Since the string doesn't start with a number, it becomes 0. 0 equals 0. Logic passed. Weird, but that's how it worked.

Now, I flip the switch to PHP 8.2 and run it again. Silence. Nothing prints. I'm staring at the screen wondering why the logic suddenly failed. I check the docs, and there it is: PHP 8 changed how numeric strings are compared. Now, if you compare a number to a non-numeric string, PHP converts the number to a string and compares them. "0" does not equal "guest_user".

This is a huge deal. If your codebase relies on these "lucky" loose comparisons, your app is going to start behaving randomly after an upgrade. The fix is simple, but tedious: I have to go through and ensure I'm using strict comparisons (===) or actually checking the type. I'll change it to if ($status === 'guest') and sleep better tonight.

Taming the Switch

While I'm digging through the UserRole class, I find this monstrosity of a switch statement that maps roles to permission levels. It's about 40 lines of case, break, and a default.

switch ($role) {
    case 'admin':
        $level = 10;
        break;
    case 'editor':
        $level = 5;
        break;
    case 'user':
        $level = 1;
        break;
    default:
        $level = 0;
        break;
}

It works, but it's noisy. Since I'm upgrading to PHP 8, I remember the match expression. I'm going to try replacing this whole block to see if I can make it more readable. I'll try this:

$level = match ($role) {
    'admin' => 10,
    'editor' => 5,
    'user' => 1,
    default => 0,
};

I refresh the page. It works perfectly. Not only is it shorter, but match is strict. If I accidentally passed an integer 1 instead of a string '1', match wouldn't just guess; it would throw an error if no match was found. It's a safety net that PHP 7 just didn't provide.

Shrinking the Boilerplate

Finally, I'm looking at the Data Transfer Objects (DTOs) used for user profiles. They are full of the same repetitive pattern: declare the property, then assign it in the constructor. It's boring to write and boring to read.

class UserProfile {
    public string $username;
    public string $email;

    public function __construct(string $username, string $email) {
        $this->username = $username;
        $this->email = $email;
    }
}

In PHP 8, we have Constructor Property Promotion. I'm going to try and collapse this. I'll move the visibility modifiers (public) directly into the constructor arguments.

class UserProfile {
    public function __construct(
        public string $username,
        public string $email,
    ) {}
}

I just deleted five lines of code and the class does exactly the same thing. PHP automatically declares the properties and assigns the values for me. It feels like cheating, but in a large project with hundreds of these classes, this cleanup removes an incredible amount of visual noise.




📋 Practical Task

Refactoring the Legacy Order Status Handler

You have been handed a legacy file from a PHP 7.4 project. Your goal is to upgrade it to PHP 8.2 standards by fixing a logic bug and modernizing the syntax.

The Code:


class OrderHandler {
    public $status;
    public $orderId;

    public function __construct($status, $orderId) {
        $this->status = $status;
        $this->orderId = $orderId;
    }

    public function getStatusLabel($status) {
        switch ($status) {
            case 'pending':
                return 'Waiting for payment';
            case 'shipped':
                return 'On the way';
            case 'delivered':
                return 'Arrived';
            default:
                return 'Unknown';
        }
    }

    public function isInvalid($status) {
        // This was intended to return true if status is 0 or "0"
        // But in PHP 7, "pending" == 0 was true.
        return $status == 0;
    }
}

$order = new OrderHandler("pending", 101);
echo $order->isInvalid("pending"); // In PHP 7, this printed 1. In PHP 8, it prints nothing/false.

Your Tasks:

  • Fix the Bug: Rewrite the isInvalid method so it explicitly checks if the status is the string "0" or the integer 0, without relying on loose comparison traps.
  • Modernize the Logic: Replace the switch statement in getStatusLabel with a PHP 8 match expression.
  • Clean the Boilerplate: Use Constructor Property Promotion to rewrite the OrderHandler class, adding type hints (string for status, int for orderId).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.