Explorez votre code avec de l'analyse statique

đź”— https://julien.deniau.me/phpstan/

Je m'appelle Julien Deniau

Je suis un vieux… codeur

J'ai codé mes premiers sites :

  • sur membres.lycos.fr
  • en PHP 3
  • avec Macromedia Dreamweaver
  • Pas de fichier ".h" du C
  • Pas de `System.out.println()` du Java
  • 
                                    echo '<html>'; // et c'est parti !
                                


Bref, PHP c'Ă©tait l'aventure !

Parse error: syntax error, unexpected T_PAAMAYIM_NEKUDOTAYIM
PHP Parse error: syntax error, unexpected '$a' (T_VARIABLE) on line 6

Mais la liberté du langage était tellement belle…

PHP Fatal error: Uncaught Error:
Call to a member function getTitle() on null

đź‘Ť voyager

♥ avec une carte

👍 découvrir

♥ comprendre avec un dictionnaire

đź‘Ť construire

♥ manuel

Je suis un aventurier

Je suis un explorateur.

Aventurier : seul avec ses bytes et son couteau

Explorateur : aller fouiller avec les outils adaptés

Les outils

Tests

PHP 7 : structuration du langage, typage statique

Plus de méthode avec paramètre inconnu !



                                Type error: Argument 1 passed to slugify()
                                must be of the type string or null, integer given
                            
↓
Fatal en prod !

Les outils

Tests
Analyse
statique
  • Analyseur : Il "lit" le code,
  • Statique : il n'exĂ©cute pas le programme

Des dizaines d'analyseurs statiques

exakat/php-static-analysis-tools

PHPStan

PHP Static Analysis Tool

phpstan/phpstan

PHPStan n'est pas

  • Un framework de test,
  • Du code qui s'exĂ©cute,
  • Magique : il ne devine pas ce que vous ne lui dites pas
class Foo {
    private $bar;  // considéré comme "@mixed"
    
    public function __construct(array $bar) {
        $this->bar = $bar;
    }
    
    public function getBar() {
        return strtoupper($this->bar);
    }
}

$foo = new Foo(['io']);
$foo->getBar();
[OK] No errors
PHP Warning: strtoupper() expects parameter 1 to be string, array given
class Foo {
    /**
     * @var array
     */
    private $bar;
    
    public function __construct(array $bar) {
        $this->bar = $bar;
    }
    
    public function getBar() {
        return strtoupper($this->bar);
    }
}

$foo = new Foo(['io']);
$foo->getBar();
[ERROR] Parameter #1 $str of function strtoupper expects string, array given.

Les erreurs "de compilation"

function sayHello(string $who): string {
    return 'Hello ' . strtoupper($who);
}

$julien = new User('julien');

sayHello($julien);
                        
[ERROR] Parameter #1 $who of function sayHello expects string, User given.

Mauvaise fonction appelée

interface FooInterface {}

class Foo implements FooInterface {
  public function bar() {
    // do stuff
  }
}

function dumpFoo(FooInterface $fooInterface) {
  return  $fooInterface->bar();
}

$foo = new Foo();
dumpFoo($foo);
[ERROR] Call to an undefined method FooInterface::bar().

Mauvaise fonction appelée

use Doctrine\Common\Persistence\ObjectManager;

function doStuff(ObjectManager $entityManager)
{
  return $entityManager->createQueryBuilder()
    ->from('SomeEntity', 'a')
    ->where('a.id = 8')
    ->getResult();
}
Un ObjectManager n'a pas de méthode createQueryBuilder .
Cette méthode fait partie de Doctrine\ORM\EntityManagerInterface

Garder son code propre

Tests (conditionnel) inutiles

class Foo { public $id = 1; }

function getFoo(): Foo
{
  return new Foo();
}

$foo = getFoo();

if ($foo) {
  var_dump($foo->id);
}
                        
[ERROR] If condition is always true.

Conversions inutiles...

$a = (int) 8 % 3; 
[ERROR] Casting to int something that's already int.

...ou requise !

function div(int $a, int $b): int {
    return round($a / $b);
}
[ERROR] Function div() should return int but returns float.

Eviter les fautes de frappe

// librairie externe
namespace MangoPay;
class Address {
    public $City;
}

class UserLegal {
    public $HeadquartersAddress;
}

// code métier
$tmp = new \MangoPay\UserLegal();
$tmp->HeadquarterAddress = new \MangoPay\Address();
$tmp->HeadquarterAddress->City = 'Lyon';
                        
[ERROR] Access to an undefined property MangoPay\UserLegal::$HeadquarterAddress.
function getAnArray(iterable $iterable) : array
{
    $anArray = [];
    
    foreach ($iterable as $item) {
        $getAnArray[] = $item;
    }
    
    return $anArray;
}
Implicit array creation is not allowed - variable $getAnArray might not exist.

PHP++

Docstring

/**
 * @param array<int> $anArrayOfInt
 */
function doStuff(array $anArrayOfInt) {
  foreach ($anArrayOfInt as $anInt) {
    echo strtoupper($anInt);
  }
}
Parameter #1 $str of function strtoupper expects string, int given.

Types multiples

use Doctrine\Common\Persistence\Collections\Collection;
	
/**
 * @var array<int>|Collection<int> $var
 */
function doStuff($var) {
  if ($var instanceof Collection) {
    $var = $var->toArray();
  }
  
  // `$var` sera reconnu comme un tableau de int
}

Covariance

PHP 7.4+
interface Factory {
    function make(): object;
}

class UserFactory implements Factory {
    function make(): User;
}
En attendant
class UserFactory implements Factory {
    /**
     * @return User
     */
    function make(): object;
}
đź”— RFC Covariance / contravariance

Validation graduelle

Niveau de 0 à 7 + règles additionelles

PHPStan est rapide

700 fichiers en 30 secondes
Rien Ă  faire de plus que d'Ă©crire et (bien) documenter votre code

Concurrence ?

phan/phan

vimeo/psalm

Lequel utiliser ?

Comparatif par Badoo tech

Citations

(De mes collègues)
Au début, j’ai ressenti un mélange de frustration et de grand intérêt. […] Stan me mets régulièrement face à mes manquements.
Thomas
…un peu contraignant mais si on pèse le pour et le contre ça en vaut le détour.
L'investissement valait le coup, le code est plus propre…
Sylvère
… chiant au début quand on connait pas […] un peu comme quand tu découvres les tests :
puis quelques mois après tu lances des deploy en sirotant un petit kir cassis…
Dimitri

Merci !

Des questions ?