Very common in PHP code bases is obsessive type check, noticeably code like declare(strict_types=1) and private function( int $id ). It's basically "Always check your types".
I think a better rule is:
Check a type when the variable is created, mutated, or received from an external source. Then trust it until something happens that could change it.
The logic is simple. A variable cannot spontaneously change its type. If $id was established as an integer, and nothing has modified $id, checking whether it is still an integer doesn't make the code safer.
There are really three boundaries where checking matters:
In WordPress, hooks are a good example of the third boundary. If your code receives a value from a filter, you don't control what another plugin might have created, that's where the check belongs.
I recently encountered a plugin that required an integer as a function argument. The problem was that the value came from a WordPress hook and wasn't properly checked there.
The strict function signature caught the problem later by throwing a TypeError.
Technically, the type enforcement worked. Architecturally, it failed.
The plugin should have validated the value when it crossed the WordPress hook boundary, where it could decide how to handle an unexpected type gracefully. Instead, invalid data was allowed deeper into the application until PHP produced the fatal error.
On a production site, especially in a plugin running alongside code from many other developers, that's not necessarily the kind of "safety" we want.
Finding an invalid type is not the same as handling an invalid type correctly.
This becomes particularly relevant with OOP and private methods.
Consider:
private function my_method( int $id ){
If my_method() is private, you control every place from which it can be called.
Suppose $id was already validated when it entered the object, and every internal operation preserves that invariant. There is no outside code capable of suddenly calling this method with a string.
In that situation, the runtime type enforcement is checking something your own architecture has already guaranteed.
Instead, you can document the internal contract:
/**
* @param int $id
*/
private function my_method( $id ){
Your IDE or static-analysis tooling can warn you when your own code violates that contract, without requiring PHP to enforce the same invariant again at runtime.
Public methods are different: they are boundaries because outside code can call them. Private methods aren't necessarily boundaries.
An is_int() check or PHP parameter type check is individually extremely cheap. Performance isn't a good argument for obsessively removing a handful of them.
But unnecessary checks accumulate, and their bigger cost is often code complexity.
A defensive check needs a failure path. That means another if, fallback, exception, try/catch, or return value. Then the caller may defensively handle that failure too.
Soon we're writing code to gracefully recover from states that our architecture should have made impossible.
That's boilerplate, additional branches, additional CPU work, and additional code that has to be understood and tested.
Imagine entering an office building where your badge is checked at the entrance.
Now imagine having to show the same badge at every internal door — even though nothing between those doors could possibly have changed your identity.
That's not necessarily better security. It's mostly redundant security.
Types can be treated the same way.
Validate where trust changes.
When data enters your code, check it. When you create it, establish its type. When you mutate it, establish the new invariant.
After that, trust your own code.
In my current project, I needed to fetch data from an API, sanitize it, and return it in a form that is easy to work with. Over the years, I have accumulated snippets and go-to methods for various tasks, but I haven't spent much time refining them. I write code with a focus on performance, readability, and potential enhancements. When I wrote the first version of this code, which was essentially a bunch of if statements, it struck me that for a sequence of conditions, I could use switch(true). I refactored the code, first using the ?? NULL coalescing operator.
Then, I realized that starting with empty() would be better for readability. I should mention that an important principle in my code is "return early," and using empty() at the beginning is ideal.
After trying a few variations, I settled on the following steps:
Here is my refactored function:
function get_ajax_data(){
switch(true){
case !has_valid_nonce():
case empty($_POST['order_id']):
case empty($_POST['ebook_id']):
case empty($_POST['user_id']):
case empty($_POST['action']):
case empty($_POST['format']):
case empty($_POST['order_id']):
case !is_numeric($_POST['order_id']):
case !is_numeric($_POST['ebook_id']):
case !is_numeric($_POST['user_id']):
case !($_POST['format'] === 'epub' || $_POST['format'] === 'mobi'):
return false;
}
return [
'action' => ltrim($_POST['action'],'ebook-insert-client-'),
'format' => $_POST['format'],
'order_id' => $_POST['order_id'],
'ebook_id' => $_POST['ebook_id'],
'user_id' => $_POST['user_id'],
];
}
function has_valid_nonce(){
if( empty($_POST['nonce']) ) return false;
require_once ABSPATH.'wp-includes/pluggable.php';
return wp_verify_nonce( $_POST['nonce'], 'ebook-insert-client-ajax' );
}
The next step was to validate the URL. In PHP, this is pretty straightforward, right? Well, sort of.
case !filter_var( $post['ebook-url'], FILTER_VALIDATE_URL):
Since I’m writing code for WordPress, I could use the wp_http_validate_url() function, but after looking at the source code, I found it to be overly complex and unnecessary for my case. So, I stuck with filter_var().
In this case, I need to receive a URL with a query string with a timestamp. Initially, I used the native PHP function filter_var(), but when I examined what this function considers a valid URL, I realized it was too broad for my use case. So, I moved it into a function and added disqualifying logic:
function is_url_valid($url){
switch( true ){
case $url[0] !== 'h': //to disqualify ftp://
case str_contains($url,'@'): //to early disqualify email
case str_contains($url,'user:'): //to early disqualify login
case str_contains($url,'..'): // 'http://example..com' will pass as a valid URL by var_filter() WTF?!
return false;
default:
return filter_var($url, FILTER_VALIDATE_URL);
}
}
Then I needed to handle parsed URLs. In this scenario, the URL must always have a .epub extension and a numerical value as the query string.
case $parsed_url = parse_url( $post['ebook-url'] ): //early return if URL could not be parsed
case empty($parsed_url['host']):
case empty($parsed_url['query']):
case !is_numeric($parsed_url['query']):
case empty($parsed_url['path']):
case str_contains($parsed_url['path'],'//'): // two slashes "//" will pass as valid URL via filter_var() WTF?!
case !str_ends_with($parsed_url['path'], '.epub'): // in this case only .epub file extension is expected
Yes, I know this might be controversial. Declaring variables in an if statement is generally not considered good practice. However, since this is a switch statement, it looks a bit different and I would argue it's still easy to read and comprehend. If you're using a modern code editor like VS Code, you won't have any issues seeing where the variable comes from, especially given the self-explanatory name.
Without delving into too many details, I found that the slowest part of this process is the WordPress core function wp_verify_nonce(), which takes about 900 microseconds, and I can't do anything about it. Without nonce verification, the rest of the code runs in just 3 microseconds on my local environment. That's pretty efficient, isn't it?
From now on, I plan to use this approach to handle input data, disqualifying malformed or corrupted inputs early and letting only sanitized data pass through. While this article focuses on using switch(true) to quickly filter out bad data, the concept applies to other scenarios too. For instance, when handling inputs like emails or names, I would employ different sanitization techniques to guard against SQL injection and other security risks. The key takeaway is the strategy of catching potential issues early, whether due to incorrect data or malicious attempts.
I'm looking forward to your feedback, are there any weaknesses or areas for improvement, or should I reconsider this approach because there's something better out there? Let me know in the comments below.
PHP 8.4 is coming soon, and it will introduce a few new and useful functions. In WP core, there have historically been so-called pluggable functions, allowing WP plugins and themes to use new functions as long as WP is running on a certain version. Here is my take on the new functions in PHP 8.4:
if(!function_exists('array_find')){
function array_find(array $array, callable $callback){
foreach($array as $value){
if($callback($value)){
return $value;
}
}
return null;
}
}
if(!function_exists('array_find_key')){
function array_find_key(array $array, callable $callback){
foreach($array as $key => $value){
if($callback($value)){
return $key;
}
}
return null;
}
}
if(!function_exists('array_any')){
function array_any(array $array, callable $callback){
foreach($array as $value){
if($callback($value)){
return true;
}
}
return false;
}
}
if(!function_exists('array_all')){
function array_all(array $array, callable $callback){
foreach($array as $value){
if(!$callback($value)){
return false;
}
}
return true;
}
}
To test it, here is some code you can run to see how it behaves.
$array = [1, 3, -7, 9];
// 1. array_find: Find the first negative number in the array
$found = array_find($array, function($value){
return $value < 0;
});
echo "First negative number: " . ($found !== null ? $found : "None found") . "\n";
// Output: First negative number: -7
// 2. array_find_key: Find the key of the first negative number
$found_key = array_find_key($array, function($value){
return $value < 0;
});
echo "Key of first negative number: " . ($found_key !== null ? $found_key : "None found") . "\n";
// Output: Key of first negative number: 2
// 3. array_any: Check if any number in the array is negative
$any_negative = array_any($array, function($value){
return $value < 0;
});
echo "Any negative number: " . ($any_negative ? "Yes" : "No") . "\n";
// Output: Any negative number: Yes
// 4. array_all: Check if all numbers in the array are positive
$all_positive = array_all($array, function($value){
return $value > 0;
});
echo "All positive numbers: " . ($all_positive ? "Yes" : "No") . "\n";
// Output: All positive numbers: No
If this makes it into WP core soon, it will be great for implementing these functions into the plugins and themes codebase.
UPDATE:
I've received feedback from the community that Tailwind can be filtered into a version containing only used classes. Maybe it's not that bad, but I can't see how it would work for me as adds unnecessary steps to my workflow. "Keep it simple" is my philosophy.
Original article:
In the ever-evolving landscape of web development, developers are constantly on the lookout for tools and frameworks that promise to make their lives easier and their websites more efficient. One such tool that has gained significant popularity in recent years is Tailwind CSS. Tailwind is praised for its utility-first approach and the ability to rapidly build custom designs without leaving your HTML. However, as with many hyped-up tools, there are often hidden drawbacks that are not immediately apparent. In this post, we’ll explore one of the major downsides of using Tailwind CSS—its sheer size and the unnecessary bloat it introduces.
Recently, I created a simple website using Tailwind to investigate just how big the Tailwind CSS files are and how many lines of code they contain. To my astonishment, the current version of Tailwind CSS downloaded amounted to 154KB. This may not sound overly alarming at first glance, especially in the age of high-speed internet. However, a deeper dive using Chrome’s coverage console revealed a staggering reality: the CSS file contained about 175000 lines of code or 3MB of uncompressed CSS.
Yes, you read that correctly— 175000 lines of CSS code in a single file. This is by far the largest CSS file I have ever encountered. Such a massive file introduces significant bloat and can severely impact the performance of your website.
The primary issue with such a large CSS file is the unnecessary bloat it creates. Bloat refers to the inclusion of redundant or unnecessary code that does not contribute to the functionality of the website. In the case of Tailwind, the utility-first approach means that the CSS file includes styles for every possible utility class, regardless of whether they are used in your project or not. This results in an enormous CSS file that needs to be downloaded every time a user visits your website.
Every time a user loads a webpage, their browser has to parse and render the CSS file. With a file as large as Tailwind's, this process consumes significantly more computing power and resources. This can lead to slower load times, especially on devices with limited processing capabilities, such as mobile phones or older computers. In a world where users expect websites to load almost instantaneously, even a slight delay can result in a poor user experience and potential loss of visitors.
Website speed is a crucial factor in user retention and search engine rankings. A slow-loading website can frustrate users and lead to higher bounce rates. Additionally, search engines like Google consider page speed as a ranking factor. Therefore, using a CSS framework that introduces unnecessary bloat can directly impact your website’s SEO performance and overall user satisfaction.
While it's true that there is a possibility of filtering Tailwind's original CSS to remove unused styles, Tailwind itself doesn't offer an official tool for this purpose. Instead, developers have to rely on third-party tools like PurgeCSS to achieve this. However, using these third-party tools can be a clumsy and often cumbersome process. They require additional configuration and maintenance, which can introduce complexity and potential points of failure into your build process. Moreover, these tools might not always be perfectly accurate, sometimes removing necessary styles or leaving behind unused ones. This adds an extra layer of troubleshooting and can detract from the time-saving benefits that Tailwind initially promises. Thus, while filtering is a potential workaround, it is not a perfect solution and highlights another drawback of adopting Tailwind CSS without careful consideration.
When I viewed websites built with Tailwind, they all seemed to be using much less CSS code, yet were still very bloated. It appears there are tools for JavaScript frameworks that can filter Tailwind, but it's not clear how WordPress developers can access these tools or how easy they are to use. This lack of clarity and straightforward integration for WordPress users further complicates the adoption of Tailwind CSS in traditional CMS environments. The additional steps and potential for errors can be discouraging, especially for developers seeking a streamlined workflow.
The allure of new and shiny libraries promoted by YouTube influencers and other online sources can be tempting. However, as developers, it is our responsibility to thoroughly test and evaluate these tools before integrating them into our projects. While Tailwind CSS offers some benefits, it’s essential to weigh these against the potential drawbacks.
Before adopting any new library or framework, take the time to conduct a thorough analysis. Test the impact on your website’s performance, check for unnecessary bloat, and consider the long-term implications. Sometimes, the initial convenience offered by a tool can lead to significant challenges down the line.
In conclusion, while Tailwind CSS might seem like an appealing option for rapid development, its substantial size and the unnecessary bloat it introduces make it a questionable choice for performance-conscious developers. To speed up your WordPress or any other web project, always test new tools and libraries rigorously. The best choice is often not the one that is most hyped, but the one that aligns best with your performance goals and project requirements.
Stay informed, test diligently, and make choices that ensure your websites are fast, efficient, and user-friendly. Happy developing!