PHPDoc, typed properties, and the case for strict WordPress plugin development.

WordPress doesn't enforce strict typing, but that doesn't mean you shouldn't. Here's the workflow we use to maintain type-safe, documented plugin codebases.

7 min read Monday Digital Lab
0 type-related bugs shipped to production since adopting strict_types in all Lab plugins

The WordPress Typing Gap

WordPress core is written for PHP 5.6 compatibility and makes heavy use of dynamic typing, string-keyed arrays, and mixed-type parameters. This was a reasonable choice for a CMS built to run on the widest possible range of shared hosting environments. It is not a reasonable choice for a production plugin in 2026.

The consequence is that WordPress sets a cultural precedent of loose typing that propagates through the plugin ecosystem. Most third-party plugins pass untyped data between functions, rely on WordPress's own coercive behaviour, and have no mechanism for catching type mismatches before they become production bugs.

declare(strict_types=1)

Every PHP file in a well-typed codebase begins with declare(strict_types=1). This single declaration changes PHP's type coercion behaviour for that file: instead of silently converting "42" to 42 when an integer is expected, PHP throws a TypeError. You catch the error during development rather than in a production log.

The constraint applies only to function calls made from within that file. Code in other files that does not declare strict_types can still call your strictly-typed functions and PHP will coerce at the boundary. The declaration does not enforce a global strict mode — it enforces strict mode for the file that declares it.

PHP — strict_types declaration and typed function signature
<?php

declare(strict_types=1);

/**
 * Resolve the visitor's country code from a raw IP address string.
 *
 * @param  string $ip  Dotted-decimal IPv4 or colon-separated IPv6 address.
 * @return string      ISO 3166-1 alpha-2 country code, or empty string on failure.
 */
function resolve_country_code( string $ip ): string {
    if ( filter_var( $ip, FILTER_VALIDATE_IP ) === false ) {
        return '';
    }

    $record = mdjhd_lookup_ip( $ip );

    return $record->country->isoCode ?? '';
}

Typed Properties

PHP 7.4 introduced typed class properties. This means the type constraint is enforced at assignment time, not just at method call boundaries. An uninitialized typed property throws an Error if accessed before assignment rather than silently returning null.

For plugin classes that store configuration, request data, or database results, typed properties make the expected shape of the object visible to static analysis tools without any documentation comments.

PHP — typed properties in a plugin settings class
<?php

declare(strict_types=1);

final class Plugin_Settings {
    public string  $api_key;
    public int     $cache_ttl;
    public bool    $debug_mode;
    public ?string $fallback_country;

    public function __construct(
        string  $api_key,
        int     $cache_ttl     = HOUR_IN_SECONDS,
        bool    $debug_mode    = false,
        ?string $fallback_country = null
    ) {
        $this->api_key          = $api_key;
        $this->cache_ttl        = $cache_ttl;
        $this->debug_mode       = $debug_mode;
        $this->fallback_country = $fallback_country;
    }
}

PHPDoc as a Contract

PHPDoc comments serve two audiences: the developer reading the code, and the static analysis tool processing it. In a strictly-typed codebase, PHPDoc fills the gaps that PHP's type system cannot express natively — array shapes, union types in older PHP versions, generic collections, and the meaning of string parameters that accept a fixed set of values.

We write PHPDoc for every public function signature, using @param and @return with the most specific types available. For array parameters, we use @param array{key: type} syntax where the shape is known. This is not documentation theatre — PHPStan and Psalm both parse and enforce these annotations during CI.

PHPStan integration

Run PHPStan at level 6 or above. Levels below 6 allow too many implicit mixed types to pass unchecked. Most WordPress-specific PHPStan extensions bring WordPress API functions into the analysis graph so you get type information for add_action, get_option, and other core functions.

The Toolchain

Static analysis is only useful if it runs automatically. Our plugin development workflow includes the following at every git push:

  • PHPStan at level 7 with the szepeviktor/phpstan-wordpress extension — catches type mismatches, undefined variables, dead code branches, and return type violations.
  • PHP_CodeSniffer with WordPress Coding Standards — enforces documentation comment presence, naming conventions, and WordPress-specific patterns like nonce verification.
  • Rector — automated refactoring for PHP version upgrades and deprecation fixes. We run it against a target PHP version of 8.1 minimum.
  • Pest PHP for tests — strictly typed test cases catch assertion mismatches that PHPUnit's looser API would silently pass.

When It Matters Most

Strict typing matters most at three specific points in a plugin's lifecycle: when onboarding a new developer, when updating a WordPress core dependency that changes return types, and when debugging a production edge case where the input data is unexpected.

The absence of strict types does not produce bugs in every codebase every week. But when a type-related bug does appear in an untyped codebase, it is routinely the hardest category of WordPress bug to trace because PHP's coercion behaviour hides the origin point. With strict_types, the same bug throws a TypeError at the precise line where the wrong type was passed. The debugging time difference is not marginal.

Ready to Start?

Your next production-ready tool starts here.

Lightweight. Privacy-First. Built from real production experience. Pick a plugin and ship it today.

Back to top