HardТеория11 min

Объявления типов

strict_types, coercion mode, variance rules, union/intersection/DNF types, полная таблица

Объявления типов (Type Declarations)

declare(strict_types=1)

Директива strict_types управляет режимом проверки типов при вызове функций.

<?php
// strict_types=1 — STRICT mode
declare(strict_types=1);

function multiply(int $a, int $b): int
{
    return $a * $b;
}

echo multiply(6, 7);      // 42 — OK
// echo multiply(6.5, 7);  // TypeError! float given, int expected
// echo multiply('6', 7);  // TypeError! string given, int expected
<?php
// Without strict_types (default) — COERCIVE mode
// declare(strict_types=0);  // default, can be omitted

function multiply(int $a, int $b): int
{
    return $a * $b;
}

echo multiply(6, 7);      // 42
echo multiply(6.0, 7);    // 42 — float silently truncated to int
echo multiply('6', 7);    // 42 — string silently converted to int
echo multiply(6.9, 7);    // 42 — truncated! not rounded!
// echo multiply('abc', 7); // TypeError — non-numeric string

Правила strict_types

<?php
declare(strict_types=1);

// Rule 1: strict_types affects the CALLING file, not the defining file
// If file A (strict) calls function from file B (non-strict) —
// strict rules apply because the CALL is in strict file

// Rule 2: strict_types affects function calls and return values ONLY
$a = '42' + 0;      // int(42) — arithmetic operators ALWAYS juggle
$b = '42' == 42;    // true    — comparison operators ALWAYS juggle

// Rule 3: strict_types must be the FIRST statement in file
// It cannot be inside functions or conditionally declared

// Rule 4: Some functions are not affected
var_dump((int) '42.5');     // int(42) — cast always works
var_dump(intval('42.5'));   // int(42) — intval always works
var_dump(settype($x, 'int')); // true — settype always works

// Rule 5: Internal functions in coercive mode
// Built-in functions follow coercive rules regardless of strict_types
// since PHP 8.0 (was different in PHP 7.x)

Ловушка экзамена: strict_types действует на месте ВЫЗОВА, а не определения. Если файл A (strict) вызывает функцию из файла B (не-strict) -- применяются strict правила, потому что вызов происходит в strict-файле.

Coercion Mode (режим приведения)

Без strict_types=1 PHP автоматически приводит значения к ожидаемому типу.

Таблица приведений в coercive mode

<?php
// Coercive mode — automatic conversions

function takeInt(int $x): void { var_dump($x); }
function takeFloat(float $x): void { var_dump($x); }
function takeString(string $x): void { var_dump($x); }
function takeBool(bool $x): void { var_dump($x); }

// → int parameter
takeInt(42);       // int(42) — exact
takeInt(42.0);     // int(42) — float without fraction → int
// takeInt(42.5);  // Deprecated in 8.1: implicit float-to-int loses precision
takeInt('42');     // int(42) — numeric string → int
takeInt(true);     // int(1)  — bool → int
takeInt(false);    // int(0)  — bool → int
// takeInt(null);  // TypeError in 8.1+ (deprecated conversion)
// takeInt('abc'); // TypeError — non-numeric string

// → float parameter
takeFloat(42);      // float(42.0) — int → float (always safe)
takeFloat(42.5);    // float(42.5)
takeFloat('3.14');  // float(3.14) — numeric string
takeFloat(true);    // float(1.0)
takeFloat(false);   // float(0.0)

// → string parameter
takeString('hello'); // string(5) "hello"
takeString(42);      // string(2) "42"
takeString(3.14);    // string(4) "3.14"
takeString(true);    // string(1) "1"
takeString(false);   // string(0) ""
// takeString(null);  // Deprecated in 8.1+
// takeString([]);    // TypeError

// → bool parameter
takeBool(true);     // bool(true)
takeBool(false);    // bool(false)
takeBool(1);        // bool(true)
takeBool(0);        // bool(false)
takeBool('');       // bool(false)
takeBool('0');      // bool(false)
takeBool('hello');  // bool(true)

Изменения в PHP 8.1: Implicit Conversion Deprecation

<?php
// PHP 8.1+ deprecated implicit narrowing conversions

function takeInt(int $x): void {}

// These are DEPRECATED in PHP 8.1 (will be errors in PHP 9.0):
// takeInt(42.5);   // Deprecated: losing precision (float→int with fraction)
// takeInt(null);   // Deprecated: null→int
// takeInt('');     // Deprecated: ''→int

// These are STILL OK:
takeInt(42.0);     // OK — no precision loss (42.0 → 42)
takeInt(true);     // OK — explicit bool→int mapping

Скалярные типы

<?php
declare(strict_types=1);

// PHP 7.0+ scalar type declarations
function example(
    int $integer,
    float $floating,
    string $text,
    bool $flag,
): void {
    // All four scalar types available since PHP 7.0
}

// int — whole numbers
function factorial(int $n): int
{
    return $n <= 1 ? 1 : $n * factorial($n - 1);
}

// float — accepts int too (int is "subtype" of float)
function area(float $radius): float
{
    return M_PI * $radius ** 2;
}
echo area(5);    // Works! int 5 accepted as float
echo area(5.0);  // Works! float as expected

// string
function greet(string $name): string
{
    return "Hello, {$name}!";
}

// bool
function toggle(bool $current): bool
{
    return !$current;
}

Важно: int автоматически расширяется до float даже в strict mode. Это единственное допустимое неявное преобразование: area(5) работает, потому что int безопасно расширяется до float.

Nullable Types

<?php
declare(strict_types=1);

// Nullable with ? prefix (PHP 7.1+)
function findUser(int $id): ?string
{
    return $id > 0 ? 'Alice' : null;
}

// Nullable with union (PHP 8.0+)
function findOrder(int $id): string|null
{
    return $id > 0 ? 'Order #' . $id : null;
}

// ?type is syntactic sugar for type|null
// These are identical:
function a(?string $x): void {}
function b(string|null $x): void {}

// Nullable property
class User
{
    public ?string $email = null;  // Can be string or null
}

// Nullable with default value
function connect(?string $host = null): void
{
    $host ??= 'localhost';  // Null coalescing assignment
    echo "Connecting to {$host}\n";
}

connect();         // Connecting to localhost
connect(null);     // Connecting to localhost
connect('db.io');  // Connecting to db.io

// IMPORTANT: ?type and type|null differ only in style
// But in union with other types, use explicit null:
function process(int|string|null $value): void {}
// NOT: function process(?int|string $value)  // Parse error!

Typed Properties (PHP 7.4+)

<?php
declare(strict_types=1);

class Product
{
    // Typed properties
    public int $id;
    public string $name;
    public float $price;
    public bool $active = true;
    public ?string $description = null;
    public array $tags = [];

    // Uninitialized typed property
    public string $sku;

    public function __construct(int $id, string $name, float $price)
    {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
        // $sku is left uninitialized
    }
}

$product = new Product(1, 'Widget', 9.99);
echo $product->name;   // 'Widget'
echo $product->active;  // true

// Accessing uninitialized property:
// echo $product->sku;  // Error: Typed property must not be accessed before initialization

// Check if initialized
$ref = new ReflectionProperty(Product::class, 'sku');
var_dump($ref->isInitialized($product));  // false

// Default values for typed properties
class Config
{
    public int $port = 8080;
    public string $host = 'localhost';
    public bool $debug = false;
    // public int $count = null;  // Error! Default must match type
    public ?int $count = null;    // OK — nullable with null default
}

Union Types (PHP 8.0+)

<?php
declare(strict_types=1);

// Union of scalar types
function formatId(int|string $id): string
{
    return match (true) {
        is_int($id) => sprintf('%06d', $id),
        is_string($id) => $id,
    };
}

echo formatId(42);       // '000042'
echo formatId('ABC-1');  // 'ABC-1'

// Union with classes
function render(string|Stringable $content): string
{
    return (string) $content;
}

// Union in properties
class Setting
{
    public int|float|string|bool $value;
}

// Union restrictions:
// - void CANNOT be in union (it means "nothing")
// - never CANNOT be in union
// - mixed CANNOT be in union (it's already everything)
// - No duplicate types: int|int is error
// - No redundant types: int|?int is error (? already includes null)
// - false was the ONLY literal in PHP 8.0 union; true added in 8.2

// Common patterns
function divide(int|float $a, int|float $b): int|float
{
    if ($b == 0) {
        throw new DivisionByZeroError('Division by zero');
    }
    $result = $a / $b;
    return is_int($result) ? $result : (float) $result;
}

Intersection Types (PHP 8.1+)

<?php
declare(strict_types=1);

interface Cacheable
{
    public function getCacheKey(): string;
    public function getCacheTtl(): int;
}

interface Serializable
{
    public function serialize(): string;
    public function unserialize(string $data): void;
}

interface Loggable
{
    public function toLogString(): string;
}

// Must implement ALL interfaces
function cache(Cacheable&Serializable $item): void
{
    $key = $item->getCacheKey();
    $data = $item->serialize();
    // Store in cache...
}

// Triple intersection
function process(Cacheable&Serializable&Loggable $item): void
{
    echo $item->toLogString();
    cache($item);  // Works — Cacheable&Serializable&Loggable satisfies Cacheable&Serializable
}

// Intersection ONLY works with:
// - Interfaces
// - Classes (but limited, since PHP has single inheritance)
// NOT with: int, string, bool, float, array, null, void, etc.

class CachedEntity implements Cacheable, Serializable, Loggable
{
    public function getCacheKey(): string { return 'entity'; }
    public function getCacheTtl(): int { return 3600; }
    public function serialize(): string { return '{}'; }
    public function unserialize(string $data): void {}
    public function toLogString(): string { return 'CachedEntity'; }
}

process(new CachedEntity());  // OK

DNF Types (PHP 8.2+)

<?php
declare(strict_types=1);

// DNF = Disjunctive Normal Form
// Combines intersection (AND) with union (OR)

// (A&B)|C — either (A AND B) or C
function store((Cacheable&Serializable)|null $item): void
{
    if ($item === null) {
        return;
    }
    $key = $item->getCacheKey();
    $data = $item->serialize();
}

// Complex DNF
function process(
    (Cacheable&Loggable)|(Serializable&Loggable)|null $item,
): string {
    if ($item === null) {
        return 'null';
    }
    return $item->toLogString();  // Loggable is in both intersections
}

// DNF in properties
class Service
{
    public (Iterator&Countable)|array|null $data = null;
}

// DNF rules:
// 1. Each "group" must be a valid intersection in parentheses
// 2. Groups are combined with |
// 3. null/false/true can only be at top level, not inside ()
// 4. Scalar types cannot be inside () intersection

// Valid:
function a((A&B)|null $x): void {}
function b((A&B)|(C&D) $x): void {}
function c((A&B)|string $x): void {}

// Invalid:
// function d(A&(B|C) $x): void {}      // Not DNF form
// function e((A&B|C) $x): void {}      // Missing inner parentheses
// function f((int&string) $x): void {} // Scalars in intersection

Standalone Types (PHP 8.2+)

<?php
declare(strict_types=1);

// null as standalone type
function reset(): null
{
    return null;  // Can ONLY return null
}

// true as standalone type
function ping(): true
{
    return true;  // Can ONLY return true
}

// false as standalone type
function disconnect(): false
{
    return false;
}

// Practical use: functions that only return true or throw
function ensureValid(mixed $data): true
{
    if (!is_array($data)) {
        throw new InvalidArgumentException('Array expected');
    }
    if (empty($data)) {
        throw new InvalidArgumentException('Cannot be empty');
    }
    return true;
}

// In legacy code wrapping
function legacyOperation(): int|false
{
    // Many PHP built-in functions return false on failure
    $result = @file_get_contents('/nonexistent');
    return $result !== false ? strlen($result) : false;
}

Variance Rules (Ковариантность и контравариантность)

Return Type Covariance (ковариантность)

<?php
declare(strict_types=1);

// Return types are COVARIANT — child can return MORE SPECIFIC type

class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {}

interface AnimalFactory
{
    public function create(): Animal;
}

class DogFactory implements AnimalFactory
{
    public function create(): Dog  // More specific — OK
    {
        return new Dog();
    }
}

class PuppyFactory extends DogFactory
{
    public function create(): Puppy  // Even more specific — OK
    {
        return new Puppy();
    }
}

// With union types
interface Processor
{
    public function run(): int|string;
}

class IntProcessor implements Processor
{
    public function run(): int  // Narrower union — OK
    {
        return 42;
    }
}

Parameter Type Contravariance (контравариантность)

<?php
declare(strict_types=1);

// Parameters are CONTRAVARIANT — child can accept MORE GENERAL type

interface DogHandler
{
    public function handle(Dog $dog): void;
}

class AnimalHandler implements DogHandler
{
    public function handle(Animal $animal): void  // More general — OK
    {
        // Can handle any Animal, including Dog
    }
}

// With union types
interface NumberProcessor
{
    public function process(int $value): void;
}

class FlexibleProcessor implements NumberProcessor
{
    public function process(int|string $value): void  // Wider — OK
    {
        // Accepts more types than parent requires
    }
}

Полная таблица правил variance

<?php
declare(strict_types=1);

// RETURN TYPE RULES (covariant — must be same or more specific):
// Parent: Animal    → Child: Animal ✓, Dog ✓, Puppy ✓
// Parent: ?Animal   → Child: ?Animal ✓, Animal ✓, Dog ✓
// Parent: int|string → Child: int|string ✓, int ✓, string ✓
// Parent: mixed     → Child: mixed ✓, int ✓, string ✓, anything ✓
// Parent: void      → Child: void ✓ (only void)
// Parent: (none)    → Child: anything ✓ (adding return type is OK)

// PARAMETER TYPE RULES (contravariant — must be same or more general):
// Parent: Dog       → Child: Dog ✓, Animal ✓
// Parent: int       → Child: int ✓, int|string ✓, mixed ✓
// Parent: ?int      → Child: ?int ✓, mixed ✓, int|string|null ✓
// Parent: int|string → Child: int|string ✓, int|string|float ✓, mixed ✓

// PROPERTY TYPES — INVARIANT (must be exact same type):
// Because properties can be both read (covariant) and written (contravariant),
// they must be invariant

class ParentClass
{
    public int $value;
}

class ChildClass extends ParentClass
{
    // public string $value;  // Fatal error! Must be int
    // public int|string $value;  // Fatal error!
    public int $value;  // Must be exactly int — OK
}

Weak/Strict Comparison Table

<?php
declare(strict_types=1);

// == (loose) vs === (strict) comparison table for PHP 8.0+

// |         | true  | false | 0     | 1     | ""    | "0"   | null  | "php" |
// |---------|-------|-------|-------|-------|-------|-------|-------|-------|
// | true    | true  | false | false | true  | false | false | false | true  |
// | false   | false | true  | true  | false | true  | true  | true  | false |
// | 0       | false | true  | true  | false | false*| true  | true  | false*|
// | 1       | true  | false | false | true  | false | false | false | false |
// | ""      | false | true  | false*| false | true  | false | true  | false |
// | "0"     | false | true  | true  | false | false | true  | false | false |
// | null    | false | true  | true  | false | true  | false | true  | false |
// | "php"   | true  | false | false*| false | false | false | false | true  |
//
// * = changed in PHP 8.0 (was true in PHP 7.x)

// Key PHP 8.0 changes:
var_dump(0 == "");      // false (was true)
var_dump(0 == "php");   // false (was true)
var_dump("" == 0);      // false (was true)

// Still true in PHP 8.0:
var_dump(0 == false);   // true
var_dump(0 == null);    // true
var_dump("" == null);   // true
var_dump("" == false);  // true
var_dump("0" == false); // true
var_dump("0" == 0);     // true

Практические паттерны

Value Object с полной типизацией

<?php
declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amount,         // In cents
        public string $currency,
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
        if (!preg_match('/^[A-Z]{3}$/', $currency)) {
            throw new InvalidArgumentException("Invalid currency: {$currency}");
        }
    }

    public function add(self $other): self
    {
        $this->ensureSameCurrency($other);
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function subtract(self $other): self
    {
        $this->ensureSameCurrency($other);
        $result = $this->amount - $other->amount;
        if ($result < 0) {
            throw new UnderflowException('Insufficient funds');
        }
        return new self($result, $this->currency);
    }

    public function multiply(int|float $factor): self
    {
        return new self((int) round($this->amount * $factor), $this->currency);
    }

    public function format(): string
    {
        return number_format($this->amount / 100, 2, '.', ',') . ' ' . $this->currency;
    }

    private function ensureSameCurrency(self $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException(
                "Cannot operate on {$this->currency} and {$other->currency}"
            );
        }
    }
}

$price = new Money(1999, 'USD');        // $19.99
$tax = new Money(160, 'USD');           // $1.60
$total = $price->add($tax);            // $21.59
echo $total->format();                  // '21.59 USD'

Type-safe DTO с PHP 8.4

<?php
declare(strict_types=1);

final readonly class CreateUserDto
{
    /**
     * @param list<string> $roles
     */
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
        public array $roles = ['user'],
        public ?string $avatar = null,
    ) {
        if ($this->age < 0 || $this->age > 150) {
            throw new InvalidArgumentException("Invalid age: {$this->age}");
        }
        if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: {$this->email}");
        }
    }

    /**
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        return new self(
            name: (string) ($data['name'] ?? ''),
            email: (string) ($data['email'] ?? ''),
            age: (int) ($data['age'] ?? 0),
            roles: (array) ($data['roles'] ?? ['user']),
            avatar: isset($data['avatar']) ? (string) $data['avatar'] : null,
        );
    }
}

Проверь себя

5 из 12

Дочерний класс принимает `int|string` вместо `int`. Это допустимо для параметра?

Можно ли передать `int` в функцию ожидающую `float` в strict mode?

Какое неявное преобразование deprecated в PHP 8.1 для coercive mode?

Можно ли использовать `void` в union типе?

На что влияет `declare(strict_types=1)`?