Объявления типов (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,
);
}
}