Strict Typing & Static Analysis Rob Allen Rob Allen ~ @akrabat
A presentation at Sunshine PHP in February 2020 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
