Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Static Analysis Crash Course for Framework Developers

Ondřej Mirtes
December 06, 2023
59

Static Analysis Crash Course for Framework Developers

In recent years, static analysis tools like PHPStan have become super popular in the PHP world. These tools do something cool: they can spot all sorts of mistakes in your code before you even run it.

Usually, people try these tools on their own code. But if you're the person behind a framework or library that others use, guess what? They're probably using these tools too. So, the code people write using your stuff also needs to pass these checks.

This creates an extra thing you need to worry about. You've got to make sure your framework or library works nicely with these tools. And if you're smart about it, you can even help guide other developers to use your code the right way.

In my talk I'll take you through three important steps:

1) Laying the Foundation: I'll start by covering the essentials. This involves establishing a basic standard, ensuring that users achieve a green build when working with your framework.

2) Empowering with Strong Types: The second part focuses on the potential of strong types. We'll explore how they can effectively steer users towards correct usage.

3) Going Advanced with Custom Rules: Lastly, I'll teach you how to do something advanced but useful. You can create special checks for your framework using PHPStan. I'll give you examples from big frameworks like Symfony and Doctrine so you can see how it works.

By the end you'll have the knowledge you need to make sure your framework works great, and people love using it.

Ondřej Mirtes

December 06, 2023
Tweet

Transcript

  1. Ondřej Mirtes
    Static Analysis Crash Course
    for Framework Developers

    View full-size slide

  2. $ composer require --dev phpstan/phpstan
    What is PHPStan?
    $ vendor/bin/phpstan analyse src/ tests/

    View full-size slide

  3. What is PHPStan?

    View full-size slide

  4. 1. Use static analysis yourself

    View full-size slide

  5. Rule levels in PHPStan
    0⃣1⃣2⃣3⃣4⃣5⃣6⃣7⃣8⃣9⃣

    View full-size slide

  6. Rule levels in PHPStan
    0⃣ 5⃣

    All basic bugs checked

    View full-size slide

  7. Missing typehints 6⃣
    function foo($a, $b) {
    ...
    }
    Function foo() has parameter $a with no type speci
    fi
    ed.
    Function foo() has parameter $b with no type speci
    fi
    ed.
    Function foo() has no return type speci
    fi
    ed.

    View full-size slide

  8. Partially wrong unions 7⃣
    interface Foo { }
    interface Bar {
    function doBar(): void;
    }
    function (Foo|Bar $fb): void {
    $fb->doBar();
    };
    Call to an unde
    fi
    ned method Foo|Bar::doBar().

    View full-size slide

  9. Nullables 8⃣
    interface Bar {
    function doBar(): void;
    }
    function (?Bar $b): void {
    $b->doBar();
    };
    Cannot call method doBar() on Bar|null.

    View full-size slide

  10. Be stricter about "mixed" 9⃣
    function foo(mixed $m) {
    $m->doBar();
    }
    Cannot call method doBar() on mixed.

    View full-size slide

  11. 2. Ensure good DX for SA of code
    written against your framework

    View full-size slide

  12. Strong types
    public function do(): void
    {
    $a = $this->callFramework();
    }

    View full-size slide

  13. Advanced PHPDoc types
    /** @var int<0, 23> */
    private int $hours;
    /** @var array */
    private array $items;
    /** @param callable(string): int $cb */
    public function do(callable $cb): void
    {
    }

    View full-size slide

  14. Advanced PHPDoc types
    phpstan.org
    Documentation » Writing PHP Code » PHPDoc Basics
    Documentation » Writing PHP Code » PHPDoc Types

    View full-size slide

  15. live.symfony.com/account/replay/video/659

    View full-size slide

  16. __call, __get, __set 🪄🧙
    /**
    * @property int $count
    * @method int sum(int $a, int $b)
    * @mixin Lorem
    */
    class MagicFoo
    {
    }

    View full-size slide

  17. __call, __get, __set 🪄🧙
    interface PropertiesClassReflectionExtension {
    function hasProperty(
    ClassReflection $classReflection,
    string $propertyName
    ): bool;
    function getProperty(
    ClassReflection $classReflection,
    string $propertyName
    ): PropertyReflection;
    }
    If there's "->getName()" then there's "->name"

    View full-size slide

  18. phpstan.org
    Documentation » Developing Extensions » Class Reflection Extensions
    __call, __get, __set 🪄🧙

    View full-size slide

  19. 🚩 Red flags 🚩

    View full-size slide

  20. 🚩 Being forced to "fix" 🚩
    a type
    public function do(): void
    {
    /** @var SpecificFoo $a */
    $a = $this->callFramework();
    }

    View full-size slide

  21. public function do(): void
    {
    $a = $this->callFramework();
    assert($a instanceof SpecificFoo);
    }
    🚩 Being forced to "fix" 🚩
    a type

    View full-size slide

  22. 🚩 Stringly-typed 🚩
    public function do(): void
    {
    $issues = $this->githubClient->api('issue');
    // what is $issues!?
    $issues = $this->githubClient->issues();
    // $issues is IssueApi object
    }

    View full-size slide

  23. 🚩 Optional and nullable 🚩
    parameters
    function setValidity(
    \DateTimeImmutable $from,
    \DateTimeImmutable $to
    ): void
    {
    }

    View full-size slide

  24. 🚩 Optional and nullable 🚩
    parameters
    function setValidity(
    ?\DateTimeImmutable $from,
    ?\DateTimeImmutable $to
    ): void
    {
    }

    View full-size slide

  25. 🚩 Optional and nullable 🚩
    parameters
    $foo->setValidity($from, $to);
    $foo->setValidity(null, null);
    $foo->setValidity($from, null);
    $foo->setValidity(null, $to);

    View full-size slide

  26. 🚩 Optional and nullable 🚩
    parameters
    function setValidity(
    \DateTimeImmutable $from,
    \DateTimeImmutable $to
    ): void
    { }
    function removeValidity(): void
    { }

    View full-size slide

  27. 🚩 Di
    ff
    erent return types 🚩
    based on parameters
    function getParameter(
    ?string $name = null
    ): array|string

    View full-size slide

  28. 🚩 Di
    ff
    erent return types 🚩
    based on parameters
    function getParameter(string $name): string
    /** @return array */
    function getParameters(): array

    View full-size slide

  29. 3. Practical examples
    to achieve strong types

    View full-size slide

  30. /** @return ($name is string ? string : array) */
    function getParameter(
    ?string $name = null
    ): array|string
    {
    ...
    }
    Conditional return type

    View full-size slide

  31. Constraint Validator
    class UserEmailValidator extends ConstraintValidator {
    function validate($value, Constraint $constraint): bool
    {
    if (!$constraint instanceof UserEmail) {
    throw new UnexpectedTypeException(
    $constraint,
    ContainsAlphanumeric::class
    );
    }
    }
    }

    View full-size slide

  32. Constraint Validator
    /** @template TConstraint of Constraint */
    abstract class ConstraintValidator {
    /** @param TConstraint $constraint */
    function validate($value, Constraint $constraint): bool;
    }

    View full-size slide

  33. Constraint Validator
    /** @extends ConstraintValidator */
    class UserEmailValidator extends ConstraintValidator {
    function validate($value, Constraint $constraint): bool
    {
    \PHPStan\dumpType($constraint); // outputs: UserEmail
    }
    }

    View full-size slide

  34. Permission Voter
    class PostVoter extends Voter {
    function supports(string $attribute, $subject): bool
    {
    return $subject instanceof Post;
    }
    function voteOnAttribute(string $attribute, $subject): bool
    {
    /** @var Post $post */
    $post = $subject;
    }
    }

    View full-size slide

  35. Permission Voter
    /** @template T */
    abstract class Voter {
    /** @phpstan-assert-if-true T $subject */
    abstract function supports(string $attribute, $subject): bool;
    /** @param T $subject */
    abstract function voteOnAttribute(
    string $attribute,
    $subject
    ): bool;
    }

    View full-size slide

  36. Permission Voter
    /** @extends Voter */
    class PostVoter extends Voter {
    function supports(string $attribute, $subject): bool
    {
    return $subject instanceof Post;
    }
    function voteOnAttribute(string $attribute, $subject): bool
    {
    \PHPStan\dumpType($subject); // outputs: Post
    }
    }

    View full-size slide

  37. Permission Voter
    \PHPStan\dumpType($subject); // outputs: mixed
    $voter = new PostVoter();
    if ($voter->supports('attr', $subject)) {
    \PHPStan\dumpType($subject); // outputs: Post
    $voter->voteOnAttribute('aaa', $subject); ✅
    } else {
    $voter->voteOnAttribute('aaa', $subject); ❌
    }
    Parameter #2 $subject of method PostVoter::voteOnAttribute()
    expects Post, mixed given.

    View full-size slide

  38. Console commands today
    function configure(): void
    {
    $this->addArgument(
    'paths',
    InputArgument::OPTIONAL | InputArgument::IS_ARRAY
    );
    }
    function execute(InputInterface $input, OutputInterface $output)
    {
    $paths = $input->getPaths(); // is mixed
    ...
    }

    View full-size slide

  39. phpstan / phpstan-symfony
    Console commands today
    function execute(InputInterface $input, OutputInterface $output)
    {
    $paths = $input->getPaths(); // is array
    ...
    }

    View full-size slide

  40. Hypothetical
    strongly-typed future
    class RunCommandDTO {
    #[Argument, Optional]
    public array $paths;
    }
    function execute(RunCommandDTO $input, OutputInterface $output)
    {
    $paths = $input->paths; // is array
    ...
    }

    View full-size slide

  41. 4. Test your framework
    with static analysis

    View full-size slide

  42. use function PHPStan\Testing\assertType;
    $parameters = $bag->getParameter();
    assertType('array', $parameters);
    $foo = $bag->getParameter('foo');
    assertType('string', $foo);
    assertType

    View full-size slide

  43. reactphp / async / tests / types / await.php

    View full-size slide

  44. 5. Write custom
    PHPStan rules

    View full-size slide

  45. Custom PHPStan rules
    #[ORM\Column(type: "datetime"]
    private DateTimeImmutable $created;
    Property User::$created type mapping mismatch: database can
    contain DateTime but property expects DateTimeImmutable.

    View full-size slide

  46. Custom PHPStan rules
    #[Route('/blog/{slug}', name: 'blog_show')]
    public function show(string $title): Response
    Route parameter slug not found in controller action show().
    Parameter $title of action show() not found in the route.

    View full-size slide

  47. Custom PHPStan rules
    phpstan.org
    Documentation » Developing Extensions » Core Concepts
    Documentation » Developing Extensions » Custom Rules

    View full-size slide

  48. github.com/phpstan/phpstan

    View full-size slide