PHP Data Types

Every value that exists in a PHP script — a number, a piece of text, a list, an object — has a data type, and that type determines what you can do with the value and how PHP stores it in memory. PHP is a dynamically typed and, by default, weakly typed language: a variable’s type is decided at runtime based on the value assigned to it, and PHP will freely convert between types when it needs to. Understanding how PHP’s type system actually behaves — not just the eight type names — is one of the most important skills for writing correct, bug-free PHP, because most “weird” PHP bugs trace back to a misunderstood type conversion.

Overview: How PHP’s Type System Works

Unlike languages such as Java or C#, you never declare a variable’s type in PHP. Instead, every variable is stored internally by the Zend Engine (PHP’s execution engine) as a container called a zval (“Zend value”). A zval bundles together the actual value, a type tag, and a reference count used for memory management. When you write $x = 5; the zval’s type tag is set to “integer”; if you later write $x = "hello"; the same variable now points to a zval tagged “string”. The variable itself has no fixed type — only the value currently inside it does. This is what “dynamically typed” means.

PHP groups its data types into three categories:

  • Scalar types — single values: int, float (also called double), string, bool.
  • Compound types — values built from other values: array, object, callable, iterable.
  • Special typesnull (the absence of a value) and resource (a handle to an external resource such as a file or database connection, largely superseded by objects in modern PHP).

The table below summarizes the core types you will use constantly:

Type Category Example literal Notes
int Scalar 42, -7, 0x1A Platform-dependent size, typically 64-bit
float Scalar 3.14, 2.5e3 IEEE 754 double precision; imprecise for exact decimals
string Scalar "hello", 'world' Byte sequence, not true Unicode-aware
bool Scalar true, false Many values are “falsy” (see Common Mistakes)
array Compound [1, 2, 3] Ordered map; works as both list and dictionary
object Compound new stdClass() Instance of a class; enums are objects internally
null Special null A variable can be null regardless of its declared type unless marked non-nullable
resource Special result of fopen() Increasingly replaced by objects (e.g. CurlHandle)

Syntax

There is no keyword you write to “declare” a type — the type comes from the literal or expression you assign. What you can do explicitly is inspect, cast, or constrain types:

gettype($value);        // returns the type name as a string
var_dump($value);       // prints type AND value, recursively for arrays/objects
(int) $value;            // cast operator — one-off conversion
settype($value, "int"); // converts the variable in place, returns bool
is_int($value);          // one of several is_*() type-checking functions
declare(strict_types=1); // must be the very first statement in a file
  • Cast operators: (int), (float), (string), (bool), (array), (object) convert a value to a new type without changing the original variable.
  • is_*() functions: is_int(), is_float(), is_string(), is_bool(), is_array(), is_object(), is_null(), is_callable(), is_numeric() let you branch on a value’s runtime type.
  • declare(strict_types=1): an opt-in, per-file directive that stops PHP from silently coercing scalar arguments and return values in typed function signatures — it must be the first line of code in the file.

Examples

Example 1: Inspecting every scalar and compound type

<?php
$integer = 42;
$float = 3.14;
$string = "Hello, PHP!";
$boolean = true;
$array = [1, 2, 3];
$object = new stdClass();
$object->name = "PHP";
$nullValue = null;

echo gettype($integer) . PHP_EOL;
echo gettype($float) . PHP_EOL;
echo gettype($string) . PHP_EOL;
echo gettype($boolean) . PHP_EOL;
echo gettype($array) . PHP_EOL;
echo gettype($object) . PHP_EOL;
echo gettype($nullValue) . PHP_EOL;

Output:

integer
double
string
boolean
array
object
NULL

Note that gettype() returns historical names — "double" for float and uppercase "NULL" for null — which is why is_*() functions are usually preferred in real code; they are more readable and consistent.

Example 2: Type juggling in action

<?php
$a = "10";
$b = 10;

var_dump($a == $b);
var_dump($a === $b);

$sum = "5" + "3";
echo $sum . PHP_EOL;

$concat = "5" . "3";
echo $concat . PHP_EOL;

$price = "19.99";
$total = $price * 2;
echo $total . PHP_EOL;

Output:

bool(true)
bool(false)
8
53
39.98

This is PHP’s weak typing at work. == (loose equality) converts one side to match the other before comparing, so the string "10" and the integer 10 compare equal. === (strict equality) refuses to convert, so it checks type and value, returning false. The arithmetic operators + and * automatically convert numeric strings to numbers, while . (concatenation) always treats its operands as strings — which is why "5" + "3" is 8 but "5" . "3" is "53".

Example 3: Type declarations, strict_types, and casting

<?php
declare(strict_types=1);

function describeType(int|float|string|bool|null $value): string
{
    return match (true) {
        is_int($value) => "Integer: $value",
        is_float($value) => "Float: $value",
        is_string($value) => "String: \"$value\"",
        is_bool($value) => "Boolean: " . ($value ? "true" : "false"),
        is_null($value) => "Null value",
    };
}

echo describeType(42) . PHP_EOL;
echo describeType(3.14) . PHP_EOL;
echo describeType("hello") . PHP_EOL;
echo describeType(false) . PHP_EOL;
echo describeType(null) . PHP_EOL;

$stringNumber = "100";
$castToInt = (int) $stringNumber;
$castToFloat = (float) $stringNumber;

var_dump($castToInt);
var_dump($castToFloat);

Output:

Integer: 42
Float: 3.14
String: "hello"
Boolean: false
Null value
int(100)
float(100)

The union type int|float|string|bool|null (PHP 8.0+) tells the engine — and the reader — exactly which types are acceptable, and declare(strict_types=1) means calling describeType("42") with a numeric string would throw a TypeError instead of silently converting. The explicit casts at the bottom show how (int) and (float) convert a numeric string on demand without affecting the original variable.

How It Works Under the Hood

When the Zend Engine evaluates a comparison or arithmetic expression involving mismatched types, it follows a fixed set of type juggling rules baked into the engine itself:

  • Number + numeric string — the string is parsed as a number and normal arithmetic occurs.
  • Number + non-numeric string — since PHP 8, this throws a TypeError for arithmetic operators (PHP 7 issued a warning and treated the string as 0).
  • Loose comparison (==) — PHP picks a common type to compare in. Since PHP 8, comparing a number to a non-numeric string converts the number to a string (this reversed PHP 7’s riskier behavior, fixing bugs like 0 == "abc" being true).
  • Comparison with bool — the other operand is converted to bool. Empty string, "0", 0, 0.0, empty array, and null are all “falsy”; everything else, including "0.0" and "false" (as strings), is truthy.

Underneath, every PHP array is actually an ordered hash table (an “ordered map”), which is why arrays can act as both lists and associative dictionaries. Objects, by contrast, are stored by handle — assigning an object to a new variable copies a reference to the same underlying instance, while assigning a scalar or array triggers PHP’s copy-on-write mechanism, which only physically duplicates the data when one of the copies is actually modified. This is why passing large arrays to functions is cheap until the function mutates them.

Common Mistakes

Mistake 1: Trusting loose comparison in in_array()

<?php
$statuses = ["0", "1", "2"];
$search = false;

if (in_array($search, $statuses)) {
    echo "Found match (unexpectedly)";
} else {
    echo "No match";
}

Output:

Found match (unexpectedly)

This is wrong because in_array() defaults to loose comparison. When PHP compares false to the string "0", it converts "0" to a boolean (which is false, since "0" is one of PHP’s falsy strings), so false == "0" evaluates to true — a match nobody intended. The fix is to pass true as the third argument to force strict comparison:

<?php
$statuses = ["0", "1", "2"];
$search = false;

if (in_array($search, $statuses, true)) {
    echo "Found match (unexpectedly)";
} else {
    echo "No match";
}

Output:

No match

Mistake 2: Comparing floats with ==

<?php
$total = 0.1 + 0.2;

if ($total == 0.3) {
    echo "Equal";
} else {
    echo "Not equal";
}

Output:

Not equal

Floats are stored as IEEE 754 binary approximations, so 0.1 + 0.2 actually evaluates to 0.30000000000000004, which is not exactly equal to the literal 0.3. This is not a PHP quirk — it is how floating-point math works in every language that uses this format. The fix is to compare within a small tolerance instead of for exact equality:

<?php
$total = 0.1 + 0.2;
$epsilon = 0.00001;

if (abs($total - 0.3) < $epsilon) {
    echo "Equal (within tolerance)";
} else {
    echo "Not equal";
}

Output:

Equal (within tolerance)

Best Practices

  • Add declare(strict_types=1); as the first line of new files so type-hinted functions reject mismatched scalar types instead of silently coercing them.
  • Prefer === and !== over == and != unless you specifically want type coercion.
  • Type-hint function parameters and return values (int, ?string, union types, etc.) — this documents intent and lets PHP catch mistakes early.
  • Never compare floats for exact equality; compare the absolute difference against a small epsilon, or use arbitrary-precision libraries (like bcmath) for money.
  • Validate and convert external input (from $_GET, $_POST, JSON, databases) explicitly with casts or filter_var() rather than assuming its type.
  • Use is_numeric() before doing arithmetic on a string you are not sure is numeric, to avoid a TypeError.
  • Remember that 0, 0.0, "0", "", [], and null are all falsy — be explicit with === null or === '' when you specifically mean “empty” versus “falsy”.
  • Prefer PHP 8.1+ enum types over loose string or int “status” constants when a value should only ever be one of a fixed set.

Practice Exercises

  1. Write a script that stores the string "7" and the integer 7 in two variables, then prints the result of comparing them with both == and ===, explaining in a comment why each result occurs.
  2. Write a strictly-typed function sumValues(int $a, int $b): int with declare(strict_types=1) enabled. Call it once with two integers, and describe (as a comment) what would happen if you called it with two numeric strings instead.
  3. Given $value = "3.5 apples";, use is_numeric() to safely check whether it can be treated as a number before attempting any arithmetic on it, and print an appropriate message either way.

Summary

  • PHP has eight data types: int, float, string, bool, array, object, null, and resource.
  • PHP is dynamically and weakly typed — a variable’s type is determined by its current value and can change at runtime.
  • Internally, values are stored in zvals; the Zend Engine applies fixed juggling rules whenever mismatched types are compared or combined.
  • == allows type conversion during comparison; === requires both type and value to match.
  • declare(strict_types=1) and scalar type declarations let you opt into safer, more predictable function signatures.
  • Common pitfalls include loose comparisons in functions like in_array(), exact float equality checks, and trusting the type of unvalidated external input.