Функция как контракт

Функция в PHP — именованный или безымянный блок кода, который можно вызвать с аргументами и получить результат. В простом виде она выглядит как «входные значения → вычисление → возвращаемое значение», но в реальном проекте функция ещё задаёт контракт: какие типы принимает, какие ошибки может бросить, меняет ли внешнее состояние и можно ли передать её как callback.

<?php

declare(strict_types=1);

function formatPrice(int $amount, string $currency = 'KZT'): string
{
    return number_format($amount, 0, '.', ' ') . ' ' . $currency;
}

echo formatPrice(12500); // 12 500 KZT

Сигнатура функции здесь сообщает больше, чем тело: нужен int, есть значение по умолчанию, результат всегда string. Это продолжает тему Типы и strict_types: типы не делают код «магически правильным», но фиксируют границы, на которых PHP может бросить TypeError.

flowchart TD A[Вызов функции] --> B[Вычислить аргументы слева направо] B --> C[Сопоставить с параметрами] C --> D{Есть типы параметров?} D -->|да| E[Проверить типы] D -->|нет| F[Выполнить тело] E -->|ошибка| G[TypeError] E -->|ок| F F --> H{Есть return type?} H -->|да| I[Проверить возвращаемое значение] H -->|нет| J[Вернуть результат] I -->|ошибка| G I -->|ок| J
flowchart TD
    A[Вызов функции] --> B[Вычислить аргументы слева направо]
    B --> C[Сопоставить с параметрами]
    C --> D{Есть типы параметров?}
    D -->|да| E[Проверить типы]
    D -->|нет| F[Выполнить тело]
    E -->|ошибка| G[TypeError]
    E -->|ок| F
    F --> H{Есть return type?}
    H -->|да| I[Проверить возвращаемое значение]
    H -->|нет| J[Вернуть результат]
    I -->|ошибка| G
    I -->|ок| J
Путь вызова функции: аргументы сначала вычисляются, затем сопоставляются с параметрами, после чего PHP проверяет типы на входе и на выходе.
Быстрое повторение
Что именно фиксирует сигнатура `function formatPrice(int $amount, string $currency = 'KZT'): string` как контракт функции?

User-defined и built-in функции

User-defined functions — функции, которые объявляет сам разработчик через function. Built-in functions — встроенные функции PHP и расширений: strlen(), array_map(), json_encode(), password_hash(), mb_strlen() и тысячи других. Они вызываются одинаково, но живут в разных местах: ваши функции — в коде проекта, встроенные — в ядре PHP или подключённых extension.

<?php

function normalizeEmail(string $email): string
{
    return mb_strtolower(trim($email));
}

$email = normalizeEmail(' USER@Example.COM ');
var_dump($email); // string(16) "user@example.com"

Функции часто становятся маленькими «именованными решениями». Если выражение из Операторы и управляющие конструкции начинает повторяться в нескольких местах, его стоит вынести в функцию с понятным именем.

Scope: что видно внутри функции

У функции своя локальная область видимости. Переменная снаружи не становится автоматически доступной внутри.

<?php

$taxRate = 0.12;

function addTax(int $amount): int
{
    // $taxRate здесь не видна
    return (int) round($amount * 1.12);
}

Можно использовать global или $GLOBALS, но в прикладном коде это почти всегда хуже явного параметра. Функция, которая получает всё нужное через аргументы, легче читается, тестируется и анализируется.

<?php

function addTax(int $amount, float $rate): int
{
    return (int) round($amount * (1 + $rate));
}

Есть ещё static-переменные внутри функции: они сохраняют значение между вызовами, но видны только этой функции. Это полезно редко — например, для простого memoization, но может спрятать состояние там, где читатель его не ждёт.

Быстрое повторение
Почему функцию `addTax(int $amount, float $rate)` обычно легче тестировать, чем вариант, который читает `$taxRate` через `global`?

Параметры, defaults, named arguments

Аргументы по умолчанию подставляются только когда аргумент не передали. Если передать null, это уже явное значение, а не «используй default».

<?php

function label(string $text = 'Без названия'): string
{
    return $text;
}

echo label();        // Без названия
echo label('Отчёт'); // Отчёт
// label(null);      // TypeError, потому что параметр string

С PHP 8.0 можно вызывать функции с named arguments. Это удобно у функций с несколькими опциями, где позиционный вызов плохо читается.

<?php

$html = htmlspecialchars(
    string: $title,
    flags: ENT_QUOTES | ENT_SUBSTITUTE,
    encoding: 'UTF-8',
    double_encode: false,
);

Важный практический эффект: имена параметров становятся частью публичного API. Если библиотека выпустила функцию send(string $to, string $subject), а пользователи вызывают send(to: ..., subject: ...), переименование $subject в $title может сломать чужой код. Это уже не только стиль, а совместимость.

Быстрое повторение
Что произойдёт при вызове `label(null)`, если функция объявлена как `function label(string $text = 'Без названия'): string`?

Variadic: переменное число аргументов

Оператор ... в параметрах собирает оставшиеся аргументы в массив. Такой параметр называют variadic.

<?php

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

echo sum(10, 20, 30); // 60

Тот же оператор используется при распаковке массива в аргументы:

<?php

$parts = [10, 20, 30];
echo sum(...$parts); // 60

Variadic хорошо подходит для функций вроде sum(), builders, логирования или API, где количество однотипных значений заранее неизвестно. Если аргументы разнотипные и имеют смысловые имена, named arguments обычно читаются лучше.

Возврат значений и ссылки

return завершает функцию и возвращает значение. Если функция объявлена с return type, PHP проверяет результат на выходе.

<?php

function findUserName(array $users, int $id): ?string
{
    return $users[$id]['name'] ?? null;
}

void означает, что функция ничего не возвращает как значение. never — что функция вообще не возвращает управление обычным способом: например, всегда бросает исключение или завершает процесс.

Передача по ссылке через & позволяет функции менять переменную вызывающего кода. Это мощный, но шумный инструмент: по месту вызова не всегда видно, что значение изменится.

<?php

function appendSuffix(string &$value): void
{
    $value .= '-archived';
}

$status = 'invoice';
appendSuffix($status);
echo $status; // invoice-archived

Обычно лучше вернуть новое значение. Ссылки уместны в низкоуровневом коде, совместимости со старым API или там, где это явно ожидаемая модель, как у некоторых built-in функций.

Анонимные функции и замыкания

Анонимная функция не имеет имени и обычно передаётся как значение. В PHP такие функции представлены объектом Closure.

<?php

$prices = [1200, 250, 80];

$withTax = array_map(
    function (int $amount): int {
        return (int) round($amount * 1.12);
    },
    $prices,
);

print_r($withTax); // [1344, 280, 90]

Если анонимной функции нужна переменная из внешней области видимости, используется use. По умолчанию значение захватывается на момент создания closure.

<?php

$rate = 0.12;

$addTax = function (int $amount) use ($rate): int {
    return (int) round($amount * (1 + $rate));
};

echo $addTax(1000); // 1120

Чтобы менять внешнюю переменную, можно захватить её по ссылке: use (&$counter). Но это такой же риск скрытого состояния, как global: иногда нужно, но лучше не делать это привычкой.

Arrow functions

Arrow functions появились как короткая форма для одновыраженных callbacks. Они записываются через fn (...) => expression и автоматически захватывают внешние переменные по значению.

<?php

$rate = 0.12;
$prices = [1200, 250, 80];

$withTax = array_map(
    fn (int $amount): int => (int) round($amount * (1 + $rate)),
    $prices,
);

Arrow function хороша, когда тело действительно помещается в одно выражение. Если внутри появляются guard-условия, несколько шагов, try/catch или побочные эффекты, обычная анонимная функция честнее: она не притворяется короткой.

callable и first-class callable syntax

callable — тип для значения, которое можно вызвать как функцию. Callback может быть closure, строкой с именем функции, массивом вида [$object, 'method'], массивом вида [ClassName::class, 'staticMethod'] или объектом с методом __invoke().

<?php

function applyDiscount(array $amounts, callable $discount): array
{
    return array_map($discount, $amounts);
}

$result = applyDiscount(
    [1000, 2000],
    fn (int $amount): int => (int) round($amount * 0.9),
);

С PHP 8.1 есть first-class callable syntax: strlen(...), $object->method(...), SomeClass::method(...). Он создаёт Closure из вызываемого выражения и обычно лучше старого строкового синтаксиса: IDE и статические анализаторы понимают его точнее.

<?php

$length = strlen(...);

echo $length('PHP'); // 3

$names = ['Ada', 'Rasmus', 'Grace'];
usort($names, strcmp(...));

В проектах с PHPDoc, generics и статический анализ это особенно заметно: Closure(int): string или callable(User): bool в PHPDoc сообщает анализатору больше, чем просто callable в сигнатуре.

Типичные ловушки

Первая ловушка — слишком широкие функции: много параметров, много веток, неясный результат. Часто это признак, что функция смешала несколько задач.

Вторая — named arguments в публичном API. Они улучшают читаемость вызова, но закрепляют имена параметров как совместимый контракт.

Третья — захват переменных в closure. use ($x) берёт значение на момент создания, а не на момент вызова. Arrow functions делают это автоматически, поэтому иногда ошибка выглядит «слишком аккуратно».

Четвёртая — callable без уточнения формы. Для PHP достаточно, что значение вызывается. Для человека и статического анализатора полезнее знать: это Closure, метод объекта, invokable-класс или callback с конкретной сигнатурой.

См. также

Источники

  1. PHP Manual: Функции
  2. PHP Manual: Параметры и аргументы функции
  3. PHP Manual: Анонимные функции
  4. PHP Manual: Стрелочные функции
  5. PHP Manual: Синтаксис создания первоклассных callable-значений
  6. PHP Manual: Вызываемые выражения