Static Code Analysis in PHP

PHP Usergroup Münster

About Me

Benjamin Cremer

Benjamin Cremer
@benjamincremer

PHP Usergroup Münster

Once a month. Speakers wanted!
meetup.com/phpugms
@phpugms

Questions?

Feel free to ask at any time

Compile Time
vs.
Runtime Errors

Syntax Errors

A special kind of compile time error


function foo() {
    bar()
    $baz
}
    

# Run PHP Syntax check (lint)
$ php -l example.php
PHP Parse error:  syntax error, unexpected '$baz' (T_VARIABLE) in example.php
on line 4
Errors parsing syntax-error.php
    

# "compile" code
# Syntax errors are also compile-time errors
$ php example.php
PHP Parse error:  syntax error, unexpected '$baz' (T_VARIABLE) in example.php on line 4
    

Run Syntax Checks with PHP Lint

  • First line of defence
  • Easy to implement
  • Fast

Syntax Check with PHP Parallel Lint

Wrapper around php -l with several options for your convenience. checks your files in parallel making use of your multiple-core machines.

Screenshot of PHP-Parallel-Lint

Compile Time Errors


interface Car
{
    public function drive(): int;
}

class Tesla implements Car
{
    public function drive(): string
    {
        return 'fasel';
    }
}
            

$ php -l example.php
No syntax errors detected in example.php
            

$ php example.php
PHP Fatal error:  Declaration of Tesla::drive(): string must
be compatible with Car::drive(): int in example.php on line 8
            

Runtime Errors


class Car
{
    public function drive(): void {}
}

function testdrive(Car $car) {
    $car->fly();
}
            

$ php -l example.php
No syntax errors detected in example.php
                    

$ php example.php
(Exit code: 0)
               

class Car
{
    public function drive(): void {}
}

function testdrive(Car $car) {
    $car->fly();
}

testdrive(new Car()); // execute above code
            

$ php example.php
PHP Fatal error:  Uncaught Error: Call to undefined method Car::fly()
in example.php:9
               

class Car
{
    public function drive(): void {}
}

function testdrive(Car $car, bool $fly) {
    if ($fly) {
        $car->fly();
    } else {
        $car->drive();
    }
}
            

testdrive(new Car(), false); // works just fine
           

testdrive(new Car(), true); // kaboom
           

Write Tests?

Write a lot of Tests?

Types vs. Tests

Ideology

A talk by Gary Bernhardt from Strange Loop 2015

destroyallsoftware.com/talks/ideology

Static Analysis

What is Static Analysis?

Predict the behavior of software without running it.

Promote runtime errors to compile time error.

Find Bugs In Your Code Without Writing Tests!

The analyser needs your help

Type Inference

Inferring types when types are not given.


function add_strings($part1, $part2)
{
    // concat operation always yields a string
    return $part1 . $part2;
}

// $foo can be inferred as string
$foo = add_string($unknownA, $unknownB);
            

/**
 * @param DateTimeInterface $dateTime
 * @param int $days
 * @return \DateTimeInterface
 */
function addDays($dateTime, $days) {
    [...]
}
            

function addDays(
    \DateTimeInterface $dateTime,
    int $days
): \DateTimeInterface {
    [...]
}
            

/**
 * @return string[]
 */
function getDaysOfWeek(): array {
    return [
        'monday',
        'tuesday',
        [...]
    ];
}
            

            /**
             * @method Booking|null findOneByUuid(string $uuid)
             * @method Booking[] findByUuid(string $uuid)
             */
            class BookingRepository extends EntityRepository
            {
            }
            

/** @var \DateTimeInterface $date */
$date = frameworkmagic();
            

$date = frameworkmagic();
assert($date instanceof \DateTimeInterface);
            

PHP-CS-Fixer and PHP_CodeSniffer can assist you with that

PHP_CodeSniffer Demo


                php composer require --dev doctrine/coding-standard
            

Gimme the tools

PHPStorm + Php Inspections (EA Extended)

Not very CI friendly

yes, there is a inspection.sh, but no

Abstract syntax tree

PHP AST Parsers

nikic/php-parser by Nikita Popov
A PHP parser written in PHP

nikic/php-ast by Nikita Popov
Extension that exposes the abstract syntax tree generated by PHP 7

nikic/php-parser Demo


php composer require nikic/php-parser
./vendor/bin/php-parse src/ast_example.php
            
vimeo/psalm phan/phan phpstan/phpstan
Author Vimeo Rasmus Lerdorf Ondřej Mirtes
PHP Version >= 5.6 >= 7.0 >= 7.1
AST PHP-Parser ext-ast PHP-Parser

And so much more: https://github.com/exakat/php-static-analysis-tools

PHPStan

  • fast
  • extensible
  • in active development
  • used in Build-Pipelines at CHECK24

Usage


                composer require --dev phpstan/phpstan
                ./vendor/bin/phpstan analyse -l3 src/
            

                docker run --rm -v $PWD:/app phpstan/phpstan analyse -l3 /app/src/
            

namespace Example;

class Testdriver
{
    public static function testdrive(Car $car, bool $fly)
    {
        if ($fly) {
            $car->fly();
        } else {
            $car->drive();
        }
    }
}
            

$ ./vendor/bin/phpstan analyse -l3 src/Testdriver.php
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ -------------------------------------------------
  Line   Testdriver.php
 ------ -------------------------------------------------
  9      Call to an undefined method Example\Car::fly().
 ------ -------------------------------------------------


 [ERROR] Found 1 error
            

Demo

https://github.com/bcremer/shopware/tree/add-phpstan-config

Thank you!

Please leave feedback

@benjamincremer
talks.benjamin-cremer.de/phpugms_sca

PHP Usergroup Münster

meetup.com/phpugms
@phpugms