levelmax World 2 Honest Types
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. B
L
Level 2.B ~25 min Target: PHPStan level 8

Boss: typed repository

A repository is where database rows become domain objects, which makes it the boundary this whole world has been about. Get it right and nothing above it has to wonder whether a price is a string, whether a list has gaps, or whether a product might secretly be null.

This one gets it wrong in every way World 2 covers. Every column arrives as a string, and an @var tag pretends otherwise. A list of mugs comes back keyed by ID. get() promises a Product and can return null. page() returns a Page of who knows what, for page number zero if you ask.

Catalogue (read only) is written against the honest version, and PHPStan reports where the two disagree.

Your checklist

Level Ask of ProductRepository
2.1 array is a shrug Does every array say what's in it, and is a list really a list?
2.2 Refined scalar types Do page numbers and prices say which ints they allow?
2.3 Array shapes Is every raw row converted and checked at the boundary, rather than trusted?
2.4 Type aliases If you find yourself writing the same shape twice, does it have one name?
2.5 Generics Does page() say what its Page holds?
2.6 Narrowing and assertions Is every null dealt with, and every @var claim replaced with a check?

Start with Analyse, then work down the checklist. The tests cover the runtime behaviour, including rows that aren't products.

CHALLENGE

ProductRepository hands out whatever the database sends. Prices arrive as strings, a category list is keyed by ID, get() can return null, and hydrate() uses an @var tag to claim the row is already typed.

Bring src/ProductRepository.php up to World 2 standards, so the tests pass and PHPStan is clean at level 8:

  • find() returns a Product or null, and get() throws ProductNotFound instead of returning null
  • inCategory() returns a list, in the order the database gives it
  • page() returns a Page<Product>, and only accepts page numbers and sizes that make sense
  • every row is converted and checked with Row, and a row that isn't a product throws an UnexpectedValueException
Running on PHP 8.3…

Run the tests, run PHPStan, or submit for both. Ctrl/⌘ + Enter submits from the editor.