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

Writing Strongly Typed PHP: Let Types Do the Te...

Writing Strongly Typed PHP: Let Types Do the Testing

PHP has come a long way. Readonly properties, enums, property hooks, and a mature static analysis ecosystem now let us say much more about our code in types than before. A machine can check all of it for free, before a single test runs.

In this talk, I'll show how leaning on the type system can quietly remove whole groups of bugs — bugs developers still write unit tests for today. We'll look at small, everyday habits that make code easier to read, both for humans and for tools like PHPStan. Then we'll step back and ask a bigger question. Which parts of your application really need tests? And which parts can a static analyser guard for you instead?

You'll leave with a fresh way to think about where bugs hide, and a simple plan to write fewer tests while shipping more reliable PHP.

Avatar for Ondřej Mirtes

Ondřej Mirtes

June 02, 2026

More Decks by Ondřej Mirtes

Other Decks in Programming

Transcript

  1. function foo(string $id, string $name, string $email, string $directory, string

    $filename, string $address, string $price, string $date, ) { } Stringly-typed code
  2. public function foo( Email $email, SplFileInfo $directory, Address $address, Money

    $price, DateTimeImmutable $date, ): FooBar { } Objects for everything
  3. “I do one thing well” function foo(array $values) { …

    } function bar(string $value) { … }
  4. empty() • "" • 0 • 0.0 • "0" •

    NULL • FALSE • an empty array()
  5. phpstan/phpstan-strict-rules • No empty, no == • Only booleans in

    conditions: if, elseif, !, &&, || • $strict=true for: in_array(), array_search()... • Sames types in switch(X) and case X
  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. Missing typehints 6⃣
  7. Array shapes /** * @param array{foo: int, bar: string} $a

    */ function foo(array $a) { \PHPStan\dumpType($a['foo']); // int \PHPStan\dumpType($a['bar']); // string }
  8. phpstan.org Documentation » Writing PHP Code » PHPDoc Types Documentation

    » Writing PHP Code » PHPDocs Basics ...and more, much more
  9. Generics interface Query {} class FindCustomerById implements Query { ...

    } class QueryBus { public function dispatch(Query $query) { ... } } $customer = $bus->dispatch(new FindCustomerById($id)); // what is $customer?
  10. Generics class Customer { ... } /** @implements Query<Customer> */

    class FindCustomerById implements Query { ... }
  11. Generics class QueryBus { /** * @template T * @param

    Query<T> $query * @return T */ public function dispatch(Query $query) { ... } }
  12. Generics documentation phpstan.org Blog » Guides » Generics in PHP

    using PHPDocs Blog » Guides » Generics By Examples Blog » Guides » What’s Up With @template-covariant? Blog » Guides » A guide to call-site generic variance
  13. Wiring • Controllers • Facades • Passing values somewhere else

    • Getters • Setters • Assigning to properties
  14. Business logic • Value validation • Arithmetic operations, rounding •

    Filtering • Parsers • State machines • if conditions, loops...
  15. What is a unit test? • Executed PHP code directly

    and only in memory • Does not connect to the database • Does not access the filesystem • Does not make network calls • Does not need a DI container
  16. function doSomething(int $id): void { $foo = $this->someRepository ->getById($id); //

    business logic with $foo... $bars = $this->otherRepository ->getByFoo($foo); // business logic with $foo & $bars }
  17. #[DataProvider('dataCalculate')] function testCalculate( Foo $foo, int $expectedResult, ): void {

    $result = $this->calculator ->calculate($foo); $this->assertSame($expectedResult, $result); }
  18. $transaction = $this->em->beginTransaction(); try { $article = $this->em->find(Article::class, $id); $article->setTitle($title);

    $article->setAuthor($author); $article->setPublishDate($publishDate); $article->setText($text); // calculate author's fee // interesting logic here // … $this->em->flush(); $transaction->commit(); catch (\Throwable $e) { $transaction->rollback(); throw $e; } boring – statically verified boring – statically verified interesting - will test this! Approach to testing
  19. $transaction = $this->em->beginTransaction(); try { $article = $this->em->find(Article::class, $id); $article->setTitle($title);

    $article->setAuthor($author); $article->setPublishDate($publishDate); $article->setText($text); $this->authorFeeCalculator->calculateFee($text); $this->em->flush(); $transaction->commit(); catch (\Throwable $e) { $transaction->rollback(); throw $e; } AuthorFeeCalculator AuthorFeeCalculatorTest Approach to testing