A presentation at Sunshine PHP in in Miami, FL, USA by Rob Allen
Strict Typing & Static Analysis Rob Allen Rob Allen ~ @akrabat
Use Types to Help Focus on What You’re Doing, Not How You’re Doing It Anthony Ferrara, 2016 Rob Allen ~ @akrabat
PHP Types boolean float array callable resource integer string object iterable NULL Rob Allen ~ @akrabat
PHP is a dynamically typed language Rob Allen ~ @akrabat
Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); Rob Allen ~ @akrabat
Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); // string(15) “Two + three = 5” Rob Allen ~ @akrabat
Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); // string(15) “Two + three = 5” var_dump(“10” + 5); Rob Allen ~ @akrabat
Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); // string(15) “Two + three = 5” var_dump(“10” + 5); // int(15) Rob Allen ~ @akrabat
Rob Allen ~ @akrabat
But… $result = 10 + “10,000”; var_dump($result); Rob Allen ~ @akrabat
But… $result = 10 + “10,000”; var_dump($result); // int(20) Rob Allen ~ @akrabat
But… $result = “false”; if ($result) { echo “TRUE”; } else { echo “FALSE”; } Rob Allen ~ @akrabat
But… $result = “false”; if ($result) { echo “TRUE”; } else { echo “FALSE”; } // TRUE Rob Allen ~ @akrabat
Rob Allen ~ @akrabat
You can’t trust a computer! Rob Allen ~ @akrabat
Test all the things Rob Allen ~ @akrabat
Validate your inputs function foo($i, $b) { if (! is_int($i)) { throw new Exception(‘$i must be an integer’); } if (! is_bool($b)) { throw new Exception(‘$b must be a boolean’); } // … } & write tests for the new code-paths Rob Allen ~ @akrabat
A type system solves this class of errors Rob Allen ~ @akrabat
If we filter input by a type — you can think of it as a subcategory of all available input — many of the tests become obsolete. @brendt_gd Rob Allen ~ @akrabat
Function Type Declarations function foo(int $i, bool $b) : string { // … } • $i is always an integer • $b is always a boolean • foo() will always return a string Rob Allen ~ @akrabat
This function is much easier to reason about Rob Allen ~ @akrabat
Coercion of Typed Declarations function foo(int $i, bool $b) : string { // … } foo(“1”, 1); // $i = int(1) // $b = bool(true) Rob Allen ~ @akrabat
Strictly Typed Declarations declare(strict_types=1); foo(“1”, 1); Fatal error: Uncaught TypeError: Argument 1 passed to foo() must be of the type int, string given Rob Allen ~ @akrabat
This reduces cognitive load Rob Allen ~ @akrabat
Typed Properties in PHP 7.4! We can now add types to class properties class Person { public int $age; } Rob Allen ~ @akrabat
Initialisation of Typed Properties $liz = new Person(); var_dump($liz->age); Rob Allen ~ @akrabat
Initialisation of Typed Properties $liz = new Person(); var_dump($liz->age); Fatal error: Uncaught Error: Typed property Person::$age must not be accessed before initialization Rob Allen ~ @akrabat
Coercion of Typed Properties class Person { /* … */} $liz = new Person(); $liz->age = “93”; // int(93) Rob Allen ~ @akrabat
Strictly Typed Properties declare(strict_types=1); $liz = new Person(); $liz->age = “93”; Fatal error: Uncaught TypeError: Typed property Person::$age must be int, string used Rob Allen ~ @akrabat
But that’s all at runtime… Rob Allen ~ @akrabat
Static Analysis checks our code before we run it Rob Allen ~ @akrabat
Static Analysis Static code analysers simply reads your code and points out errors They ensure: • • • • • no syntax errors classes, methods, functions constants, variables exist no arguments or variables unused DocBlocks make sense data passed to methods are the correct type Rob Allen ~ @akrabat
Static Analysers Interactive: • IDE Rob Allen ~ @akrabat
PhpStorm Rob Allen ~ @akrabat
Static Analysers Interactive: • IDE Command line: • Phan • PHPStan • Psalm Rob Allen ~ @akrabat
Phan • • • • • Developed by Rasmus Lerdorf & Etsy Stable Parses entire code base and then analyses Requires php-ast extension Doesn’t parse /** @var Foo $foo */; use assert() instead Rob Allen ~ @akrabat
Phan 1. composer require —dev “phan/phan” 2. vendor/bin/phan —init —init-level=1 3. vendor/bin/phan —memory-limit 2G Rob Allen ~ @akrabat
Phan output Rob Allen ~ @akrabat
Phan output Rob Allen ~ @akrabat
PHPStan • • • • • Developed by Ondřej Mirtes Fast Can analyse subset of code base Autoloads classes Support for framework specific magic methods Rob Allen ~ @akrabat
PHPStan 1. composer require —dev “phpstan/phpstan” 2. vendor/bin/phpstan analyse app lib —level=max Rob Allen ~ @akrabat
PHPStan output Rob Allen ~ @akrabat
PHPStan output Rob Allen ~ @akrabat
Psalm • • • • • Developed by Vimeo Comprehensive Can analyse subset of code base Autoloads classes Support custom PHPDoc tags (e.g. @psalm-param) Rob Allen ~ @akrabat
Psalm 1. composer require —dev “vimeo/psalm” 2. vendor/bin/psalm —init 3. vendor/bin/psalm Rob Allen ~ @akrabat
Psalm output Rob Allen ~ @akrabat
Psalm output Rob Allen ~ @akrabat
Observations • All are good at finding type issues • All three also found found different issues • Phan found unused use statements • PHPStan found logic errors • Psalm is more type-picky, especially with mixed Rob Allen ~ @akrabat
What Problems Do Static Analysers Find? Rob Allen ~ @akrabat
Report 1 class Acl extends BaseAcl { protected $siteKey = ”; protected $settings; public function __construct(array $settings) { $this->settings = $settings; } Rob Allen ~ @akrabat
Report 1 class Acl extends BaseAcl { protected $siteKey = ”; protected $settings; public function __construct(array $settings) { $this->settings = $settings; } INFO: MissingPropertyType - Property Acl::$siteKey does not have a declared type - consider string Rob Allen ~ @akrabat
Report 1: Fix (PHP 7.4) class Acl extends BaseAcl { protected string $siteKey = ”; protected $settings; Add property’s type Rob Allen ~ @akrabat
Report 1: Fix (PHP 7.3) class Acl extends BaseAcl { /** @var string */ protected $siteKey = ”; protected $settings; Use a docblock to declare property’s type Rob Allen ~ @akrabat
Report 2 public function setSiteKey($siteKey) { $this->siteKey = $siteKey; return $this; } Rob Allen ~ @akrabat
Report 2 public function setSiteKey($siteKey) { $this->siteKey = $siteKey; return $this; } INFO: MissingParamType - Parameter $siteKey has no provided type INFO: MissingReturnType - Method setSiteKey does not have a return type, expecting Acl Rob Allen ~ @akrabat
Report 2: Fix public function setSiteKey(string $siteKey) { $this->siteKey = $siteKey; return $this; } Add parameter’s type Rob Allen ~ @akrabat
Report 2: Fix public function setSiteKey(string $siteKey): Acl { $this->siteKey = $siteKey; return $this; } Add parameter’s type & return type Rob Allen ~ @akrabat
An bona fide potential bug! public function populate(): void { $siteKey = $this->getSiteKey() ?: 1; $routes = $this->router->slugRoutes($siteKey); foreach ($routes as $route) { Rob Allen ~ @akrabat
An bona fide potential bug! public function populate(): void { $siteKey = $this->getSiteKey() ?: 1; $routes = $this->router->slugRoutes($siteKey); foreach ($routes as $route) { ERROR: InvalidScalarArgument Argument 1 of slugRoutes expects string, int(1)|non-empty-string provided Rob Allen ~ @akrabat
Fix for this potential bug public function populate(): void { $siteKey = $this->getSiteKey() ?: SiteKey::DEFAULT; $routes = $this->router->slugRoutes($siteKey); foreach ($routes as $route) { Change the default to the new default Rob Allen ~ @akrabat
Another potential bug public function home(Request $request) { $name = $request->get(‘key_name’); $group = $this->cgService->loadByKey($name); … Rob Allen ~ @akrabat
Another potential bug public function home(Request $request) { $name = $request->get(‘key_name’); $group = $this->cgService->loadByKey($name); … ERROR: InvalidArgument - Argument 1 of ChoiceGroupService::loadByKey expects string, array<array-key, mixed>|mixed|null provided Rob Allen ~ @akrabat
Query parameters $name = $request->get(‘key_name’) example.com/home?key_name=FOO $name = ‘FOO’ Rob Allen ~ @akrabat
Query parameters $name = $request->get(‘key_name’) example.com/home?key_name=FOO $name = ‘FOO’ example.com/home $name = null Rob Allen ~ @akrabat
Query parameters $name = $request->get(‘key_name’) example.com/home?key_name=FOO $name = ‘FOO’ example.com/home $name = null example.com/home?key_name[]=FOO&key_name[]=BAR $name = [‘FOO’, ‘BAR’] Rob Allen ~ @akrabat
Fix for this potential bug public function home(Request $request) { $name = $request->get(‘key_name’); if (!is_string($name)) { throw new InvalidInputException(‘key_name’); } $group = $this->cgService->loadByKey($name); … Validate input & error when invalid Rob Allen ~ @akrabat
Collections class ChoiceService { public function fetchChoices(): array { … } … usage: foreach ($service->fetchChoices() as $choice) { … } Rob Allen ~ @akrabat
Collections /** * @return Choice[] */ public function fetchChoices(): array { … } Declare type of array returned using a docblock Rob Allen ~ @akrabat
Collections /** * @return Choice[] */ public function fetchChoices(): array { … } ERROR: InvalidReturnStatement - The type ‘array{0: Choice|null}’ does not match the declared return type ‘array<array-key, Choice>’ for ChoiceService::fetchChoices Rob Allen ~ @akrabat
False positive? // loadByKey() returns: ?Choice $choice = $choiceService->loadByKey($keyName); if (!$choice) { $this->redirectToHome(); // throws an Exception } $data = $choice->getArrayCopy(); return $this->format($data); Rob Allen ~ @akrabat
False positive? // loadByKey() returns: ?Choice $choice = $choiceService->loadByKey($keyName); if (!$choice) { $this->redirectToHome(); // throws an Exception } $data = $choice->getArrayCopy(); return $this->format($data); Cannot call method getArrayCopy on possibly null value Rob Allen ~ @akrabat
False positive?: Fix /** * @psalm-return never-return */ public function redirectToHome(): void { // … } Guarantees that the function exits or throws an exception Rob Allen ~ @akrabat
Psalm annotations @psalm-var, @psalm-param & @psalm-return: Psalm versions for types not understood by phpDocumenter never-return adds an implicit exit at end of function Rob Allen ~ @akrabat
Psalm annotations @psalm-var, @psalm-param & @psalm-return: Psalm versions for types not understood by phpDocumenter never-return adds an implicit exit at end of function @psalm-seal-properties: Prevent __set/__get not declared with @property Rob Allen ~ @akrabat
Psalm annotations @psalm-var, @psalm-param & @psalm-return: Psalm versions for types not understood by phpDocumenter never-return adds an implicit exit at end of function @psalm-seal-properties: Prevent __set/__get not declared with @property @psalm-suppress: Suppress specific Psalm issues Full list: https://psalm.dev/docs/annotating_code/supported_annotations Rob Allen ~ @akrabat
Applying Static Analysis to Your Project Rob Allen ~ @akrabat
Applying to Your Project • Add to CI Rob Allen ~ @akrabat
Applying to Your Project • Add to CI • Use levels and fix the “easy” items first • Set to lowest level and fix all issues • Increase level and repeat Rob Allen ~ @akrabat
1745 errors found 3229 other issues found Rob Allen ~ @akrabat
Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml Rob Allen ~ @akrabat
Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml 2. Subsequent psalm runs will treat all errors in the baseline file as warnings Rob Allen ~ @akrabat
Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml 2. Subsequent psalm runs will treat all errors in the baseline file as warnings 3. Fix warnings as you can (and allow no new errors!) Rob Allen ~ @akrabat
Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml 2. Subsequent psalm runs will treat all errors in the baseline file as warnings 3. Fix warnings as you can (and allow no new errors!) 4. Update baseline as you go: vendor/bin/psalm —update-baseline Rob Allen ~ @akrabat
Beware surreptitious updating of the baseline! Rob Allen ~ @akrabat
To Sum Up Rob Allen ~ @akrabat
Takeaways • PHP 7’s type declarations reduce cognitive load Rob Allen ~ @akrabat
Takeaways • PHP 7’s type declarations reduce cognitive load • Upgrade to PHP 7.4 for typed properties! Rob Allen ~ @akrabat
Takeaways • PHP 7’s type declarations reduce cognitive load • Upgrade to PHP 7.4 for typed properties! • Static analysis finds bugs in your code Rob Allen ~ @akrabat
Takeaways • • • • PHP 7’s type declarations reduce cognitive load Upgrade to PHP 7.4 for typed properties! Static analysis finds bugs in your code Use plugins to give additional info to your static analyser Rob Allen ~ @akrabat
Takeaways • • • • • PHP 7’s type declarations reduce cognitive load Upgrade to PHP 7.4 for typed properties! Static analysis finds bugs in your code Use plugins to give additional info to your static analyser Autofixers are great! e.g. psalter, rector Rob Allen ~ @akrabat
Takeaways • • • • • • PHP 7’s type declarations reduce cognitive load Upgrade to PHP 7.4 for typed properties! Static analysis finds bugs in your code Use plugins to give additional info to your static analyser Autofixers are great! e.g. psalter, rector Add static analysis to your CI Rob Allen ~ @akrabat
Thank you! https://joind.in/talk/3e63b Rob Allen - Independent API developer Rob Allen ~ @akrabat
Photo credits - Watch mechanism: https://www.flickr.com/photos/shinythings/2168994732 - Rocket launch: https://www.flickr.com/photos/gsfc/16495356966 - Stars: https://www.flickr.com/photos/gsfc/19125041621 Rob Allen ~ @akrabat
PHP 7 introduced type declarations and so brought PHP into the world of strict typing. Each subsequent release in the 7 series has improved this and PHP 7.4 is no different with exciting new type features. In addition, PHP 7 enabled static analysis tools that coupled with type declarations enable us to significantly remove bugs in our code before we even run it! In this talk, I will review PHP’s strict type system, including the new PHP 7.4 features and show how they can make our code safer and clearer and easier to reason about. We will then turn our attention to the available static analysis tools and look at how, with strict typing, we can eliminate whole classes of bugs and make our applications better. By the end of this session, you will be well placed to write better PHP code that has fewer bugs and works as you expect every time.
Here’s what was said about this presentation on social media.
@akrabat giving fun PHP typing common problems and sharing wisdom how to guarantee expected types #sunphp20 @SunShinePHP pic.twitter.com/GhvjsNFIVk
— Mark Niebergall (@mbniebergall) February 7, 2020
Rob Allen’s “Strict Typing and Static Analysis” talk at @SunShinePHP was fantastic! Great pace, very informative! Plus, the British accent made me feel like I was in a Lexus “December to remember” commercial. Win-win! @akrabat
— Jason Pancake 🥞 (@jasonpancake) February 7, 2020