Types and the Type System
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05; the version in which each type or rule appeared is noted, so code that must run on PHP 8.3 or 8.4 can be checked against it. It describes the values PHP knows about and the type declarations that let you state, and have the engine enforce, which values a parameter, return value, property or constant may hold. Conversions between types (juggling, casting, comparisons) are the subject of Type Juggling and Comparisons.
The type system at a glance
PHP is dynamically typed: every value carries its type at runtime, and a variable can hold values of different
types over its life. It is also gradually typed: declarations are optional, and where present they are checked at
runtime, throwing a TypeError on mismatch. The manual describes it as a nominal type system (types are related by
their declared names, class B extends A, not by their structure) with a behavioural subtyping relation checked at
compile time, while the types of actual values are verified at run time.
The building blocks are:
-
Atomic types:
-
built-in: the scalar types
bool,int,floatandstring;array;object;resource;never;void; the relative class typesself,parentandstatic; the singleton (literal) typesfalseandtrue; the unit typenull; -
user-defined types, also called class-types: classes, interfaces and enumerations;
-
callable.
-
-
Composite types: union types (
T|U), intersection types of class-types (T&U) and their combination in disjunctive normal form (DNF,(T&U)|V). -
Type aliases:
mixed(any value:object|resource|array|string|float|int|bool|null) anditerable(Traversable|array). PHP has no user-defined type aliases; static analysers provide them through PHPDoc.
get_debug_type() (PHP 8.0) returns a value’s type using the names you would write in a declaration, which makes it
the right function for error messages; the older gettype() returns historical names such as "integer", "double"
and "NULL":
<?php
declare(strict_types=1);
enum Format { case Ebook; }
$values = [null, true, 42, 3.14, 'Solaris', [1, 2], new stdClass(), Format::Ebook, fopen('php://memory', 'r'), strlen(...)];
foreach ($values as $value) {
printf("%-22s %s\n", get_debug_type($value), gettype($value));
}
null NULL
bool boolean
int integer
float double
string string
array array
stdClass object
Format object
resource (stream) resource
Closure object
Source: Type System and get_debug_type
The types, one by one
null
null is the only value of the null type. A variable is null if it was assigned null, has not been set yet (an
undefined variable, which also raises a warning when read) or was unset(). The constant is case-insensitive
(NULL, null), but lower case is the convention. is_null($x) is the same as $x === null.
<?php
declare(strict_types=1);
$returnedAt = null;
var_dump($returnedAt === null); // bool(true)
var_dump(isset($returnedAt)); // bool(false) -- isset() is false for null
function findTitle(array $byIsbn, string $isbn): ?string
{
return $byIsbn[$isbn] ?? null; // null means "not found"
}
var_dump(findTitle(['978-0-00-000001-1' => 'Kindred'], '000')); // NULL
Since PHP 8.2 null can be declared on its own (useful in a subclass that narrows a return type) and has always been
usable in unions as T|null or ?T.
Source: NULL
bool
bool has two values, true and false (case-insensitive constants). When another type is used in a boolean
context, these values are considered false: false, 0, 0.0, -0.0, "", "0", [], null and
SimpleXML objects created from empty elements without attributes. Everything else is true, including "0.0",
" ", "false" and every object.
<?php
declare(strict_types=1);
$candidates = [
'0' => 0, '0.0' => 0.0, "''" => '', "'0'" => '0', "'0.0'" => '0.0', "' '" => ' ',
"'false'" => 'false', '[]' => [], '[0]' => [0], 'null' => null, 'new stdClass()' => new stdClass(),
];
foreach ($candidates as $label => $value) {
printf("%-16s %s\n", $label, $value ? 'true' : 'false');
}
0 false
0.0 false
'' false
'0' false
'0.0' true
' ' true
'false' true
[] false
[0] true
null false
new stdClass() true
The string "0" being false is a frequent source of bugs: if ($input) rejects a user who typed 0. Compare
explicitly ($input !== '') when the distinction matters.
Source: Booleans
int
An int is a signed integer of the platform’s word size: 64 bits on all current platforms, so the range is
PHP_INT_MIN (-9223372036854775808) to PHP_INT_MAX (9223372036854775807); PHP_INT_SIZE is 8. Integer literals
can be written in decimal, hexadecimal (0x), octal (0o since 8.1, or a leading 0) and binary (0b), with _
separators between digits since PHP 7.4.
<?php
declare(strict_types=1);
$copies = 1_250_000; // 7.4: digit separators
$flags = 0b0000_0101; // binary: 5
$mode = 0o755; // 8.1: explicit octal prefix (493)
$legacy = 0755; // the old octal form, still valid
$colour = 0xFF_A5_00; // hexadecimal
var_dump($copies, $flags, $mode, $legacy === $mode, $colour);
// Integer overflow does not wrap: the result silently becomes a float.
var_dump(PHP_INT_MAX + 1); // float(9.223372036854776E+18)
var_dump(is_int(PHP_INT_MAX + 1)); // bool(false)
// Division always yields a float unless both operands divide evenly...
var_dump(7 / 2, 8 / 2); // float(3.5) int(4)
// ...so use intdiv() for integer division.
var_dump(intdiv(7, 2)); // int(3)
Overflowing into a float loses precision without any warning. When integers can exceed 64 bits (money in minor units rarely does; cryptography and identifiers sometimes do), use the GMP or BCMath extensions described in Math, Numbers and Randomness.
Source: Integers
float
A float is an IEEE 754 double-precision number (about 15 significant decimal digits). Literals use a decimal point or
an exponent (1.5, 1e3, 1_000.5). PHP_FLOAT_EPSILON, PHP_FLOAT_MIN, PHP_FLOAT_MAX and PHP_FLOAT_DIG
describe the format, and NAN and INF are special values.
<?php
declare(strict_types=1);
var_dump(0.1 + 0.2 == 0.3); // bool(false)
var_dump(abs((0.1 + 0.2) - 0.3) < PHP_FLOAT_EPSILON); // bool(true)
var_dump(floor((0.1 + 0.7) * 10)); // float(7) -- not 8
var_dump(NAN == NAN, is_nan(NAN)); // bool(false) bool(true)
var_dump(1e308 * 10); // float(INF)
// 8.4: fdiv() and fpow() follow IEEE 754 instead of throwing
var_dump(fdiv(1, 0)); // float(INF)
Never use floats for money: store integer minor units (cents) or use BcMath\Number. Never compare floats with ==;
compare against a tolerance, or round first.
Source: Floating point numbers
string
A string is a sequence of bytes, not characters: PHP strings have no encoding of their own. strlen() counts
bytes, so strlen('Lem') is 3 but strlen('Łódź') is 7 in UTF-8; the mbstring and intl extensions work with
characters (see Unicode, mbstring and intl). Strings
can be written with single quotes, double quotes (with escape sequences and interpolation), heredoc and nowdoc; they
are mutable byte arrays accessible by offset. All of this is covered in
Strings.
Source: Strings
Numeric strings
A numeric string is a string that looks like a number: optional leading whitespace, an optional sign, digits with an
optional decimal part and exponent, and optional trailing whitespace (allowed since PHP 8.0). A leading-numeric
string starts like a number and continues with other characters ("12 books"); any other string is
non-numeric. The distinction drives arithmetic and comparisons:
<?php
declare(strict_types=0); // these rules apply in coercive mode
var_dump(is_numeric('12'), is_numeric(' 12 '), is_numeric('1e3'), is_numeric('12 books'), is_numeric('0x1A'));
// bool(true) bool(true) bool(true) bool(false) bool(false)
var_dump('12' + 1); // int(13) numeric string
var_dump(' 1.5 ' * 2); // float(3) whitespace allowed on both sides
var_dump('12 books' + 1); // int(13) leading-numeric: works, with a warning
try {
var_dump('books' + 1); // non-numeric: TypeError since 8.0
} catch (TypeError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
int(13)
float(3)
Warning: A non-numeric value encountered in ... on line 10
int(13)
TypeError: Unsupported operand types: string + int
Before PHP 8.0, arithmetic on a non-numeric string silently produced 0 with a warning, and leading-numeric strings
produced only a notice; books written for PHP 7 show that older behaviour.
Source: Numeric strings
array
A PHP array is an ordered map: an ordered collection of key/value pairs, where keys are int or string and
values can be anything. The same type serves as list, dictionary, set, stack and queue. array_is_list() (8.1)
checks whether the keys are exactly 0, 1, 2, …. Arrays are value types with copy-on-write semantics. Key casting rules,
destructuring and the array API are covered in Arrays.
<?php
declare(strict_types=1);
$titles = ['Kindred', 'Solaris']; // a list
$byIsbn = ['978-0-00-000001-1' => 'Kindred']; // a map
var_dump(array_is_list($titles), array_is_list($byIsbn)); // bool(true) bool(false)
Native declarations can only say array; the shape of its keys and values (list<string>,
array<string, Book>, array{title: string, year: int}) is expressed in PHPDoc for static analysers.
Source: Arrays and array_is_list
object
An object is an instance of a class, created with new (or by cloning, unserialising, casting an array to
object, which yields a stdClass, and so on). The object type accepts any object; a class, interface or enum name
accepts instances of that type and its subtypes. Objects are handled by reference-like handles: assigning or passing
an object does not copy it. See Classes and Objects.
<?php
declare(strict_types=1);
function describe(object $thing): string
{
return $thing::class; // ::class on an object, PHP 8.0
}
echo describe(new ArrayObject()), "\n"; // ArrayObject
echo describe((object) ['title' => 'Kindred']), "\n"; // stdClass
Source: Objects
Enumerations
An enum (8.1) declares a closed set of named values, each case being a singleton object of the enum’s type. Enums
are class-types and can be used in declarations: function lend(Format $format) accepts only Format cases. See
Enumerations.
Source: Enumerations (type overview)
resource
A resource is a handle to an external resource — an open file or stream, a process, a socket — managed by an
extension. It cannot be used in declarations. Most extensions have replaced their resources with proper objects
(CurlHandle and GdImage in 8.0, finfo in 8.1, PgSql\Connection in 8.1, …), and the remaining ones (mainly
streams from fopen() and proc_open()) are expected to follow. Resources are freed automatically when nothing
references them, so fclose() is a matter of releasing a lock or a file descriptor early, not of preventing leaks.
<?php
declare(strict_types=1);
$stream = fopen('php://memory', 'w+');
var_dump(is_resource($stream), get_resource_type($stream)); // bool(true) string(6) "stream"
fclose($stream);
var_dump(is_resource($stream), get_debug_type($stream)); // bool(false) string(17) "resource (closed)"
$curl = curl_init();
var_dump($curl instanceof CurlHandle); // bool(true) -- an object since PHP 8.0
Source: Resources and List of Resource Types
callable
callable accepts anything that can be called: a Closure (including arrow functions and first-class callables such
as strlen(...)), an object with __invoke(), a function name as a string, [$object, 'method'] or
[ClassName::class, 'staticMethod']. It can be used for parameters and return types but not for properties, and it
cannot describe a signature. In new code prefer the Closure type and first-class callable syntax; see
Closures and Callables.
<?php
declare(strict_types=1);
function apply(callable $fn, string $value): string
{
return $fn($value);
}
final class Shout { public function __invoke(string $s): string { return strtoupper($s); } }
echo apply('ucfirst', 'kindred'), "\n"; // Kindred
echo apply(strrev(...), 'kindred'), "\n"; // derdnik
echo apply(new Shout(), 'kindred'), "\n"; // KINDRED
echo apply(fn (string $s): string => "«{$s}»", 'kindred'), "\n"; // «kindred»
Source: Callbacks / Callables
iterable and mixed
iterable (7.1) is an alias for Traversable|array: anything foreach can loop over. It is often the best parameter
type for functions that only iterate, because it accepts generators as well as arrays.
mixed (8.0) accepts any value, including null; it is the top type. Writing mixed is different from writing
nothing: it documents that any value is genuinely allowed. mixed cannot be part of a union (mixed|null is an
error) and cannot be nullable.
<?php
declare(strict_types=1);
function countItems(iterable $items): int
{
$n = 0;
foreach ($items as $_) { $n++; }
return $n;
}
function generateIsbns(): Generator { yield '978-0-00-000001-1'; yield '978-0-00-000002-8'; }
var_dump(countItems(['a', 'b', 'c'])); // int(3)
var_dump(countItems(generateIsbns())); // int(2)
function dump(mixed $value): void { echo get_debug_type($value), "\n"; }
dump(null); // null
void and never
void (7.1) is a return-only type meaning "returns no value": the function may use return; but not return $x;,
and its call evaluates to null. never (8.1) is a return-only type meaning "never returns": the function always
throws, calls exit() or loops forever. never is the bottom type: it is a subtype of every type, so a child method
may narrow any return type to never.
<?php
declare(strict_types=1);
function logLoan(string $isbn): void
{
if ($isbn === '') {
return; // allowed: no value
}
echo "Loaned {$isbn}\n";
}
function fail(string $message): never
{
throw new RuntimeException($message);
}
function titleOf(array $book): string
{
return $book['title'] ?? fail('Book without a title'); // fail() satisfies the string return type
}
logLoan('978-0-00-000001-1');
echo titleOf(['title' => 'Kindred']), "\n";
try {
titleOf([]);
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n"; // Book without a title
}
Since PHP 8.5, a void function’s result can be explicitly discarded with the (void) cast, which pairs with the
#[\NoDiscard] attribute described in Functions.
self, parent and static
The relative class types refer to classes relative to where they are written: self is the class in which the
method is declared, parent its parent class, and static (return type only, 8.0) the class on which the method was
actually called, which is what fluent interfaces and named constructors need with inheritance (late static binding,
see Cloning, Late Static Binding
and Object Lifecycle).
<?php
declare(strict_types=1);
class Query
{
private array $filters = [];
public static function create(): static { return new static(); }
public function where(string $filter): static
{
$clone = clone $this;
$clone->filters[] = $filter;
return $clone;
}
public function filters(): array { return $this->filters; }
}
final class BookQuery extends Query {}
$q = BookQuery::create()->where('year < 1970');
echo $q::class, ' ', implode(', ', $q->filters()), "\n"; // BookQuery year < 1970
Source: Relative class types
true and false
The singleton types false (in unions since 8.0, standalone since 8.2) and true (8.2) accept only that literal.
They exist mainly to describe internal functions precisely (strpos() returns int|false) and to let subclasses
narrow bool returns. true|false is a compile-time error: write bool.
<?php
declare(strict_types=1);
function firstPosition(string $haystack, string $needle): int|false
{
return strpos($haystack, $needle);
}
var_dump(firstPosition('Solaris', 'S')); // int(0) -- 0, not false!
var_dump(firstPosition('Solaris', 'x')); // bool(false)
Source: Singleton types
Composite types
Union types (8.0)
A union type T|U|V accepts a value of any of its member types. ?T is shorthand for T|null and can only be used
with a single type (?int|string is invalid; write int|string|null).
<?php
declare(strict_types=1);
function shelfLabel(int|string $id): string
{
return is_int($id) ? sprintf('#%05d', $id) : strtoupper($id);
}
echo shelfLabel(42), ' ', shelfLabel('sf-07'), "\n"; // #00042 SF-07
Source: Union types
Intersection types (8.1)
An intersection type T&U accepts only values that are instances of all the listed class-types. It is useful
when a function needs an object that implements several interfaces. Only class-types can be intersected (int&string
or self&Countable are errors).
<?php
declare(strict_types=1);
function summarise(Countable&Traversable $collection): string
{
$first = null;
foreach ($collection as $item) { $first = $item; break; }
return count($collection) . " items, first: {$first}";
}
echo summarise(new ArrayIterator(['Kindred', 'Solaris'])), "\n"; // 2 items, first: Kindred
Source: Intersection types
DNF types (8.2)
Disjunctive normal form combines both: a union of intersections, where each intersection is parenthesised:
(Countable&Traversable)|null or (A&B)|(C&D)|array. Every combination of union and intersection must be written in
this normal form.
<?php
declare(strict_types=1);
function maybeCount((Countable&Traversable)|array|null $items): int
{
return $items === null ? 0 : count($items);
}
var_dump(maybeCount(null), maybeCount([1, 2]), maybeCount(new ArrayObject([1, 2, 3]))); // int(0) int(2) int(3)
Source: Composite types
Type declarations
Types can be declared on function and method parameters and return values (7.0, nullable 7.1), on properties
(7.4) and on class constants (8.3). Every type except resource can be declared; void, never and static are
return-only, and callable is not allowed for properties.
<?php
declare(strict_types=1);
final class Catalogue
{
public const string NAME = 'City Library'; // 8.3: typed class constant
/** @var array<string, string> isbn => title */
private array $titles = []; // 7.4: typed property
public ?string $lastAdded = null; // nullable property, explicitly initialised
public function add(string $isbn, string $title): static // parameter and return types
{
$this->titles[$isbn] = $title;
$this->lastAdded = $isbn;
return $this;
}
public function find(string $isbn): ?string
{
return $this->titles[$isbn] ?? null;
}
}
$c = (new Catalogue())->add('978-0-00-000001-1', 'Kindred');
var_dump($c->find('978-0-00-000001-1'), $c->find('nope'), Catalogue::NAME);
Source: Type declarations
Typed properties and the uninitialised state
A typed property without a default value starts uninitialised: reading it before assigning throws an Error, and
isset() returns false. This is different from null and is what lets a non-nullable property exist before a
constructor runs. Untyped properties default to null.
<?php
declare(strict_types=1);
final class Draft
{
public string $title; // no default: uninitialised
public $notes; // untyped: null
}
$d = new Draft();
var_dump(isset($d->title), $d->notes); // bool(false) NULL
try {
echo $d->title;
} catch (Error $e) {
echo $e->getMessage(), "\n";
// Typed property Draft::$title must not be accessed before initialization
}
Source: Typed properties
Nullable types and the implicitly nullable parameter deprecation
Before nullable types existed, the idiom for an optional object parameter was a null default: function
lend(Book $book = null). The = null default implicitly made the type nullable. PHP 8.4 deprecated this
implicit nullability, because the type declaration no longer tells the truth; declare it explicitly:
<?php
declare(strict_types=1);
final class Member {}
// Deprecated since 8.4: "Implicitly marking parameter $member as nullable is deprecated"
// function lend(string $isbn, Member $member = null) {}
// Correct: explicit nullable type (works since 7.1)
function lend(string $isbn, ?Member $member = null): string
{
return $member === null ? "{$isbn} reserved" : "{$isbn} lent";
}
echo lend('978-0-00-000001-1'), "\n"; // 978-0-00-000001-1 reserved
echo lend('978-0-00-000001-1', new Member()), "\n"; // 978-0-00-000001-1 lent
Rector and PHP-CS-Fixer can make this change across a code base automatically (Static Analysis and Refactoring).
Type declarations and references
If a by-reference parameter has a type, the type is checked only when the function is entered, not when it returns, so a function can change the type of the caller’s variable. Typed properties, on the other hand, keep their type even through references: assigning a wrong type through a reference to a typed property throws.
Coercive and strict typing: declare(strict_types=1)
By default, PHP runs scalar type declarations in coercive mode: if a function expecting int receives the string
"5" or the float 5.0, the value is converted (when the conversion is lossless and numeric). With
declare(strict_types=1); as the very first statement of a file, calls made from that file to functions with scalar
parameters, and return statements in that file, accept only the exact type. The single exception is that an
int is accepted where a float is expected (widening).
<?php
declare(strict_types=1);
function addCopies(int $copies): int { return $copies + 1; }
function average(float $total, int $count): float { return $total / $count; }
var_dump(average(10, 4)); // float(2.5) -- int to float widening is allowed
try {
addCopies('5'); // TypeError in strict mode
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
float(2.5)
addCopies(): Argument #1 ($copies) must be of type int, string given, called in ... on line 11
Key points:
-
The mode is decided by the calling file, not the file that declares the function. A library written in strict mode is still called coercively from a file without the declaration.
-
strict_typesaffects scalar parameter, return and property type checks only. Operators (==,+,.) keep their usual juggling rules. -
Internal functions follow the caller’s mode too:
strlen(42)works in coercive mode but throws in strict mode. -
Typed property assignments follow the mode of the file performing the assignment.
Almost every modern code base, framework and coding standard (PER Coding Style, the Laravel skeleton’s tooling,
Symfony) writes declare(strict_types=1); in every file. This section does the same.
Source: Strict typing
What coercive mode does
In coercive mode, the conversions are: int to float; float to int (a float with a fractional part, or a string such as "5.5", is still truncated, but since 8.1 this
emits a deprecation: "Implicit conversion from float 5.5 to int loses precision"); numeric strings to int/float; int, float and Stringable objects to string; and scalars to bool. For union types, PHP tries
int, float, string, bool in that order, picking the first that the value can be converted to without loss.
null is never coerced for user-defined functions; passing null to a non-nullable parameter of an internal
function has been deprecated since 8.1.
<?php
declare(strict_types=0); // coercive mode (the default when the declare is absent)
function copies(int $n): int { return $n; }
var_dump(copies('12')); // int(12)
var_dump(copies(12.0)); // int(12)
var_dump(copies(true)); // int(1)
try {
copies('12 books'); // leading-numeric strings are rejected for parameters
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
Variance
When a child class overrides a method, its parameter types may be wider (contravariance) and its return type
narrower (covariance) than the parent’s, and property types must stay the same (invariance) unless hooks make a
property read-only or write-only. These rules, which make never and mixed useful as the bottom and top of the
lattice, are detailed in
Inheritance, Interfaces and Abstract
Classes.
Source: Covariance and Contravariance
Common pitfalls
-
Scalar type aliases are not types.
boolean,integer,doublein a declaration are treated as class names; PHP warns ("did you mean bool?") and then throwsTypeErrorfor every call. -
?intversusint = null. The second form is deprecated since 8.4; and?intwithout a default still makes the parameter required. -
Floats and
intparameters. In coercive mode5.0is accepted silently forint, while5.5is truncated to5with a deprecation notice (8.1): the outcome depends on the value, not the type. Strict mode rejects both. -
strict_typesmust come first. It must be the first statement in the file (onlydeclare(ticks=…)or encoding may accompany it), otherwise it is a compile-time error. -
Arrays and PHPDoc.
arrayalone says nothing about contents; add@param list<Book>so static analysis can check callers.
Where older books and tutorials are outdated
The books consulted for this section cover PHP 7.0 to 7.3, so they lack everything typed after that:
| Feature | Since | What older material says or omits |
|---|---|---|
Typed properties |
7.4 |
Books declare untyped properties and validate in setters. |
Union types, |
8.0 |
Expressed only in PHPDoc. |
Saner numeric strings; |
8.0 |
Books show |
Intersection types, |
8.1 |
Absent. |
DNF types, standalone |
8.2 |
Absent. |
Typed class constants |
8.3 |
Constants are always untyped in the books. |
Implicitly nullable parameters deprecated |
8.4 |
Books recommend |
|
8.5 |
Absent. |
References
-
NULL, Booleans, Integers, Floating point numbers, Strings, Numeric strings, Arrays, Objects, Enumerations, Resources, Callables, Iterables, mixed, void, never, Relative class types, Singleton types
-
PHP Manual — Properties (typed properties)