# Data types

Source: https://codewiki.com/php/data-types/

> - **what**: Types belong to PHP values, so a variable can hold a value of a different type later. Type declarations constrain selected positions such as parameters, returns, properties, and class constants.
> - **trap**: Casting and loose comparison discard information. A missing value, an invalid integer, and a valid zero can collapse to the same result.
> - **fix**: Validate before converting at input boundaries, use precise declarations and `===` in domain code, and narrow `mixed` as soon as you receive it.

## What it is and why it exists

PHP is dynamically typed: values have runtime types, but variable names are not permanently bound to one type. `$value` can hold an integer and later hold a string. Whenever an expression runs, the current value's type determines which operations and conversions apply. That flexibility suits form data, JSON, and database results, but it also postpones some mistakes until execution reaches them.

PHP also supports type declarations on parameters, return values, properties, and class constants. A declaration writes the set of allowed values into a function or object boundary, where the runtime checks it. Dynamic typing and declarations are not opposites. The former describes how variables and values behave; the latter places contracts at selected boundaries.

These are the common runtime types of PHP values. `get_debug_type()` reports a useful name for the current value, usually a class name for an object and a resource kind for a resource.

| Category | Runtime types | Typical use |
| --- | --- | --- |
| Scalar types | `bool`, `int`, `float`, `string` | One Boolean, number, or piece of text |
| Compound values | `array`, `object` | Containers and objects with behavior |
| Special values | `null`, `resource` | A missing value or external handle |

`callable` and `iterable` are closer to capability sets than separate value types reported by `get_debug_type()`. Both arrays and `Traversable` objects satisfy `iterable`; closures, function-name strings, and valid method arrays can satisfy `callable`. `mixed`, `void`, and `never` are also mainly declaration types. They mean any value, normal completion with no returned value, and no normal completion, respectively.

Data types answer "which values may appear here, and what can those values do?" A type cannot prove that a string came from a trusted source or that an integer lies in a business range. `int $quantity` excludes an array, but it does not exclude `-5`. Boundary validation still belongs to the application.

## How it works

### Values carry runtime types

Assignment binds a value to a variable name, and reassignment can change the type of the value currently associated with that name. Operators choose behavior from their operands: `.` performs string concatenation, arithmetic requires numeric values, and a condition interprets its value as a Boolean.

When checking a type, prefer a function that states the intent, such as `is_int()`, `is_string()`, `is_array()`, `is_object()`, `is_iterable()`, or `is_callable()`. `get_debug_type()` suits diagnostics and error messages. Some traditional names returned by `gettype()` differ from declaration syntax; an integer is reported as `integer`, for example. Do not turn those strings into a type-system API.

The same-looking assignment has different copying semantics for different values. Scalars and arrays behave as values, so changing a copy does not change the original variable. Object assignment copies a handle to the same object, which means both variables observe changes to that object's properties. The explicit reference operator `&` can also make variables aliases, but that is a different mechanism from object handles.

### Declarations constrain boundaries

An atomic declaration can be `int`, `string`, `array`, a class name, or an interface name, among others. A union type such as `int|string` accepts any one member. An intersection type such as `Countable&Iterator` requires an object to satisfy every member. Prefixing one type with `?` is nullable shorthand, so `?string` and `string|null` mean the same thing.

Parameters are checked when a function is entered, returns when a value is returned, and typed properties when they are assigned. PHP 8.3 also permits types on class, interface, trait, and enum constants. A failed declaration check normally throws `TypeError`, though scalar coercion may happen first depending on strict mode at the call site.

| Declaration position | When it is checked |
| --- | --- |
| Function parameter | When the call is entered |
| Return type | When the function returns |
| Object or static property | When the property is assigned |
| Class constant | When the class declaration is processed |

These checks constrain declared positions; they do not freeze variables or recursively inspect containers. Types placed on stable module boundaries are generally more useful than casts scattered among temporary local variables.

An `array` declaration proves only that the outer value is an array. It says nothing about keys or element types. Native `callable` cannot describe a parameter and return signature, and it cannot be used as a property type. PHPDoc and static analysis can describe narrower array shapes, generic collections, and callable signatures, but runtime validation is still a separate design decision.

### Context can trigger conversion

Type juggling is PHP's automatic conversion of a value according to context. A valid numeric string becomes a number in arithmetic, an integer is treated as text when joined with `.`, and a condition performs Boolean conversion. Explicit casts such as `(int)` and `(string)` use the corresponding conversion rules, but "a value of the target type was produced" does not mean "the input was valid."

Boolean context is particularly good at hiding business distinctions. `false`, integer `0`, float `0.0`, the empty string, string `'0'`, an empty array, and `null` are all false; string `'00'` and a nonempty array are true. `empty()` uses similarly broad rules, so it is a poor validator for fields where zero must survive.

`==` converts operands according to the comparison rules, while `===` requires both type and value to match. Loose comparison is not a simple rule like "convert everything to text" or "convert everything to a number." Each type pairing has its own rules. For business identifiers, status codes, and validation results, normalize to the intended type first and then use `===`.

### Strict mode only tightens scalar calls

`declare(strict_types=1);` at the top of a file enables strict types. For calls to user-defined functions made from that file, scalar arguments normally have to match the declaration exactly. The one widening exception lets an `int` satisfy a `float` declaration. The caller's file chooses the argument rule; the file containing the function cannot choose it for every caller.

Strict mode also tightens return-type handling in the file containing the declaration, but it is not an input validator. JSON, query parameters, and form fields still enter the program as strings, arrays, integers, or `null`. Your code must decide which representations, ranges, and missing states are allowed. Strict mode does not inspect the elements of an `array`, either.

## Examples

The four examples inspect runtime types, type declarations, input parsing, and comparison rules in that order. Every output below was produced with the local PHP 8.3.33 CLI.

### Inspecting the current value's type

The same variable first binds an integer and then a string with the same visible content. The array and generator have different concrete runtime types, but both satisfy `iterable`.

<!-- quick -->

```php
// file: runtime_types.php
<?php

$value = 17;
printf("%s: %s\n", get_debug_type($value), $value);

$value = '17';
printf("%s: %s\n", get_debug_type($value), $value);

$items = ['draft', 'review'];
$stream = (function (): Generator {
    yield 'publish';
})();

foreach ([$items, $stream] as $collection) {
    printf(
        "%s iterable=%s\n",
        get_debug_type($collection),
        is_iterable($collection) ? 'yes' : 'no',
    );
}
```

```text
int: 17
string: 17
array iterable=yes
Generator iterable=yes
```

<!-- /quick -->

The first two lines show that the type belongs to the current value, not the variable name. String `'17'` looks numeric, but it remains a string until a conversion occurs.

`iterable` describes the ability to be traversed by `foreach`. Inspection reports `array` and `Generator`, not two runtime values whose type is named `iterable`.

### Expressing a contract with union and nullable types

An order identifier may be an integer from a database or a textual external identifier, so the function accepts those two forms with a union. An owner may genuinely be absent, so the other function includes `null` in its declaration.

```php
// file: declared_types.php
<?php
declare(strict_types=1);

function normalizeOrderId(int|string $id): string
{
    return is_int($id)
        ? sprintf('ORD-%05d', $id)
        : strtoupper(trim($id));
}

function displayOwner(?string $owner): string
{
    return $owner ?? 'unassigned';
}

echo normalizeOrderId(27), PHP_EOL;
echo normalizeOrderId(' web-27 '), PHP_EOL;
echo displayOwner(null), PHP_EOL;

try {
    normalizeOrderId(2.5);
} catch (TypeError) {
    echo "TypeError\n";
}
```

```text
ORD-00027
WEB-27
unassigned
TypeError
```

`is_int()` narrows the union to one branch, after which both branches clearly return strings. `2.5` is neither an `int` nor a `string`, so the strict call throws `TypeError` before the function body runs.

A union should express real domain input, not grow indefinitely to avoid designing a boundary. If every caller can normalize to one identifier representation first, accepting one value object or string is simpler.

### Validate external input before converting it

Query parameters commonly enter a program as strings. This parser accepts `mixed`, then immediately checks the allowed input types, integer syntax, and nonnegative range.

```php
// file: parse_quantity.php
<?php
declare(strict_types=1);

function parseQuantity(mixed $raw): int
{
    if (is_int($raw)) {
        $quantity = $raw;
    } elseif (is_string($raw)) {
        $parsed = filter_var($raw, FILTER_VALIDATE_INT);
        if ($parsed === false) {
            throw new InvalidArgumentException('not an integer');
        }
        $quantity = $parsed;
    } else {
        throw new InvalidArgumentException('wrong input type');
    }

    if ($quantity < 0) {
        throw new InvalidArgumentException('negative quantity');
    }
    return $quantity;
}

foreach (['12', '0', '12 boxes', null] as $raw) {
    try {
        printf("%s => %d\n", get_debug_type($raw), parseQuantity($raw));
    } catch (InvalidArgumentException) {
        printf("%s => invalid\n", get_debug_type($raw));
    }
}
```

```text
string => 12
string => 0
string => invalid
null => invalid
```

Valid string `'0'` survives as integer `0`, while the string with trailing text and `null` are rejected. A direct `(int) ($raw ?? 0)` would collapse distinct error states into one integer, leaving callers unable to tell them apart.

Here `mixed` means that the boundary value is not trusted yet, not that later code may perform arbitrary operations on it. Only after the branches finish does `$quantity` carry the `int` meaning that domain code can rely on.

### Separate truthiness from equality

A conditional position tests truthiness, while a comparison operator relates two operands. Both can involve conversion rules, but they answer different questions.

```php
// file: comparisons.php
<?php

$values = ['', '0', '00', [], [0]];

foreach ($values as $value) {
    printf(
        "%s => %s\n",
        json_encode($value),
        $value ? 'true' : 'false',
    );
}

var_export('10' == 10);
echo PHP_EOL;
var_export('10' === 10);
echo PHP_EOL;
```

```text
"" => false
"0" => false
"00" => true
[] => false
[0] => true
true
false
```

String `'0'` is PHP's unusual nonempty false string, while `'00'` is true. Testing a business code with `if ($code)` can reject a valid string `'0'`; test `null`, the empty string, or the required format according to the contract instead.

The final two values are equal but have different types. Once an input has been normalized, `===` prevents the comparison itself from introducing another implicit conversion.

## Pitfalls

### Treating a cast as validation

> **Pitfall:** `(int)` guarantees an integer result, but not that the original value was a complete, valid integer in the business range. Missing values and some invalid inputs can therefore become a plausible `0`.

**Fix:** constrain the allowed input types and text syntax first, then convert and check the range. The interface contract should say whether an error produces a result object or a domain exception.

### Expecting `strict_types` to sanitize request data

> **Pitfall:** Strict mode controls checks at declared positions. It does not parse query parameters into integers or validate array keys, string formats, or numeric ranges.

**Fix:** create a parsing layer at HTTP, CLI, message-queue, and database boundaries. Once that layer returns narrow types, strict declarations on internal functions have stable meaning.

### Depending on loose comparison and `empty()`

> **Pitfall:** `==` and `empty()` place several representations on the same branch. String `'0'`, integer `0`, `null`, and the empty string may mean entirely different things in the domain.

**Fix:** write down the accepted representations and missing-value policy, normalize once, and use `===`, `is_*()`, or a domain validator. One broad condition is not a data contract.

### Assuming `array` includes an element type

> **Pitfall:** `array $orders` checks only that the outer value is an array. Missing keys, wrong element types, and list-versus-map shape can all pass the runtime declaration.

**Fix:** validate the shape at a boundary and use named objects for stable domain data. PHPDoc array shapes and generics help static analysis, but they do not replace runtime checks.

### Treating object assignment as object copying

> **Pitfall:** `$copy = $original` copies an object handle, not the instance. A property change through either variable is visible through the other variable because both refer to the same object.

**Fix:** use `clone` when you need a new identity, and define a cloning policy for mutable objects held inside it. When sharing is intended, make ownership clear through names and interfaces instead of leaving readers to guess.

<!-- deep -->

## Runtime values and declaration types

Runtime value types and declaration types overlap, but they are not one enumeration. Keeping them separate prevents the wrong names from leaking between reflection, error messages, and static analysis.

### Atomic declarations and capability sets

`bool`, `int`, `float`, `string`, `array`, `object`, `null`, and class or interface names can appear in suitable userland declaration positions. `resource` is the exception: it is a runtime type but cannot be used as a userland type declaration. A resource-accepting boundary usually takes `mixed`, then checks `is_resource()` and `get_resource_type()`.

`iterable` is equivalent to accepting an `array` or an object implementing `Traversable`, so a value's concrete type remains an array or class. `callable` accepts a value callable from the current scope, but native syntax cannot constrain its parameter and return signature. Callability also depends on scope and visibility, so an external string should not become a trusted callback merely because it has the right shape.

`callable` cannot be a property type because the stored value cannot be guaranteed to remain callable from every scope. If a property holds a closure, it can be declared as `Closure`. A more general callback should be validated at the construction boundary and converted to a clear internal representation.

### Unions, intersections, and DNF

A union uses `|` to mean "satisfies at least one member." PHP rejects some obviously redundant combinations at compile time, including `int|INT` and `bool|false`. Because `mixed` already includes every value, it cannot be combined with another union member either.

An intersection uses `&` to mean "satisfies every member." Its members must be class or interface types, so `int&Countable` is invalid. Intersections suit APIs that require an object with two independent capabilities; they do not express array element constraints.

Since PHP 8.2, a disjunctive normal form type can combine intersections and unions, as in `(Countable&Iterator)|array`. The intersection must be parenthesized. Keep the expression narrow enough to explain aloud; once the signature becomes a puzzle, a named interface is usually easier to maintain.

### `mixed`, `void`, and `never`

`mixed` accepts every value, including `null`. It suits a boundary that has not been parsed or a truly transparent forwarding layer, but it transfers narrowing responsibility to the function body. A function that accepts `mixed` and immediately performs arithmetic or member access usually has an unfinished contract.

`void` is return-only and says that normal completion provides no result value. A function may execute bare `return;`, but it cannot return an expression. Calling a `void` function still produces `null` as an implementation result, though callers should not treat that result as API data.

`never` is also return-only and says that the function cannot complete normally, for example because it always throws or terminates the process. A `never` function violates its declaration if it reaches the end or tries to return normally. It describes control flow, not a value that a variable can store.

## Conversion cannot preserve provenance

Conversion rules see only the current value. They do not know whether it came from trusted configuration or a user request. Once a string becomes an integer, its original spelling, leading characters, missing state, and any truncated part may be gone. Conversion should happen only after the representation has been accepted.

### Numeric strings and arithmetic

A well-formed numeric string can participate in arithmetic and becomes an `int` or `float` according to its contents. A nonnumeric string in PHP 8 arithmetic raises `TypeError`; a string with trailing nonnumeric content also produces a diagnostic. Relying on conversion during arithmetic lets expression side effects define the validation policy.

An explicit parser can handle syntax errors, range errors, and missing values separately. Monetary values usually need decimal-scale and rounding rules as well; converting to a binary float first does not make them exact. A data type provides storage, while domain rules still have to be stated.

### Boolean conversion is not text parsing

A Boolean cast does not interpret natural-language words as true and false. Every nonempty string outside the false-value set is true, so `(bool) 'false'` and `(bool) 'no'` are both `true`. For form Boolean fields, enumerate the permitted text or use a validator with an explicit policy.

Likewise, `empty()` answers PHP's broad truthiness question, not "does the field exist?" or "is the text blank?" Array-key existence, nullable values, and whitespace-only strings need separate checks, or valid zero values will easily be mistaken for missing values.

### Normalize before comparison

Loose comparison tables vary with the operand pairing. Memorizing a few isolated examples does not create a reliable contract. A sturdier boundary parses input into one known type and uses strict comparison inside.

If an interface genuinely accepts several representations, such as integer `10` and string `'10'` for the same identifier, its normalization function should document that policy and return one type. Conversion then happens once, so later authorization, lookup, and branching do not each choose a different rule.

## Strict mode at file boundaries

`strict_types` is a file-level directive, and the argument rule follows the file where the call occurs. A function defined in a strict file can still receive coerced scalar arguments when a non-strict file calls it. Conversely, a strict file uses strict argument checks when it calls a user-defined function declared elsewhere.

This design means a library author cannot force every caller to use strict scalar arguments merely by adding the directive to library files. The library should still declare accurate types because the contracts and `TypeError` failures matter, but external data must be parsed at an explicit library or application boundary.

Calls made by internal functions have their own rules, so do not extrapolate from the call-site rule for user-defined functions. When a boundary behavior matters, run a minimal example on the target PHP version and check the official manual instead of letting generated code guess from another language's strict mode.

### Strict does not mean static

Enabling strict mode does not turn PHP into a statically typed language. Variables can still be rebound to different types, undeclared positions can still hold any value, and many mistakes still surface only when execution reaches the relevant path.

A static analyzer can use native declarations, PHPDoc, and control flow to find more problems earlier, but its conclusions depend on truthful annotations. Asserting an unvalidated `mixed` value into a narrow type in a comment only hides risk. Runtime parsing and the static contract need to describe the same boundary.

<!-- /deep -->

[Checkpoint: php/data-types](https://codewiki.com/php/data-types/#checkpoint)

## Further reading

- [PHP manual: Types](https://www.php.net/manual/en/language.types.php)
- [PHP manual: Type declarations](https://www.php.net/manual/en/language.types.declarations.php)
- [PHP manual: Type juggling](https://www.php.net/manual/en/language.types.type-juggling.php)
- [PHP manual: Type comparison tables](https://www.php.net/manual/en/types.comparisons.php)
- [PHP manual: Callables](https://www.php.net/manual/en/language.types.callable.php)
