Skip to content

PHP Type Safety

PHP has evolved significantly in terms of type safety, offering developers tools to write more robust and maintainable code. This guide covers type hinting, strict typing, and best practices for implementing type safety in PHP.

PHP by default performs type coercion (automatic type conversion) which can lead to unexpected behavior and hard-to-debug issues:

<?php
// Without strict types, PHP performs automatic type conversion
function calculateTotal(int $price, int $quantity): int {
return $price * $quantity;
}
// These all "work" but may not behave as expected
calculateTotal("10", "5"); // Returns 50 (strings converted to integers)
calculateTotal(10.7, 5.3); // Returns 56 (floats truncated to integers)
calculateTotal(true, false); // Returns 0 (booleans converted: true=1, false=0)
calculateTotal("10abc", "5"); // Returns 50 (string "10abc" converted to 10)
calculateTotal(null, 5); // Returns 0 (null converted to 0)

These automatic conversions can cause:

  • Silent data corruption: "10.5" becomes 10, losing precision
  • Logic errors: true becomes 1, affecting calculations
  • Security vulnerabilities: Unexpected type conversions in validation
  • Debugging nightmares: Issues may only surface in production with specific data

With strict types enabled, PHP enforces exact type matching:

<?php
declare(strict_types=1);
function calculateTotal(int $price, int $quantity): int {
return $price * $quantity;
}
// This will throw a TypeError in strict mode
calculateTotal("10", "5"); // TypeError: Argument 1 must be of type int, string given
calculateTotal(10.7, 5.3); // TypeError: Argument 1 must be of type int, float given

  • Scalar types are the basic data types in PHP: string, int, float, and bool.
  • Type hinting these parameters ensures your functions receive exactly the data types they expect, preventing unexpected behavior from automatic type conversion.
  • PHP supports type hints for scalar types since PHP 7.0:
<?php
declare(strict_types=1);
function processData( string $name, int $age, float $salary, bool $isActive ): array {
return [
'name' => $name,
'age' => $age,
'salary' => $salary,
'active' => $isActive
];
}
// Usage
$result = processData("John Doe", 30, 50000.50, true);

<?php
declare(strict_types=1);
function greetUser(?string $name): string {
if ($name === null) {
return "Hello, Guest!";
}
return "Hello, {$name}!";
}
// Both are valid
greetUser("Alice"); // "Hello, Alice!"
greetUser(null); // "Hello, Guest!"

Return type declarations specify what type of data a function will return. This helps both developers and IDEs understand function behavior and catches return type mismatches early.

<?php
declare(strict_types=1);
function calculateTax(float $amount): float {
return $amount * 0.1;
}
function getUsers(): array {
return ['user1', 'user2', 'user3'];
}
function isValid(): bool {
return true;
}
function processOrder(): void {
// Function returns nothing
echo "Order processed";
}

Union types allow a parameter or return value to accept multiple specific types using the pipe (|) operator. This provides flexibility while maintaining type safety by explicitly defining which types are acceptable.

<?php
declare(strict_types=1);
function formatValue(int|float $value): string {
return number_format($value, 2);
}
function getId(): int|string {
// Can return either int or string
return rand(0, 1) ? 123 : "ABC123";
}

Following these practices will help you write more reliable and maintainable PHP code with proper type safety implementation.

  1. Always use declare(strict_types=1) in new projects, it catches bugs early
  2. Type all function parameters and return types; it serves as documentation
  3. Be explicit about nullable types when needed:
    function findUser(int $id): ?User {
    // Returns User object or null
    }
  4. Use union types (PHP 8+) for flexibility:
    function processId(int|string $id): string {
    return (string) $id;
    }

Class type hints ensure that function parameters are instances of specific classes or implement certain interfaces. This is crucial for object-oriented programming and dependency injection patterns.

<?php
declare(strict_types=1);
class User {
public function __construct(
private string $name,
private string $email
) {}
}
class UserService {
public function saveUser(User $user): bool {
// Save user logic
return true;
}
public function getUser(int $id): User {
return new User("John Doe", "john@example.com");
}
}

<?php
declare(strict_types=1);
interface PaymentProcessorInterface {
public function processPayment(float $amount): bool;
}
class StripeProcessor implements PaymentProcessorInterface {
public function processPayment(float $amount): bool {
// Stripe-specific logic
return true;
}
}
class PaymentService {
public function __construct(
private PaymentProcessorInterface $processor
) {}
public function charge(float $amount): bool {
return $this->processor->processPayment($amount);
}
}

The mixed type accepts values of any type. It’s equivalent to combining all possible types into one union type. Use mixed when a function legitimately needs to handle multiple different types, but be cautious as it reduces type safety benefits.

<?php
declare(strict_types=1);
function handleData(mixed $data): mixed {
if (is_string($data)) {
return strtoupper($data);
}
if (is_array($data)) {
return count($data);
}
return $data;
}

The never type indicates that a function will never return normally. It either throws an exception, calls exit(), or enters an infinite loop. This helps static analysis tools understand that code after such function calls is unreachable.

<?php
declare(strict_types=1);
function throwError(): never {
throw new Exception("Something went wrong");
}
function redirect(string $url): never {
header("Location: {$url}");
exit();
}

Intersection types require a value to satisfy all specified types simultaneously using the ampersand (&) operator. This is commonly used with interfaces where an object must implement multiple interfaces.

<?php
declare(strict_types=1);
interface Readable {
public function read(): string;
}
interface Writable {
public function write(string $data): void;
}
function processFile(Readable&Writable $file): void {
$content = $file->read();
$file->write(strtoupper($content));
}

Property type declarations ensure that class properties can only hold values of specific types. This prevents accidental assignment of wrong data types and makes your classes more predictable and secure.

<?php
declare(strict_types=1);
class Product {
public string $name;
public float $price;
public ?string $description = null;
public array $categories = [];
private int $id;
protected DateTime $createdAt;
public function __construct(string $name, float $price) {
$this->name = $name;
$this->price = $price;
$this->createdAt = new DateTime();
}
}

The readonly modifier makes a property read-only, meaning it can only be assigned a value once during initialization. This prevents accidental modification of properties after object creation.

This is particularly useful for immutable objects or security-critical data.

<?php
declare(strict_types=1);
class Order {
public function __construct(
public readonly int $id,
public readonly string $customerEmail,
public readonly float $total
) {}
}
$order = new Order(1, "customer@example.com", 99.99);
// $order->id = 2; // Error: Cannot modify readonly property

When working with strict types, it’s important to handle TypeError exceptions gracefully and provide meaningful error messages to help debug type-related issues.

<?php
declare(strict_types=1);
function safelyProcessNumber(int $number): string {
try {
return "Number: " . ($number * 2);
} catch (TypeError $e) {
return "Error: Invalid type provided";
}
}
// Custom type validation
function validateAndProcess(mixed $value): int {
if (!is_int($value)) {
throw new TypeError("Expected integer, got " . gettype($value));
}
return $value * 2;
}