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
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
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.
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.