Author: Joseluis Laso
Viewers: 295
Last month viewers: 16
Categories: PHP Tutorials
Read this article to learn how you can write more robust tests in practice with the help of strict type hinting.
Contents
Return Type Declarations and Scalar Type Hints
Advantages of Type Hinting in Function Declarations
Examples Applied to Tests
Conclusion
Return Type Declarations and Scalar Type Hints
Following the last post about this subject, now we can see how this improvement will help us to write better code, more understandable and easier to create more robust tests.
With PHP 7, when reading the declaration of a function, the developer knows exactly what the function expects. But, without this improvement we could only figure out the parameters through the PHPdoc, or maybe the name of the function or its parameters, i.e.: a function called sum will expect two numeric parameters, and a function convertToHtml will return a string, a parameter called $content or $text probably holds an string, whilst an $int parameter will hold an integer.
But, what happens if the program fails and sends a string when the function expects an integer? Now, with this improvement we no longer have to guess.
Advantages of Type Hinting in Function Declarations
Lets see a couple of sample codes and how we can use this new improvement to our benefit:
<?php declare(strict_types=1); class Math { public static function factorial(int $number): int{ if ($number < 0){ throw new Exception( factorial expects a positive number ); } if (1 >= $number){ return 1; } return self::factorial( $number - 1) * $number; } }
The first thing we have gained with type declarations is that we do not need to check if the parameters are non-numerical, because it is not allowed. If you pass an incorrect type PHP throws a type exception when the code is executed.
Another advantage is that we no longer need to have PHPDoc. Personally, I prefer to have this extra information, but some colleagues sure will be thankful not having to write this kind extra documentation.
In addition: with the strict_types=1 declaration. Math::factorial("1"); throws an Exception, but works fine in weak mode, as expected.
When you write the tests, you no longer have to need to write code to check the types manually. In other words: if the function does not accept a string, we do not have to check this case.
Maybe the above explanation will be more clear with an example:
In strict mode:
<?php declare(strict_types=1); require_once __DIR__."/../Math.php"; class MathTest extends PHPUnit_Framework_TestCase { public function testFactorial() { $this->assertEquals(5 * 4 * 3 * 2, Math::factorial(5) ); $this->assertEquals( Math::factorial(8) * 9, Math::factorial(9) ); } }
In weak mode:
<?php declare(strict_types=0); require_once __DIR__."/../Math.php"; class MathTest extends PHPUnit_Framework_TestCase { public function testFactorial() { $this->assertEquals( 5 * 4 * 3 * 2, Math::factorial(5)); $this->assertEquals( Math::factorial(8) * 9, Math::factorial(9) ); } public function testFactorialWeak() { $this->assertEquals( 4 * 3 * 2, Math::factorial("4") ); } }
Examples Applied to Tests
<?php declare(strict_types=1); // local file calls are strict-type checked class HtmlHelper { protected static function arrayToList(array $items, bool $ordered): string { $result = $ordered ? '<ol>' : '<ul>'; foreach( $items as $item ) { $result .= sprintf( '<li>%s</li>', $item ); } $result .= $ordered ? '</ol>' : '</ul>'; return $result; } public static function arrayToUnsortedList( array $items ): string { return self::arrayToList($items, false); } public static function arrayToOrderedList( array $items ): string { return self::arrayToList($items, true); } }
The idea with this simple code is to have string return type and local calls inside the same PHP file. As I explained in my previous article, the declaration of strict_types=0|1 must be at the beginning of the file. It only affects the calls invoked in the file where this declaration is.
<?php declare(strict_types=1); require_once __DIR__."/../HtmlHelper.php"; class MathTest extends PHPUnit_Framework_TestCase { static $data; static $itemsNumber; public static function setUpBeforeClass() { self::$data = array('first element', 'second element', 'third element'); self::$itemsNumber = count(self::$data); } public function testArrayToUnsortedList() { $result = HtmlHelper::arrayToUnsortedList( self::$data ); $this->assertEquals( self::$itemsNumber, substr_count( $result, '<li>' )); $this->assertEquals( self::$itemsNumber, substr_count($result, '</li>' )); $this->assertEquals( 1, substr_count($result, '<ul>') ); $this->assertEquals( 1, substr_count($result, '</ul>') ); } public function testArrayToOrderedList() { $result = HtmlHelper::arrayToOrderedList( self::$data ); $this->assertEquals( self::$itemsNumber, substr_count($result, '<li>') ); $this->assertEquals( self::$itemsNumber, substr_count($result, '</li>') ); $this->assertEquals( 1, substr_count($result, '<ol>') ); $this->assertEquals( 1, substr_count($result, '</ol>') ); } }
As the HTML helpers always return a string, we do not have to check this case and we can limit our test to the main functionality.
To give more concrete example, I have retrieved a class from a repository of mine in github and tried to convert it to use strict typing to demonstrate its benefits.
The little class calculates the distance between two points of the earth.
My Point class before to convert to strict typing:
<?php namespace JLaso\Gps; /** * Class Point * @package JLaso\Gps * @author Joseluis Laso <jlaso@joseluislaso.es> */ class Point { /** @var float */ protected $longitude; /** @var float */ protected $latitude; /** * @param float $latitude * @param float $longitude */ function __construct($latitude, $longitude) { $this->latitude = $latitude; $this->longitude = $longitude; } /** * @param float $latitude */ public function setLatitude($latitude) { $this->latitude = $latitude; } /** * @return float */ public function getLatitude() { return $this->latitude; } /** * @param float $longitude */ public function setLongitude($longitude) { $this->longitude = $longitude; } /** * @return float */ public function getLongitude() { return $this->longitude; } /** * @param Point $point * @return float */ public function distanceTo(Point $point) { return Tools::distance($this->getLatitude(), $this->getLongitude(), $point->getLatitude(), $point->getLongitude()); } }
Applying strict typing
<?php declare(strict_types=1); namespace JLaso\Gps; /** * Class Point * @package JLaso\Gps * @author Joseluis Laso <jlaso@joseluislaso.es> */ class Point { /** @var float */ protected $longitude; /** @var float */ protected $latitude; function __construct(float $latitude, float $longitude) { $this->latitude = $latitude; $this->longitude = $longitude; } public function setLatitude(float $latitude) { $this->latitude = $latitude; } public function getLatitude(): float { return $this->latitude; } public function setLongitude(float $longitude) { $this->longitude = $longitude; } public function getLongitude(): float { return $this->longitude; } public function distanceTo(Point $point): float { return Tools::distance($this->getLatitude(), $this->getLongitude(), $point->getLatitude(), $point->getLongitude()); } }
I have created a new tag (1.1) to illustrate the improvements.
To focus our attention onto the main subject here are the differences in the Point class:
This was easy. I have just removed the PHPDoc comments and added type hints for parameters and return values.
The same thing happened for the main class named Tools.
The original code:
<?php namespace JLaso\Gps; /** * Class Tools * @package JLaso\Gps * @author Joseluis Laso <jlaso@joseluislaso.es> */ class Tools { /** * @param $latitude1 * @param $longitude1 * @param $latitude2 * @param $longitude2 * @return float distance between coordinates in kilometers * @throws \Exception */ public static function distance($latitude1, $longitude1, $latitude2, $longitude2) { if(!is_numeric($latitude1) || !is_numeric($longitude1) || !is_numeric($latitude2) || !is_numeric($longitude2)){ throw new \Exception( "distance can not be calculated with non numerical values!" ); } // normalize values $latitude1 = floatval($latitude1); $longitude1 = floatval($longitude1); $latitude2 = floatval($latitude2); $longitude2 = floatval($longitude2); $dLatitude = ($latitude2 - $latitude1) / 2; $dLongitude = ($longitude2 - $longitude1) / 2; $tmp = sin(deg2rad($dLatitude)) * sin(deg2rad($dLatitude)) + cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * sin(deg2rad($dLongitude)) * sin(deg2rad($dLongitude)); $aux = asin(min(1, sqrt($tmp))); return round(12745.9728 * $aux, 4); } /** * @param $value * @return float */ public static function toMiles($value) { return 0.621 * $value; } }
The converted code:
<?php declare(strict_types=1); namespace JLaso\Gps; /** * Class Tools * @package JLaso\Gps * @author Joseluis Laso <jlaso@joseluislaso.es> */ class Tools { public static function distance(float $latitude1, float $longitude1, float $latitude2, float $longitude2): float { $dLatitude = ($latitude2 - $latitude1) / 2; $dLongitude = ($longitude2 - $longitude1) / 2; $tmp = sin(deg2rad($dLatitude)) * sin(deg2rad($dLatitude)) + cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * sin(deg2rad($dLongitude)) * sin(deg2rad($dLongitude)); $aux = asin(min(1, sqrt($tmp))); return round(12745.9728 * $aux, 4); } public static function toMiles(float $value): float { return 0.621 * $value; } }
And the differences:
In this last case, the changes are more important, because we do not need to convert parameters to float anymore.
And finally, for the PHPunit tests we only to add the declare(strict_types=1); at the beginning of each test. Checking the special case of receiving other than float is not needed anymore.
The original test suite:
<?php use JLaso\Gps\Tools; class ConversionTest extends PHPUnit_Framework_TestCase { function testKilometersMilesConversion() { $this->assertEquals(0.621, Tools::toMiles(1)); } /** * source: www.distace.to/Origin/Destination * * Paris/Madrid 1.052,69 km #1 Paris (48.856667,2.350987) #2 Madrid (40.416691,-3.700345) */ function testParisMadridDistance() { $distance = Tools::distance(48.856667, 2.350987, 40.416691, -3.700345); // Let assume the result it's okay if the error of calculated distance is less than 1/1000 (1km) $this->assertLessThan(1.052, abs(1052.69 - $distance)); // now use the indirect method to calculate distance in the same conditions $madrid = new Point( 40.416691, -3.700345 ); $paris = new Point( 48.856667, 2.350987 ); $this->assertLessThan(1.052, abs(1052.69 - $madrid->distanceTo($paris) )); } /** * @expectedException \Exception */ function testException() { $distance = Tools::distance('a', 'b', 'c', 'd'); } }
The converted one:
<?php declare(strict_types=1); use JLaso\Gps\Tools; class ConversionTest extends PHPUnit_Framework_TestCase { function testKilometersMilesConversion() { $this->assertEquals(0.621, Tools::toMiles(1)); } /** * source: www.distace.to/Origin/Destination * * Paris/Madrid 1.052,69 km #1 Paris (48.856667,2.350987) #2 Madrid (40.416691,-3.700345) */ function testParisMadridDistance() { $distance = Tools::distance(48.856667, 2.350987, 40.416691, -3.700345); // Let assume the result it's okay if the error of calculated distance is less than 1/1000 (1km) $this->assertLessThan(1.052, abs(1052.69 - $distance)); // now use the indirect method to calculate distance in the same conditions $madrid = new Point( 40.416691, -3.700345 ); $paris = new Point( 48.856667, 2.350987 ); $this->assertLessThan(1.052, abs(1052.69 - $madrid->distanceTo($paris))); } }
Conclusion
Strict type hinting is definetly a great progress for PHP that will allow us to write more robust code with less effort to write tests.
If you would like to try my examples yourself, you can find the code in my php7-strict-types-testing repo.
What do you think? Do you think PHP 7 strict typing will also help you write more robust code? What other advantages (or disadvantages) do you see? Just post a comment with your thoughts.
You need to be a registered user or login to post a comment
1,616,107 PHP developers registered to the PHP Classes site.
Be One of Us!
Login Immediately with your account on:
Comments:
2. integers are acceptable as float input - chrisp (2016-05-29 23:53)
Note that an integer will be acceptable for float type-hinting.... - 1 reply
Read the whole comment and replies
1. They just broke PHP - Stefan Jibrail Froelich (2015-04-01 18:35)
I believe this is the first step in breaking PHP.... - 6 replies
Read the whole comment and replies