How PHP Got Types
| Version | Added |
|---|---|
| 5.0 | Class type hints for parameters |
| 5.1 | array type hint |
| 7.0 | Scalar types, return types, strict_types |
| 7.1 | Nullable types (?T), void |
| 7.2 | object type |
| 7.4 | Typed properties |
| 8.0 | Union types, mixed, static return, false type |
| 8.1 | Intersection types, never, readonly properties |
| 8.2 | true/false/null standalone, DNF types, readonly classes |
| 8.3 | Typed class constants |
strict_types Declaration
<?php
declare(strict_types=1); // MUST be the first statement
function add(int $a, int $b): int {
return $a + $b;
}
add(1, 2); // 3
add("1", "2"); // TypeError - strings rejected
Without strict_types, PHP coerces: add("1", "2") would return 3. With strict mode, mismatches throw TypeError - bugs surface immediately.
The declaration affects only the file it appears in - specifically, calls FROM that file to typed functions. Add it to every new file you write. Tools like PHPStan can enforce its presence.
Scalar Type Declarations
| Type | Accepts |
|---|---|
int | Integers |
float | Floats (also accepts int) |
string | Strings |
bool | true / false |
array | Any array |
object | Any object |
iterable | array or Traversable |
callable | Function, closure, [obj, method] |
| ClassName / InterfaceName | Instances of those |
self | Same class |
static | Late-static-bound class |
parent | Parent class |
Nullable Types
<?php
// Two equivalent ways
function findUser(?int $id): ?User { /* ... */ }
function findUser(int|null $id): User|null { /* ... */ }
// Default null forces nullable
function f(int $x = null) {} // implicit: ?int (deprecated in 8.4 - use explicit)
function f(?int $x = null) {} // explicit - good
Union Types (PHP 8.0)
<?php
function format(int|string $value): string {
return (string) $value;
}
function read(string|resource $source): string {
if (is_string($source)) return file_get_contents($source);
return stream_get_contents($source);
}
// "string|false" is the idiomatic way to express "or failure"
function findEmail(int $id): string|false {
return $repo->getEmail($id) ?? false;
}
Intersection Types (PHP 8.1)
The parameter must satisfy all listed types:
<?php
// Must be both iterable AND countable
function process(Iterator&Countable $collection): void {
echo count($collection);
foreach ($collection as $item) { /* ... */ }
}
// Useful when you need multiple capabilities
function sign(MessageInterface&Stringable $message): string {
return hash("sha256", (string) $message);
}
Disjunctive Normal Form (DNF) Types (PHP 8.2)
Union of intersections - the missing piece for expressing complex constraints:
<?php
function handle((Iterator&Countable) | array $data): void {
// Accepts:
// - an object that is both Iterator AND Countable
// - OR a plain array
}
// Each intersection is parenthesised
function example(
(HasId&HasName) | (HasUuid&HasSlug) | null $entity
): void {}
Special Types
| Type | Meaning |
|---|---|
void | Function returns nothing (cannot return $value) |
never | Function never returns (throws or exits) |
mixed | Any value (use sparingly - usually means "I haven\'t thought about types") |
true / false / null | Literal types (8.2+) - e.g., string|false |
self | The current class |
static | Late-static-bound class (use as return type for fluent APIs) |
<?php
function abort(string $msg): never {
throw new RuntimeException($msg);
// No return possible
}
class QueryBuilder {
public function where(string $col, mixed $val): static {
// returning static lets subclasses chain correctly
return $this;
}
}
Typed Properties (PHP 7.4+)
<?php
class User {
public int $id;
public string $name;
public ?string $bio = null;
public DateTimeImmutable $createdAt;
}
$u = new User();
$u->name = "Ruban";
echo $u->createdAt->format("Y-m-d");
// Error: Typed property User::$createdAt must not be accessed before initialization
Typed properties are uninitialized until set - accessing one before assignment throws. Either provide a default, set in the constructor, or use nullable.
Variance Rules
When overriding methods, you can:
- Widen parameter types (contravariance) - accept more than the parent did
- Narrow return types (covariance) - return more specific than the parent
<?php
class Animal {}
class Dog extends Animal {}
class Pet {
public function adopt(Dog $d): Animal { /* ... */ }
}
class Shelter extends Pet {
// OK - param widened, return narrowed
public function adopt(Animal $a): Dog { /* ... */ }
}
// The reverse would be unsafe - callers expecting Pet contract would break