Some powerful features have been added to PHP, especially since version 7.0 to foster secure and safe code. Type declarations and strict typing are among the well-adopted features often considered best practices for modern PHP code. However, this does not always play well with WordPress development where customizable code is encouraged with functions like apply_filters. Applying those PHP best practices in such an open environment can be challenging and raise several issues. Let’s dive into the advantages of stronger typing, the issues you could be facing as a WordPress developer, and how you can overcome them!

Safer code with strict typing

The trap of weak typing

PHP is a programming language that uses weak typing: during runtime, it will try and convert data to the expected type when it seems reasonable. In the following basic example, the string ‘7’ will automatically get converted to an int to execute the code properly:

<?php
print_r(1 + '7'); // => 8

This basic example seems reasonable and shows how weak typing can speed up development as you don’t have to handle type conversion yourself. But this means there is hidden logic executed: type conversion operations happen under the hood and they might not behave as you expect in some edge cases, which can ultimately generate hard-to-identify bugs. Let’s see a couple of edgy behaviors of PHP implicit type conversion:

<?php

function add(int $a, int $b) {
    return $a + $b;
}

$result1 = add( 2, true );
$result2 = add( 2.5, 3 );

echo $result1 . "\n"; // Outputs: 3
echo $result2 . "\n"; // Outputs: 5

true is converted to 1: this prevents the code from throwing an error and allows it to move forward. But maybe you should not have true at all at this stage, and the hidden type conversion will prevent you from spotting the issue early on.

2.5 is converted to 2: here, we are losing data. This can be problematic depending on your application! While you might be aware of this limitation of a piece of code you wrote, someone else might be reusing it as a black box and get unexpected results because of this, without getting any warnings!

The strict_type directive

Since PHP 7.0, the new directive strict_type is available. Setting it forces PHP to check at runtime that the type of function and method arguments, as well as return values, match the specified types in the function declaration. If they don’t, instead of trying to convert them under the hood, PHP will simply throw a Fatal Error. The example below demonstrates this. While this can sound extreme at first, it is usually a better trade-off to throw a Fatal Error and hence quickly identify issues rather than keeping a possible weakness hidden in your code for a long time.

To use the strict_type directive, declare it at the beginning of the file, as in the following example. Note that the directive only applies to the file it is declared in, so you would need to add this directive in all your files!

<?php
declare(strict_types=1);

function add(int $a, int $b) {
    return $a + $b;
}

$result1 = add( 2, true );
$result2 = add( 2.5, 3 );

echo $result1 . "\n"; // Outputs: 3
echo $result2 . "\n"; // Outputs: 5

apply_filters and strict typing: customization vs. quality

Introducing customization exposes your code to bugs

The following example is from a real-life bug I faced in a WordPress context:

<?php

$default_delay_between = 1;
/**
 * Filter the delay between each preload request.
 *
 * @param int $delay_between the defined delay.
 */
$delay_between = apply_filters( 'wpm_delay_between_requests', $default_delay_between );

// Loop through all requests to send
foreach ( $tasks as $task ) {
	send_the_request( $task );
	sleep( $delay_between );
}

This code handles a batch of requests that a plugin must do to an external service. Enforcing a delay between those requests can be beneficial for many reasons: adapting to rate limits on the external API, adapting to the capacities and performance of the WordPress host, etc.

This is a great use case for WordPress customization through hooks: Each plugin user might have different constraints for host performance or authorized API rates, and therefore would need to adjust the delay accordingly. A magic value would not be suitable for everyone. Therefore, plugin developers can offer a filter to modify the delay duration.

This flexibility exposes your code to bugs due to misuse of the filter: sleep( $delay_between ) expects an int. Several issues can arise. Among them, let’s take two examples:

  • If a user filters the value and returns a string, this code will throw a warning and will not wait between requests.
  • If a user filters the value and returns a float, this code will throw a warning and will truncate the float to an integer.

The first issue here is that PHP will throw a warning referencing your code and files: users’s first reaction can be to reach out to you, or your support team with the logs while the root cause is not coming from you. This will be frustrating for everyone.

A second issue is that the code might not behave as expected. If a user can do more than one request per second to the API and its hosting performances support it, they might be tempted to filter the $delay_between value and return 0.5. This float would get truncated to 0, resulting in no delay between requests at all, bursting as fast as possible and hence exhausting API rates and hosting performances! Now, this can be considered a major defect!

Strict typing to bring back safety

Strict typing can help you solve the second issue. By simply adding the directive, the code would now throw a Fatal Error if a user returns something else than an int to the wpm_delay_between_requests filter.

<?php
declare(strict_types=1);

$default_delay_between = 1;
/**
 * Filter the delay between each preload request.
 *
 * @param int $delay_between the defined delay.
 */
$delay_between = apply_filters( 'wpm_delay_between_requests', $default_delay_between );

// Loop through all requests to send
foreach ( $tasks as $task ) {
	send_the_request( $task );
	sleep( $delay_between );
}

This will allow developers and users to immediately identify the issue, preventing side effects as the code will stop running. This greatly facilitates investigation and debugging as the Fatal Error is easy to catch and one should immediately see it after introducing the faulty callback.

However, we still have an issue: the Fatal Error will reference your code and not the faulty callback. Hence, after seeing the Fatal Error, chances are still that the user would reach out to you or your support: this is still not ideal.

Typed apply_filters for resiliency

PHP will always see the issue “too late” when calling sleep. We would need to catch it earlier to identify that the root cause is an external callback. So let’s take the matter into our own hands: we can use defensive programming by checking the type ourselves, and if it is not as expected, raise a log and fall back to the default value:

<?php
declare(strict_types=1);

$default_delay_between = 1;
/**
 * Filter the delay between each preload request.
 *
 * @param int $delay_between the defined delay.
 */
$delay_between = apply_filters( 'wpm_delay_between_requests', $default_delay_between );

if ( !is_int( $delay_between ) ) {
  
  error_log('Misuse of filter wpm_delay_between_requests: expected an int but got ' . gettype($delay_between) . " $delay_between");
  $delay_between = $default_delay_between;
}

// Loop through all requests to send
foreach ( $tasks as $task ) {
	send_the_request( $task );
	sleep( $delay_between );
}

This approach solves both issues mentioned above:

  • No bugs related to wrong typing for sleep can be introduced through the filter.
  • The user will get an error log explaining why its callback is not being applied, rather than an error that would point to your code.

This approach of “typed apply_filters” is therefore a suitable trade-off to get the best of customization, resiliency, and modern code practices. It allows us to benefit from strict_types in your WordPress development without introducing possible issues to your codebase.

Typed apply_filters off-the-shelf

apply-filters-typed by WP Media

Read more about WP Media best practices for WordPress filters in our public Engineering Handbook!

Making apply_filters typed as in the previous example can be tedious: for each call to apply_filters, several lines must be added which increases code complexity and risk of introducing bugs, while reducing readability. Ultimately, you will lose time writing almost boilerplate code.

To solve this issue, WP Media, the company behind WP Rocket and where I currently work introduced the apply-filters-typed library.

This small library offers 2 wrappers for apply_filters that automatically take care of type-checking, either based on the default parameter’s type (wpm_apply_filters_typesafe) or based on a type specified as argument (wpm_apply_filters_typed). In case the filtered value does not match the expected type:

  • the default value is returned, keeping the code execution safe even with the strict_types declaration ;
  • throws an error message in error.log if WP_DEBUG is set so that developers can identify the issue and why their customization might not be taken into account.

Using those wrappers in place of apply_filters offers the best of both worlds: customization and safe code, without the burden of maintaining a more complex codebase!

For advanced needs, the library even supports custom “type” checks, if you need to validate more than just the type of the filtered value. For instance, one could validate the type of the elements within a filtered array, or if an integer is strictly positive, etc. For more information, refer to the library’s library documentation.

Introducing typed apply_filters to WordPress Core

One of the key features of WordPress is how customizable it is, and filters play a great role in this. As it is explained in this article, maintaining this level of customization can be challenging for WordPress developers. Moreover, the WordPress community is continuously trying to keep WordPress up-to-date with the standards and modern code practices, such as strict typing in PHP. At WP Media, we strongly believe that typed apply_filters are a mandatory step toward a safer framework, and to higher quality across the WordPress ecosystem.


As this article is being written, we are supporting the adoption of the apply-filters-typed library with WordPress Core, so that it natively offers typed filters for developers. Here is a trac ticket opened for a long time, and we hope this initiative can offer a suitable solution to this long standing friction.

Here is a last example just before releasing this article that perfectly illustrates why the WordPress ecosystem needs to push for strict types! A user of our plugins reported the following error log:

[14-Aug-2024 17:34:32 UTC] PHP Fatal error: Uncaught TypeError: Argument 1 passed to WP_Rocket\Engine\Common\PerformanceHints\Admin\Controller::delete_by_url() must be of the type string, null given

And here is where it occurred:

<?php
declare(strict_types=1);
...
	public function delete_post( $post_id ) {
		if ( empty( $this->factories ) ) {
			return;
		}

		$url = get_permalink( $post_id );

		if ( false === $url ) {
			return;
		}

		$this->delete_by_url( $url );
	}

So, $url is null. How did we miss that? Our automated checks like PHPStan should have warned us! Let’s look at the get_permalink definition:

get_permalinkint|WP_Post $post, bool $leavename = false ): string|false

Oh, so $url should be either false, or a string, but never null according to WordPress’s official documentation? How come? Well, looking at get_permalink‘s code, the returned value is filtered. So, without using a typed filter, as we suggest in this article, the output get_permalink can be anything, despite the function specifications.


Now, that is a weird case to handle: should our code be paranoiac and safeguard against everything? That is a loss of performance with useless code and a loss of readability of the code, so a loss of added value down the road for our customers and WordPress users. Should we say to this user that this is not our fault but someone else is doing things wrong? Well, the error is thrown in our code after all, and deactivating our plugin would also fix the issue…

I believe this example showcases why this is a critical topic for WordPress Core to tackle to foster quality in the ecosystem, and avoid discouraging developers who want to do stay up to date with modern best practices. Let’s avoid evolving backward.

A meme showing a factory worker spotting a wrong input type and throwing error. Then, the same scene, but the worker gave up.

Leave a Reply

Your email address will not be published. Required fields are marked *