Skip to content

The Quirks And Gotchas of PHP

The Quirks And Gotchas of PHP

The Quirks And Gotchas of PHP

If you come from a JavaScript background, you'll likely be familiar with some of its famous quirks, such as 1 + "1" equaling "11". Well, PHP has its own set of quirks and gotchas, too. Some are oddly similar to JavaScript's, while others can surprise a JavaScript developer.

Let's start with the more familiar ones.

1. Type Juggling and Loose Comparisons

Like JavaScript, PHP has two types of comparison operators: strict and loose. The loose comparison operator in PHP uses ==, while the strict comparison operator uses ===.

Here's an example of a loose vs. strict comparison in PHP:

var_dump(1 == "1"); // true
var_dump(1 === "1"); // false

PHP is a loosely typed language, meaning it will automatically convert variables from one type to another when necessary, just like JavaScript. This is not only when doing comparisons but also, for example, when doing numeric operations. Such conversions can lead to some unexpected results if you're not careful:

var_dump(1 + "1"); // int(2)
var_dump(1 + "1.5"); // float(2.5)
var_dump(1 + "foo"); // int(1) in PHP 7, TypeError in PHP 8

As you can see, the type system has gotten a bit stricter in PHP 8, so it won't let you commit some of the "atrocities" that were possible in earlier versions, throwing a TypeError instead. PHP 8 introduced many changes that aim to eliminate some of the unpredictable behavior; we will cover some of them throughout this article.

1.1. Truthiness of Strings

This is such a common gotcha in PHP that it deserves its own heading. By default, PHP considers an empty string as false and a non-empty string as true:

if ("0") {
    // This block executes because "0" is a non-empty string
    echo "This is considered TRUE in PHP";
}

But wait, there's more! PHP also considers the string "0" as false:

if ("0" == false) {
    // This block executes because "0" is considered FALSE in PHP
    echo "This is considered FALSE in PHP";
}

You might think we're done here, but no! Try comparing a string such as "php" to 0:

if ("php" == 0) {
    // This block executes in PHP 7
    echo "This is considered TRUE in PHP 7";
}

Until PHP7, any non-numeric string was converted to 0 when cast to an integer to compare it to the other integer. That's why this example will be evaluated as true. This quirk has been fixed in PHP 8.

For a comprehensive comparison table of PHP's truthiness, check out the PHP documentation.

1.2. Switch Statements

Switch statements in PHP use loose comparisons, so don't be surprised if you see some unexpected behavior when using them:

$value = "foo";
switch ($value) {
    case 0:
        echo "Value was 0"; // This block executes
        break;
    case "foo":
        echo "Value was foo";
        break;
}

The New Match Expression in PHP 8

PHP 8 introduced the match expression, which is similar to switch but uses strict comparisons (i.e., === under the hood) and returns a value:

$result = match ($value) {
    0 => 'Value is zero',
    1 => 'Value is one',
    default => 'Something else',
};

Unlike switch, there is no "fall-through" behavior in match, and each branch must return a value, making match a great alternative when you need a more precise or concise form of branching—especially if you want to avoid the loose comparisons of a traditional switch.

1.3 String to Number Conversion

In earlier versions of PHP, string-to-number conversions were often done silently, even if the string wasn’t strictly numeric (like '123abc'). In PHP 7, this would typically result in 123 plus a Notice:

// In PHP 7:
var_dump("123abc" + 0);
// int(123), with a Notice

In PHP 8, you’ll still get int(123), but now with a Warning, and in other scenarios (like extremely malformed strings), you might see a TypeError. This stricter behavior can reveal hidden bugs in code that relied on implicit type juggling.

Stricter Type Checks & Warnings in PHP 8

  • Performing arithmetic on non-numeric strings:

    As noted, in older versions, something like "123abc" + 0 would silently drop the non-numeric part, often producing 123 plus a PHP Notice. In PHP 8, such operations throw a more visible Warning or TypeError, depending on the exact scenario.

  • Null to Non-Nullable Internal Arguments:

    Passing null to a function parameter that’s internally declared as non-nullable will trigger a TypeError in PHP 8. Previously, this might have been silently accepted or triggered only a warning.

  • Internal Function Parameter Names:

    PHP 8 introduced named arguments but also made internal parameter names part of the public API. If you use named arguments with built-in functions, be aware that renaming or reordering parameters in future releases might break your code. Always match official parameter names as documented in the PHP manual.

Union Types & Mixed

Since PHP 8.0, we can declare union types, which allows you to specify that a parameter or return value can be one of multiple types. For example:

function getUser(int|string $id) {
// ...
}

Specifying the union of types your function accepts can help clarify your code’s intent and reveal incompatibilities if your existing code relies on looser type checking, preventing some of the conversion quirks we’ve discussed.

2. Operator Precedence and Associativity

Operator precedence can lead to confusing situations if you’re not careful with parentheses. For instance, the . operator (string concatenation similar to + in JavaScript) has left-to-right associativity, but certain logical operators have lower precedence than assignment or concatenation, leading to puzzling results in PHP 7 and earlier:

echo "Sum: " . 1 + 2;
// Actually interpreted as ((echo "Sum: ") . 1) + 2
// Outputs `2` and a Warning: A non-numeric value encountered

echo "Sum: " . (1 + 2);
// Correctly prints "Sum: 3"

PHP 8 has fixed this issue by making the + and - operators take a higher precedence.

3. Variable Variables and Variable Functions

Now, we're getting into unfamiliar territory as JavaScript Developers. PHP allows you to define variable variables and variable functions. This can be a powerful feature, but it can also lead to some confusing code:

$varName = 'hello';
$$varName = 'world';

echo $hello; // Outputs 'world'

In this example, the variable $varName contains the string 'hello'. By using $$varName, we're creating a new variable with the name 'hello' and assigning it the value 'world'.

Similarly, you can create variable functions:

function greet() {
    echo "Hello!";
}

$func = 'greet';
$func(); // Calls greet()

4. Passing Variables by Reference

You can pass variables by reference using the & operator in PHP. This means that any changes made to the variable inside the function will be reflected outside the function:

function increment(&$num) {
    $num++;
}

$number = 5;
increment($number);
echo $number; // Outputs 6

While this example is straightforward, not knowing the pass-by-reference feature can lead to some confusion, and bugs can arise when you inadvertently pass variables by reference.

5. Array Handling

PHP arrays are a bit different from JavaScript arrays. They can be used as both arrays and dictionaries, and they have some quirks that can catch you off guard. For example, if you try to access an element that doesn't exist in an array, PHP will return null instead of throwing an error:

$arr = [1, 2, 3];
var_dump($arr[3]); // NULL

Furthermore, PHP arrays can contain both numerical and string keys at the same time, but numeric string keys can sometimes convert to integers, depending on the context>

$array = [
    "1"   => "One (as string)",
    1     => "One (as int)",
    true  => "True as key?"
];

var_dump($array);
// Output can be surprising:
// array(1) {
//   [1] => string(12) "True as key?"
// }

In this example:

  • "1" (string) and 1 (integer) collide, resulting in the array effectively having only one key: 1.
  • true is also cast to 1 as an integer, so it overwrites the same key.

And last, but not least, let's go back to the topic of passing variables by reference. You can assign an array element by reference, which can feel quite unintuitive:

$array = ['apple', 'banana'];
$fruit = &$array[0];  // $fruit is now referencing the first element
$fruit = 'pear';

var_dump($array);
// array(2) {
//   [0] => "pear",
//   [1] => "banana"
// }

6 Checking for Variable Truthiness (isset, empty, and nullsafe operator)

In PHP, you can use the empty() function to check if a variable is empty. But what does "empty" mean in PHP? The mental model of what's considered "empty" in PHP might differ from what you're used to in JavaScript. Let's clarify this:

The following values are considered empty by the empty() function:

  • "" (an empty string)
  • 0 (0 as an integer)
  • 0.0 (0 as a float)
  • "0" (0 as a string)
  • null
  • false
  • [] (an empty array)

This means that the following values are not considered empty:

  • "0" (a string containing "0")
  • " " (a string containing a space)
  • 0.0 (0 as a float)
  • new stdClass() (an empty object)

Keep this in mind when using empty() in your code, otherwise, you might end up debugging some unexpected behavior.

Undefined Variables and isset()

Another little gotcha is that you might expect empty() to return true for undefined variables too - they contain nothing after all, right? Unfortunately, empty() will throw a notice in such case. To account for undefined variables, you may want to use the isset() function, which checks if a variable is set and not null:

$var = 0;
if (isset($var) && !empty($var)) {
    echo "Variable is set and not empty";
}

The Nullsafe Operator

If you have a chain of properties or methods that you want to access, you may tend to check each step with isset() to avoid errors:

if (isset($object) && isset($object->child)) {
    echo $object->child->getName();
}

In fact, because isset() is a special language construct and it doesn't fully evaluate an undefined part of the chain, it can be used to evaluate the whole chain at once:

if (isset($object->child)) {
    $result = $object->child->getName();
}

That's much nicer! However, it could be even more elegant with the nullsafe operator (?->) introduced in PHP 8:

// Instead of checking multiple times if $object or $object->child is null:
$result = $object?->child?->getName();

If you’ve used optional chaining in JavaScript or other languages, this should look familiar. It returns null if any part of the chain is null, which is handy but can also hide potential logic mistakes — if your application logic expects objects to exist, silently returning null may lead to subtle bugs.

Conclusion

While PHP shares a few loose typing quirks with JavaScript, it also has its own distinctive behaviors around type juggling, operator precedence, passing by reference, and array handling. Becoming familiar with these nuances — and with the newer, more predictable features in PHP 8 — will help you avoid subtle bugs and write clearer, more robust code. PHP continues to evolve, so always consult the official documentation to stay current on best practices and language changes.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Custom Next.js Servers - Do you really need them? cover image

Custom Next.js Servers - Do you really need them?

If you stumbled upon this post, chances are you are either using custom servers in one of your Next.js projects or considering it. You may also just be curious about custom servers and whether you need them. In either case, here are some things you should know about using custom servers in Next.js. What are custom servers? By default, Next.js uses a built-in server to handle incoming requests, which automatically starts when you run next dev or next start. However, if you need more control — such as handling specialized routing patterns or implementing custom server-side behaviors — you have the option to create a custom server. This approach gives you the flexibility to programmatically manage request handling beyond what Next.js’s native routing offers. To opt out of the default server, you need to create a server.js file in the root of your project and modify the package.json to use it: ` A basic custom server implementation using the Node.js HTTP module for some custom routing might look like this: ` In this example: - We create a Next.js app instance with next() - We get the request handler with app.getRequestHandler() - We create a custom HTTP server that intercepts requests to /custom-route - For that route, we render a specific page with custom query parameters - Next.js's default handler handles all other routes And here's an example of a custom server implementation that uses Express.js: ` In this Express.js example: - We create an Express server instance - We add custom middleware for logging requests - We define a parameterized route /custom-route/:id that renders a specific page - We create a custom API endpoint at /api/custom - We use a catch-all route to let Next.js handle all other routes Common use cases for custom servers Some of the common reasons why people have used custom servers include: 1. Custom Routing: Sometimes, you might want to implement custom routing logic beyond what Next.js's file-based routing offers, such as custom URL patterns and parameters or supporting legacy URL structures. 2. Request/Response Manipulation: Another reason is adding custom headers to responses or modifying request objects before they reach the application, such as implementing custom CORS policies. 3. Authentication and Authorization: Custom authentication flows can sometimes be implemented in a custom server, such as protecting routes based on user role or managing session state. 4. WebSockets Support: If your application requires real-time communication, you might need to implement WebSockets support, e.g., chat applications or live notifications and updates. That has been one of the most common reasons for using custom servers. 5. Background Processing: If you're using logging tools such as New Relic, you might need to implement background processing to avoid blocking responses. This is where custom servers could be useful historically. 6. Proxying Requests: Having integration with external APIs or services, you might need a custom server to be able to forward requests to those APIs, implement API gateways, or avoid CORS issues with these third-party services. 7. Non-HTTP Protocol Support: If your application needs to support protocols beyond HTTP and WebSockets that aren't supported by the Edge Runtime, having a custom server could be a solution. 8. Integration with Existing Systems: Embedding Next.js within larger applications, integrating with proprietary middleware, or working with legacy enterprise systems may require a custom server implementation. 9. Specialized Performance Requirements: Specific performance requirements, such as custom connection pooling or server-level caching strategies, may warrant a solution implemented in a custom server. 10. Multi-Application Architectures: A custom server might be needed if you need to serve multiple Next.js applications from a single server with custom routing logic or implement application-level load balancing. Caveats and Considerations of Using Custom Servers You should be aware of several important implications of using custom servers. Performance Implications 1. Loss of Automatic Static Optimization: Custom servers disable Next.js's automatic static optimization, forcing all pages to be server-rendered at runtime even if they could be statically generated. This can significantly impact performance and increase server load. 2. Increased TTFB (Time to First Byte): Without static optimization, Time to First Byte typically increases, affecting core web vitals and user experience. 3. Reduced Edge Caching Opportunities: Custom servers may interfere with CDN caching strategies that Next.js would otherwise optimize automatically. Deployment Limitations 1. Vercel Incompatibility: Custom servers cannot be deployed on Vercel, eliminating access to Vercel's optimized infrastructure for Next.js applications. 2. Serverless Deployment Challenges: Many serverless platforms are incompatible with custom server implementations, limiting deployment options. 3. Increased Infrastructure Requirements: Custom servers typically require traditional server infrastructure rather than more cost-effective serverless or edge options. Maintenance Challenges 1. Framework Updates: Custom servers require manual updates when upgrading Next.js versions, as they operate outside the standard upgrade path. 2. Divergence from Documentation: Most Next.js documentation assumes the standard server, making troubleshooting more difficult with custom implementations. 3. Knowledge Transfer: Custom server implementations create additional onboarding challenges for new team members who must understand both Next.js and your custom server logic. Compatibility Issues 1. Feature Incompatibility: Many Next.js features may not work as expected with custom servers, including: 1. Incremental Static Regeneration (ISR) 2. On-demand Revalidation 3. Image Optimization API 4. Middleware (in some configurations) 2. Standalone Output Mode: Custom servers are incompatible with Next.js's standalone output mode, which is designed to optimize deployments. 3. Next.js Compiler: Custom server files don't run through the Next.js compiler, requiring manual compatibility with your Node.js version. Security Considerations 1. Security Updates: Custom servers may miss security improvements automatically applied to the standard Next.js server. 2. Manual Security Implementation: Security features like CORS, rate limiting, and request validation must be manually implemented and maintained. 3. Increased Attack Surface: Custom servers potentially introduce additional security vulnerabilities if not correctly configured and maintained. Scaling Challenges 1. Manual Scaling Logic: Custom scaling logic must be implemented rather than leveraging platform-provided scaling for standard Next.js applications. 2. Resource Utilization: Custom servers often have less efficient resource utilization than the optimized standard Next.js server. 3. Global Distribution Complexity: Implementing global distribution and edge presence becomes significantly more complex with custom servers. Development Workflow Impacts 1. Development/Production Parity: Maintaining parity between development and production environments becomes more challenging. 2. Hot Module Replacement (HMR): Custom servers may interfere with Next.js's HMR capabilities, requiring manual configuration to maintain developer experience. 3. Debugging Complexity: Debugging becomes more complex as issues could stem from either Next.js or the custom server implementation. Migration Difficulties 1. Lock-in Effect: Once implemented, migrating away from a custom server can be challenging as application logic becomes intertwined with server implementation. 2. Refactoring Overhead: Significant refactoring may be required to move from a custom server to standard Next.js patterns. 3. Technical Debt: Custom servers often become sources of technical debt as Next.js evolves with new features that aren't compatible with custom implementations. Why you might not need a custom server and how to migrate away from it The performance and maintainability implications mentioned above provide a good incentive not to use custom servers and migrate away from them if you've used them in your project. Conveniently, Next.js has evolved a lot recently, and many of the use cases mentioned above that historically required custom servers can now be addressed using built-in Next.js features. Here's how modern Next.js handles these scenarios without custom servers: 1. Custom Routing → Dynamic Routes Next.js now provides comprehensive routing capabilities through its file-system-based router, which includes dynamic segments, catch-all routes, and optional catch-all routes. You can use the following patterns to achieve most of the use cases that historically required custom servers: ` 2. Request/Response Manipulation → Middleware Next.js Middleware provides a standardized way to modify requests and responses before they reach your application. You can easily implement custom headers, CORS, rate limiting, and more using middleware: ` 3. Authentication → Middleware + Auth Libraries Next.js Middleware combined with authentication libraries like NextAuth.js provides a more maintainable and secure approach to authentication. Instead of using a custom server, you can implement authentication logic in middleware like this: ` 4. WebSockets → Standalone WebSocket Server To preserve Next.js optimizations, you can implement WebSockets without a custom server using a standalone WebSocket server approach. This is more compatible with modern deployment platforms and preserves Next.js optimizations. To migrate away from a custom server, you can follow these steps: 1. Create a Standalone WebSocket Server First, create a separate WebSocket server file: ` 2. Create a WebSocket Client Hook ` 3. Update Your Next.js Application ` 4. Configure Your Deployment For production, you'll need to set up a proxy to forward WebSocket requests to your standalone server: ` 5. Update Your Package Scripts ` This approach gives you the best of both worlds: Next.js's optimized rendering and routing with the real-time capabilities of WebSockets, all without sacrificing deployment options or performance. 5. Background Processing → unstable_after API The new unstable_after API in Next.js 15 allows for background processing after a response has been sent. Instead of using a custom server, you can use this API to execute code after a response has been sent: ` Please note that this API is experimental and not yet stable, so it's important to watch the Next.js blog for updates. 6. Proxying Requests → Rewrites Next.js config rewrites provide a declarative way to proxy requests without custom server code. To proxy requests to an external API, you can use the following configuration, eliminating the need for a custom server: ` Use Cases That May Still Require Custom Servers While a lot of use cases that historically required custom servers can now be addressed by fully leveraging modern Next.js features, there are still some scenarios that may require a custom server implementation: - Non-HTTP Protocol Support: When your application needs to support protocols beyond HTTP and WebSockets, you might still need a custom server. - Deep Integration with Existing Systems: A custom server may be required for scenarios requiring tight integration with existing non-Node.js systems where the integration point must be at the server level. - Highly Specialized Performance Requirements: A custom server may be needed for applications with extremely specific performance needs that can't be addressed through Next.js's built-in optimizations. - Complex Multi-Application Architectures: When building complex architectures that don't fit the standard Next.js model and require custom orchestration at the server level, you might not be able to avoid a custom server. What to do if you need a custom server Follow best practices! That means if a custom server is absolutely necessary for your use case: 1. Minimize Custom Logic: Keep custom server logic to an absolute minimum. 2. Isolate Custom Code: Clearly separate custom server code from Next.js application code. 3. Document Thoroughly: Maintain detailed documentation explaining why the custom server is necessary and how it works. 4. Regular Reassessment: Periodically reassess whether the custom server is still necessary as Next.js evolves. 5. Monitoring: Implement comprehensive monitoring to quickly identify performance or stability issues related to the custom server. Conclusion Custom servers are a powerful tool that can address specific use cases that are not easily solved with modern Next.js features. However, they come with significant trade-offs in performance, deployment options, maintenance overhead, and compatibility with Next.js features. Before implementing a custom server, thoroughly evaluate whether modern Next.js features like middleware, API routes, and rewrites can address your requirements without the drawbacks of a custom server implementation. If you already have a custom server implemented, consider migrating to modern Next.js features if your use case can be addressed with them, as it will likely bring more benefits than drawbacks....

Next.js + MongoDB Connection Storming cover image

Next.js + MongoDB Connection Storming

Building a Next.js application connected to MongoDB can feel like a match made in heaven. MongoDB stores all of its data as JSON objects, which don’t require transformation into JavaScript objects like relational SQL data does. However, when deploying your application to a serverless production environment such as Vercel, it is crucial to manage your database connections properly. If you encounter errors like these, you may be experiencing Connection Storming: * MongoServerSelectionError: connect ECONNREFUSED <IP_ADDRESS>:<PORT> * MongoNetworkError: failed to connect to server [<hostname>:<port>] on first connect * MongoTimeoutError: Server selection timed out after <x> ms * MongoTopologyClosedError: Topology is closed, please connect * Mongo Atlas: Connections % of configured limit has gone above 80 Connection storming occurs when your application has to mount a connection to Mongo for every serverless function or API endpoint call. Vercel executes your application’s code in a highly concurrent and isolated fashion. So, if you create new database connections on each request, your app might quickly exceed the connection limit of your database. We can leverage Vercel’s fluid compute model to keep our database connection objects warm across function invocations. Traditional serverless architecture was designed for quick, stateless web app transactions. Now, especially with the rise of LLM-oriented applications built with Next.js, interactions with applications are becoming more sequential. We just need to ensure that we assign our MongoDB connection to a global variable. Protip: Use global variables Vercel’s fluid compute model means all memory, including global constants like a MongoDB client, stays initialized between requests as long as the instance remains active. By assigning your MongoDB client to a global constant, you avoid redundant setup work and reduce the overhead of cold starts. This enables a more efficient approach to reusing connections for your application’s MongoDB client. The example below demonstrates how to retrieve an array of users from the users collection in MongoDB and either return them through an API request to /api/users or render them as an HTML list at the /users route. To support this, we initialize a global clientPromise variable that maintains the MongoDB connection across warm serverless executions, avoiding re-initialization on every request. ` Using this database connection in your API route code is easy: ` You can also use this database connection in your server-side rendered React components. ` In serverless environments like Vercel, managing database connections efficiently is key to avoiding connection storming. By reusing global variables and understanding the serverless execution model, you can ensure your Next.js app remains stable and performant....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co