Series 8 — Part 3 of 7

PHP 8 strict types catch type mismatches at call time, not at assignment time. In a webhook handler, this means a TypeError can crash a request after all the meaningful work is done — leaving the user without a response and a 500 in the logs. This article covers how it happens and how to prevent it.

The Single Missing Argument

declare(strict_types=1);

// A function deep in the call stack
function store_message(int $conversationId, string $role, string $content, int $userId): void
{
    // ...
}

// Webhook handler calls a service...
// Service calls a helper...
// Helper calls store_message() with:
store_message($convId, 'assistant', $responseText);  // Missing $userId!
// PHP 8: Fatal error: Too few arguments to function store_message()
// This crashes the entire request AFTER the LLM has already generated the response

Without strict_types=1, PHP would accept and silently handle type coercions. With it, any type mismatch is fatal. This is the correct behavior — but it requires catching mismatches before production.

Static Analysis: Finding Type Mismatches Before Deployment

# Install PHPStan (level 8 = most strict)
composer require --dev phpstan/phpstan

# phpstan.neon
parameters:
  level: 8
  paths:
    - src/

# Run before every deployment
./vendor/bin/phpstan analyse
# PHPStan finds: Parameter #4 $userId of function store_message() expects int, missing.

# PSalm is an alternative with different strengths
composer require --dev vimeo/psalm
./vendor/bin/psalm

PHP 8.x Union Types and Nullable Parameters

declare(strict_types=1);

// PHP 8 union types — accept int or string
function process(int|string $id): string
{
    return (string) $id;
}

// Nullable shorthand — ?int means int|null
function find_user(?int $id): ?array
{
    if ($id === null) return null;
    // ...
}

// Named arguments help catch missing args at call site (PHP 8.0+)
store_message(
    conversationId: $convId,
    role:           'assistant',
    content:        $responseText,
    userId:         $userId,  // Named: compiler catches this omission
);

What to Watch For

  • Webhook context — arguments from payload parsing — Payload fields from json_decode() are mixed types. Always validate and cast before passing to typed functions: (int) ($payload['id'] ?? 0).
  • Legacy code without declare(strict_types=1) — Mixing strict and non-strict files in one project is dangerous. The strictness applies per-file. If a non-strict file calls a strict file's function, coercions may still occur.
  • PHPStan level progression — Start at level 5 on existing codebases. Level 8 on a large legacy codebase will produce hundreds of errors. Progress level by level.