The Workshop: The Road to 7.3 (Part 2) - php[architect] Magazine February 2019

Joe • September 5, 2020

learning packages phparch writing php

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the February 2019 issue from http://phparch.com for the professionally edited version.

The Workshop: The Road to 7.3, Part 2

by Joe Ferguson

Last month we explored the world of static analyzers Phan and PhpStan to find compatibility issues any several other common issues in our PHP Easy Math code base. This month we’re going to review our code base and implement new and recent features from PHP 7.x

One of the best features of new PHP versions is the performance boost. With every PHP 7 release we have enjoyed some amount of increase in speed but how much of an increase often comes down to your individual code base. We’re going to dive into some of the other new features you may have heard about but aren’t really sure how to use them or what the advantages are by implementing them into our PHP Easy Math project. Specifically we are going to cover scalar type hints, the null coalesce operator, and the breaking changes regarding error handling.

Scalar Type Hints

Type hinting has been a part of PHP for a while now however PHP 7 has taken type hints to the next level by adding support for int, float, string, and bool. Why should PHP developers care about type when we’ve gotten along thus far without them? The PHP community at large has started to come around to strict types over the past several years. The value in strict typing, or even basic type hinting comes from making your code more expressive and declarative of its intentions. Scalar Type Hints allow us to specify the kind of data we’re passing into a method and if we pass the wrong type PHP will let us know. Another benefit to type hinting our applications is static analyzers like PHPStan and Phan know more about our code and can draw more conclusions and discover more issues before we even run our code.

Looking at our tests/AdditionTest.php code we can see we have two test methods, one for our add($x, $y) method and one for sum(float …$numbers). Our third method is our data provider which we use to hold the test cases and expected results. We’re going to add some test cases of strings and booleans to see what happens:

Our mathProvider():

public function mathProvider()
{
    return [
        [0, 0, 0],
        [0, 1, 1],
        [1, 0, 1],
        [1, 1, 2],
        [1337, 1337, 2674],
        [101.35, 108, 209.35], // new
        [‘php’, ‘arch’, ‘phparch’], // new
        [true, true, true] // new
    ];
}

Wait a minute! Those first 5 cases aren’t floats!? Good catch! Once we add our float type hints PHP will convert integers to floats such as 1 becomes float(1.0) and a string 15 becomes float(15.0). These cases the coercion by the engine works in our favor as opposed to trying to pass string 'Test string 1.5' as a float type hinted value will result in a TypeError.

Let’s run only our tests/AdditionTest.php and from there only execute our testEasyMathKnowsHowToAddmethod so we’ll only see output from our add($x, $y) method:

$ ./vendor/bin/phpunit tests/AdditionTest.php --filter testEasyMathKnowsHowToAdd
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

......E.                                                            8 / 8 (100%)

Time: 27 ms, Memory: 4.00MB

There was 1 error:

1) EasyMath\Tests\AdditionTest::testEasyMathKnowsHowToAdd with data set #5 ('php', 'arch', 'phparch')
A non-numeric value encountered

/Users/joeferguson/Code/php-easy-math/src/Addition.php:21
/Users/joeferguson/Code/php-easy-math/tests/AdditionTest.php:21

ERRORS!
Tests: 7, Assertions: 6, Errors: 1.

Wait, what just happened? We added two test cases, one strings one boolean values but only the string failed. Why did the boolean pass? Without utilizing type hints PHP will coalesce types under the hood for you. In some cases this is fine, such as the case of our integers and our booleans; in PHP true plus true equals true which is why our addition method works. The boolean is coalesced in the engine and evaluated.

I have ventured down the rabbit hole of how and why of type coalescing by the PHP engine and I’m here to warn: you might want to just skip this particular trip. (If you’re an experienced C developer please jump in and write a blog post or article for the rest of us!)

Now we’ve seen the dangers of not using type hints, we’ll update our add($x, $y) method to describe the types we’re expecting:

public function add(float $x, float $y)
{
    return $x + $y;
}

Now when we run our test again we get a different error message:

$ ./vendor/bin/phpunit tests/AdditionTest.php --filter testEasyMathKnowsHowToAdd
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

......E.                                                            8 / 8 (100%)

Time: 28 ms, Memory: 4.00MB

There was 1 error:

1) EasyMath\Tests\AdditionTest::testEasyMathKnowsHowToAdd with data set #6 ('php', 'arch', 'phparch')
TypeError: Argument 1 passed to EasyMath\Addition::add() must be of the type float, string given, called in /Users/joeferguson/Code/php-easy-math/tests/AdditionTest.php on line 21

/Users/joeferguson/Code/php-easy-math/src/Addition.php:19
/Users/joeferguson/Code/php-easy-math/tests/AdditionTest.php:21

ERRORS!
Tests: 8, Assertions: 7, Errors: 1.

Because we have specified our parameters in our method signature to be float PHP can see we’re passing in a string and throw an error to show us what went wrong. Now we have some type hints we can write another test method and data provider to verify our types are constrained to our specifications.

testEasyMathAddTypes() test:

/**
 * @param float $x
 * @param float $y
 * @param float $expected
 * @dataProvider badMathProvider
 * @expectedException TypeError
 */
public function testEasyMathAddTypes($x, $y, $expected)
{
    $math = new Addition();

    $this->assertEquals(
        $expected,
        $math->add($x, $y)
    );
}

Our badMathProvider() method:

public function badMathProvider()
{
    return [
        [‘php’, ‘arch’, ‘phparch’],
    ];
}

Running our new test shows us because we used the @expectedException annotation PHPUnit will ensure the code throws our specified TypeError exception when it attempts to add our strings together:

$ ./vendor/bin/phpunit tests/AdditionTest.php --filter testEasyMathAddTypes
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 56 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

We can also update all of our test method signatures to also include our type hints: float $x, float $y, float $expected and run all of our tests to verify everything is working as we expect:

$ ./vendor/bin/phpunit tests/AdditionTest.php
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

.............                                                     13 / 13 (100%)

Time: 26 ms, Memory: 4.00MB

OK (13 tests, 13 assertions)

Since we have type hinted our method signatures in our Addition, AdditionTest classes and added a test case to cover invalid types we can be certain anyone using our library will be properly warned when if they do not use the right types when calling our methods. Our type hints also will provide value to users of modern code editors or IDEs which can help hint the user to the right types our methods are expecting. You can see an example of how this appears in PhpStorm below:

02-2019-7.3.png

Type Hints In & Out

Another feature brought to us in PHP 7 is return type hinting. We can specify in our method signatures the type of data we’ll be returning. Since our add(float $x, float $y) method takes floats into the method it makes sense we specify we’ll return a float as well:

public function add(float $x, float $y) :float
{
    return $x + $y;
}

This will also result in a nice hint in your editor or IDE when you go to use the variable you stored the result of our add() method such as PhpStorm will show us:

02-2019-7.3-2.png

We can also type hint when our method will return a class instead of a specific value. Given the following code:

<?php
interface Cat {
    static function make(): Cat;
}
class Dog implements Cat {
    static function make(): Cat {
        return new Dog();
    }
}

Here we have an interface Cat with a type hinted make() method will return and instance of our Cat interface. Then we create a Dog class which implements our Cat interface make() method which returns a new instance of the Dog class. The Dog class implements Cat so our example code is valid and we have created… CatDog? DogCat?

If we changed our Dog class’ make() return type to Dog the code would throw an error because the class would then return an instance of Cat from our Cat interface which wouldn’t match our Dog return type:

<?php
interface Cat {
    static function make(): Cat;
}
class Dog implements Cat {
    static function make(): Dog { // Type Error
        return new Dog();
    }
}

If we run this code we’ll see the following error: Fatal error: Declaration of Dog::make(): Dog must be compatible with Cat::make(): Cat. Clearly making Cats and Dogs just aren’t compatible.

Somewhat related: “CatDog” Theme Song (HQ) | Episode Opening Credits | Nick Animation - YouTube

Null Coalesce Operator (??) /editor note “??” IS the operator

The null coalesce operator may be my favorite PHP 7 feature because it allows me to remove so many isset() statements from my code. Pre 7 you always had to ensure your variable existed before attempting to use it. This is a reasonable assumption for the PHP engine to make however sometimes data doesn’t exist where our application may expect it to The most common pain point for me has been when upgrading older applications and having to deal with any number of $_GET parameters which may or may not exist.

Often times I find methods which handle both POST and GET request data where the first 20 lines or so is full of if statements checking isset() on different keys in $_POST or $_GET (or both). Imagine we have the following code (and please forgive the absence of filter_var() for sanitizing the input):

<?php
public function handleRequest()
{
    // handle GET Parameters
    if(isset($_GET[‘code’]))
    {
        $code = $_GET[‘code’];
    }
    // handle POST Parameters
    if(isset($_POST[‘name’]))
    {
        $name = $_GET[‘name’];
    }
}

We have 9 lines of code to check and set two variables. Don’t worry because the PHP internals team gave us the null coalesce operator which allows us to ditch those isset() checks for the new ?? operator. We can break down the previous code into this much cleaner example:

<?php
public function handleRequest()
{
    // ?name=Joe
    $code = $_GET[‘code’] ?? 123456;
    $name = $_GET[‘name’] ?? null;
    echo $code; // 123456
    echo $name; // Joe
}

Now we’ve turned 9 lines of code containing two if statements into 5 lines of code and we added functionality of echoing our data back to the user. Under the hood, PHP is checking to see if $_GET[‘code’] is set and if not the value of 123456 is saved as the value of our $code variable. Assuming we visited this page with ?name=Joe in our query string we would see the string Joe on the page after 123456. Because we specified Joe in the query string ?name=Joe the variable $_GET[‘name’] will have a value of Joe, which is saved as our $name variable.

If you’re familiar with traditional PHP ternary operators the ?? of the null coalesce operator will feel pretty natural. If you’re new to ternary operators you can read more and explore a new way of short conditional syntax in the PHP: Comparison Operators - Manual

A warning about ternary operators. Ternary operators make quick work of conditional checks and when used properly will save processing time however the tradeoff is less readable code. Once you have several ternary operations happening it can be a slippery slope into a rabbit hole of unreadable code. For the sake of clean, readable code make sure you the code is still readable after you’ve refactored. Read it out loud and see if the logic still makes sense.

While the null coalesce operator may seem underwhelming the real power comes from being able to chain these statements together to cover different scenarios without having to repeatedly check for values of variables:

// PHP 5.x
if (!empty($firstName)) $name = $firstName;
else $name = $firstName;

if(is_null($name)) $name = $username;
else $name = ‘Guest’;

// PHP 7.x
$name = $firstName ?? $username ?? ‘Guest’;

We have been able to consolidate our two if statements to determine if we should use $firstName or $username as a name variable depending on what we have set for the user and if all else fails we’ll use Guest. The PHP 7 code is much cleaner.

Exception Handling

The biggest breaking change for PHP 7 I have encountered has been the changes in exception handling. This is where I spent a lot of my time refactoring legacy PHP 5.x applications so we can push for the latest PHP 7 version. Why the breaking change? Previously in PHP 5.x a fatal error would not be caught with error handling and your code would just die and fall over. Depending on your error_reporting settings this can be as trivial as a blank/white screen being shown to the user or if you haven’t configured your error settings properly you may be showing the end user a full stack dump and stack trace of the fatal error which likely contains very sensitive information about your application including database credentials, API keys used or other private information. Also when a script terminates prematurely there may be other system resources left hanging, waiting for the script to finish processing. If the script never informs the system it has failed the system may hold those resources open indefinitely tying up valuable RAM, CPU of the server.

Error Reporting

Not sure what your error_reporting settings should be? Check out the manual for error_reporting and also the predefined constants for use as reporting levels. Typically for production systems I set display_errors to off so a user will never see an error page while log_errors is set on and error_log is a valid path to where we want our errors logged. This way in production I can rest assured users will not see errors but they will be logged for developers to review.

In PHP 7 when an error occurs a new Throwable interface is used to throw an instance of the Exception or Error class. Earlier when we were adding our scalar type hints and return types, we saw this in action when we tried to pass a string into a method which was expecting a float value. The PHP engine caught the TypeError (which implements the Throwable interface) and gave us the wrong type error message.

Since PHP 7 contains the breaking change how do we refactor our code to be compatible with either PHP 5.x or 7.x? This is the most common question I am asked when teaching PHP 7. When I’m working on a legacy code base still running on PHP 5 and the goal is to migrate to newer versions of PHP 7 what I like to implement is error handling to take advantage of both versions so we can run our code in PHP 7 to ensure everything functions as expected while maintaining compatibility with PHP 5 until we can upgrade all of our infrastructure. You can accomplish the same compatibly with your exception handling via try()/catch(). Since Throwable doesn’t exist in PHP 5 the catch (Throwable $t) will not be executed so the Exception falls down to the next catch (Exception $e) block. In PHP 7 because Throwable does exist it will always catch and never fall through to the Exception catch statement:

try {
    throw new Exception("Something Happened!");
} catch (Throwable $t) {
    // Handled in PHP 7 only
    echo($t->getMessage());
} catch (Exception $e) {
    // Handled in PHP 5 only
    echo($e->getMessage());
}

This code will output the same thing from PHP versions 5.1.0 to 5.6.38 and 7.0.0 - 7.3.1. If you’re outside of those PHP versions you’ll need more than my help to get the problem fixed :)

How do you know how the code will output across PHP versions? Take our try catch block and visit https://3v4l.org, enter the block, make sure to click eol versions and click the eval(); button to see the differences across PHP versions.

Examples of PHP 7 error handling

Catching TypeError

We can add a try catch for TypeError to always ensure our add() method in our library gracefully handles non-float types:

try {
    echo $add->add(‘up’,’dog’);
} catch (TypeError $e) {
    // Caught a TypeError
    echo $e->getMessage(), “\n”;
    // Argument 1 passed to EasyMath\Addition::add()
    //  must be of the type float, string
}

Catching Invalid Function

What happens if you typo a function name? We can easily catch ‘Call to undefined methods’ which are caught as Error class:

try {
    echo $add->addition(‘up’,’dog’);
} catch (Error $e) {
    // Caught Error
    echo $e->getMessage(), “\n”;
    // Call to undefined method EasyMath\Addition::addition()
}

You can also create your own Error and Exception classes, you’ll want to implement the Throwable interface and you’re ready to start throwing around your own custom PHP 7 errors!

Recap

This month we covered the three big issues I see in upgrading projects from PHP 5.x to PHP 7 whether it’s upgrading your exception handling to be PHP 7 and 5 compatible, adding scalar type hints to better leverage static analysis tools, or cleaning up noisy isset() calls by using the null coalesce operator. Make sure you check out the upgrade guide for a full list of breaking changes, deprecations, and other considerations when upgrading.

Happy Refactoring!

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the February 2019 issue from http://phparch.com for the professionally edited version.