Урок 02

Типизация: аргументы и возвращаемые значения

Зачем типизация

Типы — контракт между функцией и вызывающим кодом.

Плюсы:

  • Ошибки ловятся раньше — до выполнения.
  • Документация — видно, что принимает и что возвращает.
  • IDE — автодополнение, рефакторинг.
  • Рефакторинг безопаснее — типы проверяются.

Минус: чуть больше кода.

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

Параметры:

function greet(string $name): string
{
    return "Привет, $name";
}

Возвращаемый тип после :.

Примитивные типы

function f(
    int $a,
    float $b,
    string $c,
    bool $d,
    array $e,
    object $f,
    mixed $g,
): void {}

mixed — любой тип (PHP 8.0+).

null — только если параметр опциональный:

function f(?string $name): void {}

Возвращаемые типы

function getName(): string { return 'Иван'; }
function getId(): int { return 1; }
function isActive(): bool { return true; }
function getItems(): array { return []; }
function getUser(): User { return new User(); }
function getMaybe(): ?string { return null; }

void — ничего:

function log(string $msg): void
{
    echo $msg;
}

never (PHP 8.1+):

function fail(string $msg): never
{
    throw new Exception($msg);
}

Функция никогда не возвращает управление — прерывает через throw, exit.

Strict types

По умолчанию PHP делает неявные приведения:

function sum(int $a, int $b): int { return $a + $b; }

sum('2', '3');   // 5 — строки превратились в int
sum(2.5, 3.7);   // 5 — float → int (с потерей)

Со strict_types=1:

<?php

declare(strict_types=1);

function sum(int $a, int $b): int { return $a + $b; }

sum('2', '3');   // TypeError
sum(2, 3);       // 5

Правило: всегда declare(strict_types=1).

Работает для вызовов из того же файла. Для вызовов извне — по настройке того файла.

Union types

PHP 8.0+:

function formatId(int|string $id): string
{
    return (string) $id;
}

Несколько вариантов:

function process(int|float|string $value): void {}

С null:

function f(int|string|null $value): void {}

Типичный пример — цена:

function setPrice(int|float $price): void { }

Nullable

?type — короткая запись type|null:

function greet(?string $name): string
{
    return $name !== null ? "Привет, $name" : 'Привет';
}

Со значением по умолчанию:

function greet(?string $name = null): string
{
    return $name !== null ? "Привет, $name" : 'Привет';
}

Пересечение типов

PHP 8.1+:

function f(Countable&ArrayAccess $collection): void { }

Объект должен реализовать оба интерфейса.

DNF types

PHP 8.2+ — disjunctive normal form:

function f((A&B)|C $value): void { }

Комбинация union и intersection.

Классы и интерфейсы

function save(User $user): void { }
function process(LoggerInterface $logger): void { }

Строгая типизация — IDE, статанализ.

self, static, parent

class Query
{
    public function where(string $f): static
    {
        return $this;
    }

    public function duplicate(): self
    {
        return clone $this;
    }
}

self — тип текущего класса. static — тип вызывающего класса (для наследников).

callable

function apply(callable $fn, mixed $value): mixed
{
    return $fn($value);
}

apply('strlen', 'Hello');              // 5
apply(fn($x) => $x * 2, 5);            // 10
apply([$obj, 'method'], $arg);         // метод

Быстрее и точнее — Closure:

function apply(Closure $fn, mixed $value): mixed
{
    return $fn($value);
}

iterable

function process(iterable $items): void
{
    foreach ($items as $item) { }
}

Принимает массив или Traversable.

object

function f(object $obj): void { }

Любой объект.

Обычно лучше указать класс или интерфейс.

array

function sum(array $numbers): int
{
    return array_sum($numbers);
}

Для типизированных массивов — PHPDoc:

/**
 * @param int[] $numbers
 * @return int
 */
function sum(array $numbers): int
{
    return array_sum($numbers);
}

Для объектов:

/**
 * @param User[] $users
 * @return string[]
 */
function getNames(array $users): array
{
    return array_map(fn(User $u) => $u->name, $users);
}

PHPDoc — читает IDE и анализаторы (PHPStan, Psalm).

mixed

PHP 8.0+ — любой тип:

function debug(mixed $value): void
{
    var_dump($value);
}

Эквивалент array|bool|callable|int|float|null|object|resource|string.

Не злоупотребляй — теряется типизация.

never

PHP 8.1+:

function fail(string $msg): never
{
    throw new RuntimeException($msg);
}

function loop(): never
{
    while (true) { }
}

Функция не возвращает управление.

Ковариантность / контравариантность

Возврат — ковариантный:

class ParentClass
{
    public function get(): ParentType { }
}

class ChildClass extends ParentClass
{
    public function get(): ChildType { }   // уже (ChildType — потомок ParentType)
}

Параметры — контравариантные:

class ParentClass
{
    public function set(ChildType $x): void { }
}

class ChildClass extends ParentClass
{
    public function set(ParentType $x): void { }   // шире
}

Правило: возврат можно сузить, параметры расширить.

Типы в свойствах

class User
{
    public string $name;
    public ?string $email = null;
    public int $age = 0;
    public array $roles = [];
}

С PHP 8.1:

class User
{
    public readonly string $id;
}

Типы в константах

PHP 8.3+:

class Config
{
    const int MAX_ATTEMPTS = 5;
    const string APP_NAME = 'MyApp';
}

Проверка типов во время выполнения

function f(mixed $x): void
{
    if (!is_int($x)) {
        throw new InvalidArgumentException('Ожидается int');
    }
}

Для сложных случаев — is_object, instanceof:

if ($x instanceof User) {
    echo $x->name;
}

Type juggling

PHP сам приводит типы в некоторых случаях:

// Арифметика
'5' + 3;       // 8
'5' . 3;       // '53' (конкатенация)

// Сравнение
'5' == 5;      // true
'5' === 5;     // false

Со strict_types — при вызовах функций не приводит.

Практика

<?php

declare(strict_types=1);

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}

    public function add(Money $other): static
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Валюты разные');
        }

        return new static($this->amount + $other->amount, $this->currency);
    }

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

function total(array $items): Money
{
    $sum = new Money(0, 'RUB');
    foreach ($items as $item) {
        $sum = $sum->add($item);
    }
    return $sum;
}

$sum = total([
    new Money(10000, 'RUB'),
    new Money(25000, 'RUB'),
]);

echo $sum->format();   // 350.00 RUB

Итог

  • Типы — обязательны для функций, методов, свойств.
  • declare(strict_types=1) — всегда.
  • Union — int|string.
  • Nullable — ?string или string|null.
  • void — ничего, never — не возвращает.
  • mixed — любой (осторожно).
  • PHPDoc — для коллекций.
  • Ковариантность — возврат сужается, параметры расширяются.

Дальше — strict_types, union types, nullable.