October 23, 2026 5 min read PHP

Strict Typing in Modern PHP: Generics in DocBlocks, Union Types, and Static Analysis with PHPStan

Introduction to Modern PHP Type Safety

PHP has evolved dramatically from its loose, dynamic origins into a robust, object-oriented, enterprise-ready language. Modern PHP development at Tech Aditya relies heavily on strict typing, scalar declarations, and rigorous static analysis to catch bugs long before code reaches production environments.

While dynamic typing offers rapid prototyping, it becomes a liability in large-scale codebases. By leveraging modern language features like union types, intersection types, custom DocBlock generics, and PHPStan, engineering teams can build bulletproof applications that are easy to refactor, maintain, and scale.

Enforcing Strict Types and Native Type Declarations

The foundation of strict typing in PHP begins with the declare(strict_types=1); directive. Without this declaration, PHP silently coerces types (e.g., passing a string "123" into an integer-typed parameter). Placing this at the top of every file guarantees runtime type integrity.

PHP 8.0 and 8.1 introduced powerful native type declarations:

  • Union Types: Allow a value to accept multiple distinct types (e.g., int|string).
  • Intersection Types: Require an object to implement multiple interfaces simultaneously (e.g., Countable&Iterator).
  • Mixed Type: Represents any possible value (equivalent to object|resource|array|string|int|float|bool|null).
  • Never Return Type: Indicates that a function never returns control, typically because it throws an exception or exits (never).

Here is an example demonstrating modern native type declarations in an enterprise service class:

Snippet
<?php

declare(strict_types=1);

namespace App\Services;

use Countable;
use Iterator;
use InvalidArgumentException;

class DataProcessor
{
    private int|float $threshold;

    public function __construct(int|float $threshold)
    {
        $this->threshold = $threshold;
    }

    public function processPayload(array|string $payload): int|never
    {
        if (is_string($payload)) {
            $payload = json_decode($payload, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new InvalidArgumentException('Invalid JSON payload provided.');
            }
        }

        $count = count($payload);

        if ($count >= $this->threshold) {
            return $count;
        }

        throw new InvalidArgumentException('Payload size is below the required threshold.');
    }
}

Advanced Generics via DocBlocks in PHP

Because PHP does not yet have native runtime generics, working with collections, repositories, and custom data structures often results in IDEs and static analyzers losing track of exact inner types. We bridge this gap using standardized PHPDoc annotations recognized by PHPStan and Psalm.

Pro-Tip: Always use template annotations like @template, @extends, and @param to give static analyzers complete visibility into collection contents, drastically improving autocomplete and catching type mismatches early.

Below is an implementation of a strongly-typed generic repository pattern using DocBlock annotations:

Snippet
<?php

declare(strict_types=1);

namespace App\Repositories;

/**
 * @template T of object
 */
interface RepositoryInterface
{
    /**
     * @param int $id
     * @return T|null
     */
    public function find(int $id): ?object;

    /**
     * @return array<int, T>
     */
    public function all(): array;
}

/**
 * @template T of object
 * @implements RepositoryInterface<T>
 */
abstract class BaseRepository implements RepositoryInterface
{
    /** @var array<int, T> */
    protected array $storage = [];

    public function find(int $id): ?object
    {
        return $this->storage[$id] ?? null;
    }

    public function all(): array
    {
        return $this->storage;
    }
}

Static Analysis Configuration with PHPStan

Writing type declarations is only half the battle. Enforcing them across large codebases requires a robust static analysis pipeline. PHPStan analyzes your code without executing it, catching subtle type bugs, null dereferences, and dead code.

To get started, install PHPStan via Composer as a development dependency:

Snippet
composer require --dev phpstan/phpstan

Next, create a configuration file named phpstan.neon in your project root. For robust code quality, target Level 9—the highest strictness level available.

Snippet

parameters:
    level: 9
    paths:
        - src
        - tests
    treatPhpDocTypesAsCertain: false
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: false

Run PHPStan from your terminal using the following command:

Snippet
vendor/bin/phpstan analyse --memory-limit=1G

Comparing Type Safety Features Across PHP Versions

Understanding what each PHP version brings to the table helps engineering teams architect forward-compatible applications. The table below outlines major type-system milestones:

PHP Version Key Type Feature Developer Impact
PHP 7.0 Scalar Type Declarations Enforced int, float, string, and bool on function arguments and returns.
PHP 7.4 Typed Properties Allowed class properties to be explicitly typed, reducing constructor boilerplates.
PHP 8.0 Union Types & Mixed Enabled specifying multiple allowed types natively without DocBlocks.
PHP 8.1 Intersection Types & never Allowed complex object contracts combining multiple interfaces.
PHP 8.2 & 8.3 Readonly Classes & Typed Constants Enhanced immutability and constant definitions across object hierarchies.

Common Static Analysis Pitfalls and How to Fix Them

When upgrading a project to strict types and running PHPStan at Level 9, developers frequently encounter specific errors. Addressing these correctly ensures clean, maintainable code.

1. The dreaded "Method return type has no value type specified in iterable"

When returning arrays from methods, PHPStan expects explicit array shape or iterable type definitions in your DocBlocks.

Snippet
// Incorrect
/** @return array */

// Correct
/** @return array<string, int> */

2. Handling Mixed Types Safely

When dealing with legacy data or external APIs returning mixed types, you must narrow down the type using conditional type guards before performing operations.

Snippet
public function handle(mixed $input): string
{
    if (!is_string($input)) {
        throw new \InvalidArgumentException('Expected string input.');
    }
    return strtoupper($input); // PHPStan now knows $input is strictly a string
}

Conclusion and Next Steps

Implementing strict typing, advanced DocBlock generics, and PHPStan static analysis elevates PHP from a scripting language to an enterprise-grade powerhouse. By eliminating entire classes of runtime errors before deployment, your engineering team can deliver exceptionally reliable software.

For custom web engineering, high-performance architecture, or enterprise PHP development, feel free to explore our Web Engineering Services or Contact Tech Aditya to discuss your upcoming project with our expert architects.

Frequently Asked Questions

Quick, authoritative answers to common questions on this topic

Q1 Does PHP support native generics?
As of PHP 8.4, PHP does not have native runtime generics. However, static analyzers like PHPStan and Psalm fully support generics via standardized PHPDoc annotations (DocBlocks), giving developers complete type safety during static analysis without runtime overhead.
Q2 What is the difference between union types and intersection types in PHP?
Union types (introduced in PHP 8.0) allow a value to be one of several types (e.g., int|string). Intersection types (introduced in PHP 8.1) require a value to satisfy multiple type constraints simultaneously (e.g., Countable&Iterator).
Q3 How do I enforce strict type checking globally in a PHP file?
You can enforce strict type checking by adding 'declare(strict_types=1);' as the very first statement in your PHP files. This ensures PHP throws a TypeError if a function argument or return value does not precisely match the declared type.
Q4 What is PHPStan level 9 and why should I use it?
PHPStan level 9 is the highest strictness level available out-of-the-box. It performs rigorous checks on mixed types, dead code, impossible conditions, and ensures every single method argument and return value is completely type-safe.
Aditya Kumar

Aditya Kumar

Full-Stack Engineer

Chat on WhatsApp