Understanding PHP’s strict_types, Type Checking, and Float Coercion

Understanding PHP’s strict_types, Type Checking, and Float Coercion
Photo by Leo Rivas / Unsplash

When working with PHP, especially in modern versions like 8.2.x through 8.4.x, many developers stumble upon unexpected behaviors around type-checking. This is especially true when dealing with strict_types, is_float, and type coercion. Let’s dive deep into this with an example, breaking down what’s happening behind the scenes and what it means for your code.


The Code: A Simple Example

<?php
declare(strict_types=1);

var_dump(is_float(1));
var_dump(gettype(1));
var_dump(1);

echo "\n";

function f(float $f){
    var_dump(is_float($f));
    var_dump(gettype($f));
    var_dump($f);
}

f(1);

Breaking It Down

1️⃣ Strict Types (declare(strict_types=1))

By declaring strict_types=1, PHP is instructed to enforce strict type-checking when calling functions and methods. This means that type coercion (implicit conversion) is disabled for scalar types (like int, float, string, bool). If you pass an int where a float is expected, PHP should ideally throw a TypeError. But here’s the twist.


2️⃣ Type Checking with is_float and gettype

  • is_float(1) checks whether the literal 1 is a float. Since 1 is an integer, it returns false.
  • gettype(1) returns the type of 1, which is "integer".
  • When we pass 1 to f(float $f), we’d expect a TypeError because we’re passing an integer into a float-typed function with strict_types=1. But instead, it silently converts 1 to 1.0 (a float).

3️⃣ The Reality of PHP’s Float Coercion

Here’s the surprising part: PHP, even with strict_types, allows implicit conversion of integers to floats. This means:

  • Passing 1 to f(float $f) converts 1 (integer) to 1.0 (float).
  • is_float($f) inside f() returns true.
  • gettype($f) returns "double" (which is PHP’s internal name for float).
  • The value of $f is float(1).

Why does this happen? PHP considers integers and floats “compatible” in many contexts, even under strict type-checking. This is because floating-point numbers naturally include all integers—an int like 1 can be precisely represented as a float (1.0).


The Output

For PHP versions 8.2.x, 8.3.x, and 8.4.x, the output is:

bool(false)
string(7) "integer"
int(1)

bool(true)
string(6) "double"
float(1)

Why No TypeError?

This behavior highlights an exception to strict_types. Scalar values that can be safely and precisely converted, such as int to float, are allowed. However, passing a float to a function(int $i) will throw a TypeError, because floating-point numbers can’t always be converted to integers without potential loss of precision.


Key Takeaways

strict_types=1 applies to function/method parameters, but not to internal functions like is_float and gettype.

PHP treats integers and floats as safely interchangeable when passing to a float parameter, even with strict types.

gettype returns "double" for floats, which can be confusing, but it’s PHP’s convention.

Be cautious with implicit type conversions, especially when designing APIs or critical systems where precision matters.


Other Considerations You Might Miss

  • Rounding Issues: When converting floats to ints (e.g., 1.9 to int), PHP truncates rather than rounds. This is not reversible, unlike int to float.
  • Database Implications: If you’re storing numeric values, always be explicit about types. Databases like MySQL have clear distinctions between INT and FLOAT/DOUBLE, and mismatches can cause subtle bugs.
  • Interfacing with External APIs: When sending data as JSON or to APIs, be explicit in your serialization. 1 might be sent as an integer but expected as a float on the other side.
  • PHP Version Differences: Although your code works the same in 8.2.x to 8.4.x, future versions of PHP might tighten rules or introduce deprecation warnings for implicit coercion.

Finally

This example teaches us that PHP’s type system isn’t always as strict as we think, even with strict_types=1. Understanding these subtle behaviors is crucial for writing robust, type-safe code. Always consider how your types interact, especially when crossing boundaries like databases, APIs, or strict business logic.

Support Us