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

Static Analysis Crash Course for Framework Deve...

Ondřej Mirtes
December 06, 2023
160

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. 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.
  2. 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().
  3. Nullables 8⃣ interface Bar { function doBar(): void; } function

    (?Bar $b): void { $b->doBar(); }; Cannot call method doBar() on Bar|null.
  4. Advanced PHPDoc types /** @var int<0, 23> */ private int

    $hours; /** @var array<string, Foo> */ private array $items; /** @param callable(string): int $cb */ public function do(callable $cb): void { }
  5. Advanced PHPDoc types phpstan.org Documentation » Writing PHP Code »

    PHPDoc Basics Documentation » Writing PHP Code » PHPDoc Types
  6. __call, __get, __set 🪄🧙 /** * @property int $count *

    @method int sum(int $a, int $b) * @mixin Lorem */ class MagicFoo { }
  7. __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"
  8. 🚩 Being forced to "fix" 🚩 a type public function

    do(): void { /** @var SpecificFoo $a */ $a = $this->callFramework(); }
  9. public function do(): void { $a = $this->callFramework(); assert($a instanceof

    SpecificFoo); } 🚩 Being forced to "fix" 🚩 a type
  10. 🚩 Stringly-typed 🚩 public function do(): void { $issues =

    $this->githubClient->api('issue'); // what is $issues!? $issues = $this->githubClient->issues(); // $issues is IssueApi object }
  11. 🚩 Optional and nullable 🚩 parameters function setValidity( \DateTimeImmutable $from,

    \DateTimeImmutable $to ): void { } function removeValidity(): void { }
  12. 🚩 Di ff erent return types 🚩 based on parameters

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

    function getParameter(string $name): string /** @return array<string> */ function getParameters(): array
  14. /** @return ($name is string ? string : array<string>) */

    function getParameter( ?string $name = null ): array|string { ... } Conditional return type
  15. Constraint Validator class UserEmailValidator extends ConstraintValidator { function validate($value, Constraint

    $constraint): bool { if (!$constraint instanceof UserEmail) { throw new UnexpectedTypeException( $constraint, ContainsAlphanumeric::class ); } } }
  16. Constraint Validator /** @template TConstraint of Constraint */ abstract class

    ConstraintValidator { /** @param TConstraint $constraint */ function validate($value, Constraint $constraint): bool; }
  17. Constraint Validator /** @extends ConstraintValidator<UserEmail> */ class UserEmailValidator extends ConstraintValidator

    { function validate($value, Constraint $constraint): bool { \PHPStan\dumpType($constraint); // outputs: UserEmail } }
  18. 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; } }
  19. 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; }
  20. Permission Voter /** @extends Voter<Post> */ 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 } }
  21. 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.
  22. 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 ... }
  23. Hypothetical strongly-typed future class RunCommandDTO { #[Argument, Optional] public array

    $paths; } function execute(RunCommandDTO $input, OutputInterface $output) { $paths = $input->paths; // is array<string> ... }
  24. Custom PHPStan rules #[ORM\Column(type: "datetime"] private DateTimeImmutable $created; Property User::$created

    type mapping mismatch: database can contain DateTime but property expects DateTimeImmutable.
  25. 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.
  26. Custom PHPStan rules phpstan.org Documentation » Developing Extensions » Core

    Concepts Documentation » Developing Extensions » Custom Rules