Глава 15 · С нуля → практика
Функции: контракт, параметры и результат
От первого вызова к расчёту заказа: аргументы становятся местными параметрами, return возвращает результат в точное выражение, а вызывающий код использует его дальше.
Получить готовую подпись по имени действия: объявление и вызов
На странице магазина и в сообщении о заказе нужны часы работы. Сначала научимся вызывать одно подготовленное действие: оно покажет «Пункт выдачи: 10:00–19:00». Функция даёт набору инструкций имя, по которому программа запускает их в нужном месте. Объявление описывает действие; вызов действительно выполняет его.
- Объявлена showOpeningHours: её тело пока не выполняется.
- echo выводит «Ваш заказ готов».
- Строка showOpeningHours(); передаёт выполнение в тело функции.
- Внутренний echo показывает часы; конец тела возвращает выполнение к строке после вызова.
- Последний echo показывает «Возьмите номер заказа».
| Запись | Как прочитать |
|---|---|
function showOpeningHours() | Объявить функцию с именем showOpeningHours. Пустые () — ей не нужны входные значения. |
{ ... } | Начало и конец тела: инструкции, которые выполнятся при вызове. Отступы помогают читать, границы задают скобки. |
showOpeningHours(); | Вызвать уже объявленную функцию. () после имени означают вызов; ; завершает эту инструкцию. |
<?php
function showOpeningHours()
{
echo "Пункт выдачи: 10:00–19:00\n";
}
echo "Ваш заказ готов\n";
showOpeningHours();
echo "Возьмите номер заказа\n";
Ожидаемый результат
Ваш заказ готов
Пункт выдачи: 10:00–19:00
Возьмите номер заказа
Копируйте весь файл. Первая выполняемая печать находится после объявления. Вызов не запускает файл сначала и не останавливает его после тела: программа возвращается именно к продолжению вызвавшего кода. \n в двойных кавычках создаёт перевод строки CLI.
Для второго сообщения можно снова написать showOpeningHours();. Объявлять ту же функцию второй раз не нужно: повторное объявление имени приводит к ошибке. При изменении расписания меняется одно тело. Если расписание всегда выводится только один раз, обычный echo тоже решает задачу; польза имени появляется при повторном использовании и разделении понятных действий.
Параметр — местное имя входа; аргумент — значение конкретного вызова
Теперь памятка должна обращаться к конкретному покупателю. Сохранить «Анна» прямо внутри тела неудобно: следующему человеку нужно другое обращение. Добавим одно входное место с местным именем $customerName. Само имя в объявлении называется параметром. То, что стоит в скобках вызова, называется аргументом; сначала PHP вычисляет его значение, затем отдаёт параметру.
- В вызывающем коде $recipient содержит строку Анна.
- В скобках showPickupMessage($recipient) читается эта строка.
- В новом вызове местная $customerName получает Анна.
- Тело читает $customerName и выводит обращение.
- После } продолжается следующий вызов с отдельным значением Мария.
<?php
function showPickupMessage($customerName)
{
echo $customerName, ', ваш заказ готов', "\n";
}
$recipient = 'Анна';
showPickupMessage($recipient);
showPickupMessage('Мария');
echo 'Получатель первого заказа: ', $recipient, "\n";
Ожидаемый результат
Анна, ваш заказ готов
Мария, ваш заказ готов
Получатель первого заказа: Анна
$recipient и $customerName — разные имена. Совпадение имён не требуется: соответствие создают скобки вызова. Третий echo читает исходную внешнюю переменную. Во втором вызове строка передана непосредственно, без промежуточной переменной.
<?php
function makePickupCaption($customerName)
{
$customerName = 'Получатель: ' . $customerName;
echo $customerName, "\n";
}
$recipient = 'Анна';
makePickupCaption($recipient);
echo 'Исходное имя: ', $recipient, "\n";
Ожидаемый результат
Получатель: Анна
Исходное имя: Анна
Точка соединяет два текста. Присваивание меняет местный параметр текущего вызова. Внешний recipient сохраняет Анна: обычные параметры передаются по значению. Объекты и явные ссылки разбираются дальше отдельно.
Переменная, созданная рядом с объявлением функции, сама по себе не появляется внутри её тела. Если нужны имя, цена или путь файла, передайте их аргументами. Это отвечает на реальную проблему из обсуждения недоступной переменной внутри функции: существование имени где-то в файле не определяет область видимости. Здесь важен путь входа, а не совет добавлять global ко всем именам. Подробнее об областях видимости.
return: получить сумму обратно в место вызова и продолжить расчёт
У покупателя две упаковки чая по 150 рублей. Нужно получить 300, затем прибавить доставку 50 и показать «К оплате: 350 руб.». Простое умножение справится. Назовём повторяемое правило lineTotal, чтобы оно считало разные строки заказа. На входе цена одной упаковки и количество; на выходе число рублей за эту строку. Для первого опыта все цены целые и заранее заданы.
| Часть объявления | Что означает в этой задаче |
|---|---|
function lineTotal($unitPrice, $quantity) | Два параметра через запятую: сначала цена одной упаковки, затем количество. |
$subtotal = $unitPrice * $quantity; | Умножить местные числа и сохранить местный промежуточный результат. |
return $subtotal; | Завершить текущий вызов и отдать вычисленное число выражению вызова. Это ещё не вывод на экран. |
$itemsTotal = lineTotal(150, 2); | После возврата вызов имеет значение 300; именно оно присваивается itemsTotal. |
<?php
function lineTotal($unitPrice, $quantity)
{
$subtotal = $unitPrice * $quantity;
return $subtotal;
}
$itemsTotal = lineTotal(150, 2);
$payable = $itemsTotal + 50;
echo 'Товары: ', $itemsTotal, ' руб.', "\n";
echo 'К оплате: ', $payable, ' руб.', "\n";
Ожидаемый результат
Товары: 300 руб.
К оплате: 350 руб.
Вызов отдаёт int 300. Функция не печатает и не прибавляет доставку: эти действия видны после вызова. Внешний код может использовать 300 несколько раз. Местная subtotal не выходит наружу под своим именем — наружу передано её значение.
- $itemsTotal = lineTotal(150, 2) временно ждёт результат справа от =.
- 150 → местная $unitPrice; 2 → местная $quantity.
- $subtotal = 150 * 2 → 300.
- return $subtotal → значение выражения lineTotal(150, 2) становится 300.
- Ожидавшее присваивание завершается: $itemsTotal = 300.
- Следующая строка считает $payable = 300 + 50 → 350.
- Два echo формируют две подписанные строки.
<?php
function lineTotal($unitPrice, $quantity)
{
return $unitPrice * $quantity;
}
$teaTotal = lineTotal(150, 2);
$coffeeTotal = lineTotal(200, 3);
$payable = $teaTotal + $coffeeTotal + 50;
echo 'Чай: ', $teaTotal, ' руб.', "\n";
echo 'Кофе: ', $coffeeTotal, ' руб.', "\n";
echo 'Заказ с доставкой: ', $payable, ' руб.', "\n";
echo 'Ещё одна упаковка чая: ', lineTotal(150, 1), ' руб.', "\n";
Ожидаемый результат
Чай: 300 руб.
Кофе: 600 руб.
Заказ с доставкой: 950 руб.
Ещё одна упаковка чая: 150 руб.
Первый вызов вернул 300, второй 600; результат итогового выражения равен 950. В последнем echo результат 150 используется сразу, без сохранения. Одно return может возвращать выражение: промежуточная subtotal полезна для объяснения, но не обязательна.
Вывести сейчас или вернуть для использования: это два разных результата
В обсуждении echo и return на Stack Overflow оба варианта показывали одинаковое число, и автор не видел пользы return. Проверим требование, при котором разница видна: подпись надо сначала подготовить, потом добавить к ней адрес получения. Возвращённый текст можно соединить; уже выведенный текст сам не становится значением переменной.
<?php
function pickupCaption($orderNumber)
{
return 'Заказ ' . $orderNumber . ' готов';
}
$message = pickupCaption(42);
$message = $message . '. Получение: центральный пункт.';
echo $message, "\n";
Ожидаемый результат
Заказ 42 готов. Получение: центральный пункт.
pickupCaption(42) возвращает строку «Заказ 42 готов». Она сохраняется в message, затем становится частью более длинной строки. До последнего echo на экран не отправлено ничего.
Следующий пример намеренно наблюдает другое поведение. === null проверяет, вернулось ли специальное значение null. if (...) выполняет блок только при истинном условии. Функция без return со значением возвращает null, хотя её echo уже мог показать текст.
<?php
function showPickupCaption($orderNumber)
{
echo 'Заказ ', $orderNumber, ' готов', "\n";
}
$message = showPickupCaption(42);
if ($message === null) {
echo "В message нет подготовленного текста: функция его уже вывела\n";
}
echo "Программа продолжилась после вызова\n";
Ожидаемый результат
Заказ 42 готов
В message нет подготовленного текста: функция его уже вывела
Программа продолжилась после вызова
showPickupCaption выполняет действие — вывод. Присваивание получает null, а не показанную строку. Поэтому message не подходит для последующего добавления адреса. Вернуть значение и выполнить побочное действие технически можно в одной функции, но вызывающему коду нужно заранее знать оба эффекта.
: void после скобок объявления явно обещает действие без полезного возвращаемого значения. В такой функции допустим return; без выражения, но нельзя return $message;. Сам void не запрещает печать, запись файла или изменение переданного по ссылке значения. Пример ниже использует только вывод, без внешних действий.
<?php
function showPickupReminder(): void
{
echo "Для получения назовите номер заказа\n";
}
showPickupReminder();
echo "Памятка показана\n";
Ожидаемый результат
Для получения назовите номер заказа
Памятка показана
Вызов стоит самостоятельной инструкцией: сохранять его в переменную здесь не требуется. Двоеточие вводит объявление возвращаемого типа. Нельзя обещать одновременно void и возвращаемую строку.
Досрочный return и вложенный вызов: куда возвращается выполнение
Для пустой корзины нужна отдельная подпись. Условие $quantity === 0 строго сравнивает с целым нулём. При совпадении return заканчивает только текущую функцию. Следующий вызов продолжит работать независимо. Мы заранее считаем quantity неотрицательным целым; проверку произвольного текста этим условием не заменяют.
<?php
function cartCaption($quantity)
{
if ($quantity === 0) {
return 'Корзина пуста';
}
return 'В корзине упаковок: ' . $quantity;
}
$emptyCaption = cartCaption(0);
$filledCaption = cartCaption(3);
echo $emptyCaption, "\n";
echo $filledCaption, "\n";
echo "Можно продолжить покупку\n";
Ожидаемый результат
Корзина пуста
В корзине упаковок: 3
Можно продолжить покупку
В cartCaption(0) второй return уже не выполняется. В cartCaption(3) первый блок пропускается. Оба результата сохранены и затем выведены. return не завершил весь сценарий: последнее сообщение тоже появилось.
<?php
function lineTotal($unitPrice, $quantity)
{
return $unitPrice * $quantity;
}
function totalWithDelivery($unitPrice, $quantity, $delivery)
{
$itemsTotal = lineTotal($unitPrice, $quantity);
return $itemsTotal + $delivery;
}
$payable = totalWithDelivery(150, 2, 50);
echo 'К оплате: ', $payable, ' руб.', "\n";
Ожидаемый результат
К оплате: 350 руб.
Внешняя payable ждёт totalWithDelivery. Внутри него itemsTotal ждёт lineTotal. Внутренний return отдаёт 300 в itemsTotal; затем внешний return отдаёт 350 в payable. Это два независимых местных контекста и две разные точки возврата.
| Остановить что? | Запись и продолжение |
|---|---|
| Текущий вызов функции | return; или return значение; — продолжение в вызывающем коде. |
| Текущий цикл | break; — продолжение после цикла; функция может выполняться дальше. |
| Только текущий проход цикла | continue; — следующий проход; это не выход из функции. |
| Подключённый PHP-файл | return в нём возвращает управление месту include/require; это отдельный контекст вне функции. |
Если один путь функции возвращает число, а другой молча доходит до конца, у вызывающего кода получится число либо null. Уточните все пути до использования результата. Если расчёт обещает int, отсутствие требуемого результата является ошибкой договора.
Типы и значения по умолчанию: уточнить уже понятные вход и выход
Покупатель чаще берёт одну упаковку: quantity можно не передавать, если договор задаёт значение 1. Это выбор при отсутствующем аргументе. Переданный null означает отдельное значение и не просит PHP взять 1. Обязательные параметры ставят первыми, необязательные — после них.
<?php
function lineTotal($unitPrice, $quantity = 1)
{
return $unitPrice * $quantity;
}
echo 'Одна упаковка: ', lineTotal(150), ' руб.', "\n";
echo 'Три упаковки: ', lineTotal(150, 3), ' руб.', "\n";
Ожидаемый результат
Одна упаковка: 150 руб.
Три упаковки: 450 руб.
В первом вызове unitPrice получает 150, quantity берёт 1 из объявления. Во втором quantity получает переданное 3. = 1 в объявлении задаёт резерв, а не присваивает количество при каждом обращении к параметру.
Добавим проверяемые типы: int $unitPrice — параметр принимает целое, : int — вызов должен вернуть целое. declare(strict_types=1); первой инструкцией включает строгие правила скалярных вызовов из данного файла. Строка "2" не станет int автоматически; разрешённое расширение int → float сохраняется. Это не проверка положительности, валюты или числа элементов массива.
<?php
declare(strict_types=1);
function lineTotal(int $unitPrice, int $quantity = 1): int
{
return $unitPrice * $quantity;
}
$payable = lineTotal(150, 2) + 50;
echo 'К оплате: ', $payable, ' руб.', "\n";
Ожидаемый результат
К оплате: 350 руб.
150 и 2 — целые литералы, результат умножения в этом диапазоне int 300. Возвращённое число непосредственно входит в выражение сложения. Предел int конечен; договор не обещает успешное умножение любых сколь угодно больших значений.
PHP 8.0+ позволяет подписать аргументы именами параметров через двоеточие. Это удобно, когда порядок нескольких чисел легко перепутать. В реальном вопросе о named arguments вместо двоеточия использовали присваивание. Запись quantity: 2 выбирает параметр; запись $quantity = 2 внутри вызова лишь присваивает внешнюю переменную и остаётся позиционным выражением.
<?php
declare(strict_types=1);
function lineTotal(int $unitPrice, int $quantity = 1): int
{
return $unitPrice * $quantity;
}
$total = lineTotal(quantity: 2, unitPrice: 150);
echo 'Две упаковки: ', $total, ' руб.', "\n";
Ожидаемый результат
Две упаковки: 300 руб.
quantity получает 2 независимо от позиции в списке; unitPrice получает 150. Имена должны совпадать с объявлением. Позиционные аргументы, если они тоже есть, пишутся до именованных. Переименование параметра способно сломать такие вызовы.
На следующей ступени справочник разбирает nullable, variadic, распаковку и ошибки количества аргументов. ?string разрешает строку или null, но само по себе не делает аргумент необязательным. ?string $caption = null сочетает разрешение null с возможностью пропуска. Неявную nullable-запись string $caption = null не выбирайте: она deprecated в PHP 8.4.
Вернуть несколько связанных результатов в одном массиве
Теперь подтверждение заказа должно содержать сумму товаров, стоимость доставки и итог. Один return возвращает одно значение. Этим значением может быть массив с именованными полями. Если массив ещё незнаком, сначала пройдите список и запись. Здесь ['items' => 300] означает массив с ключом items и числом 300; $summary['items'] читает это поле.
<?php
function orderSummary($unitPrice, $quantity, $delivery)
{
$items = $unitPrice * $quantity;
return [
'items' => $items,
'delivery' => $delivery,
'payable' => $items + $delivery,
];
}
$summary = orderSummary(150, 2, 50);
echo 'Товары: ', $summary['items'], ' руб.', "\n";
echo 'Доставка: ', $summary['delivery'], ' руб.', "\n";
echo 'Сохранить к оплате: ', $summary['payable'], ' руб.', "\n";
Ожидаемый результат
Товары: 300 руб.
Доставка: 50 руб.
Сохранить к оплате: 350 руб.
Вызов вернул один массив с тремя полями. Внешняя summary получила всю структуру; последующие выражения выбрали нужные числа. На экран не отправляется сам массив: echo получает конкретные поля. Функция не сохраняет оплату в БД, а подготавливает число для следующего шага.
Граница результата теперь документируется и по ключам: items, delivery и payable всегда существуют и содержат целые суммы в одной валюте. Объявление : array проверило бы лишь внешний тип, а не названия полей. Сложный договор нужно описывать и проверять отдельно. Называйте единицы: рубли с целой ценой — учебное упрощение; суммы с копейками обычно считают в целых минимальных единицах или выбранной десятичной арифметике.
Самостоятельная задача: скидка после вызова, доставка после скидки
Заказ содержит 3 упаковки по 120 рублей. Функция lineTotal должна вернуть стоимость товара без скидки. Вызывающий код уменьшает её на фиксированные 20 рублей, добавляет доставку 40 и показывает расчёт. Перед запуском вычислите ожидаемые значения: 360 → 340 → 380. Это упражнение проверяет, что результат возвращается именно туда, где продолжается расчёт.
<?php
function lineTotal($unitPrice, $quantity)
{
return $unitPrice * $quantity;
}
$items = lineTotal(120, 3);
$discountedItems = $items - 20;
$payable = $discountedItems + 40;
echo 'Товары до скидки: ', $items, ' руб.', "\n";
echo 'Товары после скидки: ', $discountedItems, ' руб.', "\n";
echo 'К оплате с доставкой: ', $payable, ' руб.', "\n";
Ожидаемый результат
Товары до скидки: 360 руб.
Товары после скидки: 340 руб.
К оплате с доставкой: 380 руб.
Возвращено 360; скидка и доставка применены только снаружи. Если заменить return внутри функции на echo, она выведет число раньше подписи и не отдаст нужного числа переменной items. Такой эксперимент объясняет ошибку, но не служит готовым расчётом.
Следующий шаг — отдельно изменить договор: принимать список уже рассчитанных сумм вместо цены и количества. В справочных примерах ниже встретится такой вариант. Перед каждым вызовом проверяйте четыре вещи: какие значения передаются, в каких единицах, что именно возвращается и где возвращённое значение используется. Вызов сам по себе не хранит результат между запросами сайта.
Аргумент вычисляется, параметр получает результат
На практике аргумент бывает готовым числом или выражением, которое ещё нужно вычислить. В этом продолжении сохраняем порядок входов первого урока: unitPrice, затем quantity. Имена маленьких функций подсказывают, какое значение вернётся; сообщения специально делают порядок видимым.
Вызов lineTotal(2, 150) читается как «передай количество 2 и цену 150». В объявлении $quantity и $unitPrice — параметры. В месте вызова 2 и 150 — аргументы. Аргументом может быть выражение: PHP сначала вычислит его и только затем войдёт в тело.
Для списка аргументов вычисление идёт слева направо. Это не разрешение предполагать такой же порядок для любого выражения с операторами. Если подготовка требует запросов/записи, понятнее вынести её в отдельные строки и не скрывать побочные эффекты в аргументах.
- readQuantity() → 2
- readUnitPrice() → 150
- Параметры quantity=2, unitPrice=150
- Тело lineTotal → return 300
- Вызвать первый аргумент readUnitPrice(): получить 150.
- Вызвать второй аргумент readQuantity(): получить 2.
- Только теперь войти в lineTotal с местными unitPrice = 150 и quantity = 2.
- Вернуть 300 в ожидающую правую часть $itemsTotal =.
- Следующая строка добавляет доставку 50 и готовит оплату 350.
<?php
function readUnitPrice()
{
echo "1. Цена получена: 150 руб.\n";
return 150;
}
function readQuantity()
{
echo "2. Количество получено: 2 уп.\n";
return 2;
}
function lineTotal($unitPrice, $quantity,)
{
echo "3. Начат расчёт строки\n";
return $unitPrice * $quantity;
}
$itemsTotal = lineTotal(
readUnitPrice(),
readQuantity(),
);
$payable = $itemsTotal + 50;
echo '4. К оплате с доставкой: ', $payable, ' руб.', "\n";
Ожидаемый результат
1. Цена получена: 150 руб.
2. Количество получено: 2 уп.
3. Начат расчёт строки
4. К оплате с доставкой: 350 руб.
Два read-имени здесь возвращают заранее заданные учебные значения, не читают реальную БД. Последовательность сообщений позволяет увидеть порядок вычисления. Запятая после последнего параметра или аргумента не создаёт пустое третье значение: это завершающая запятая. После return в lineTotal вызывающий код использует 300 для следующего расчёта.
Завершающая запятая в многострочном вызове доступна с PHP 7.3, в объявлении параметров — с PHP 8.0. Она облегчает добавление строк в diff и не означает «пропустить аргумент». Запись f(1, , 3) не становится допустимой.
Передать значение или разрешить изменить переменную
Нужно предложить скидочную цену и сохранить исходную для сравнения. Первый договор возвращает новую цену; второй намеренно переписывает переданную переменную. Эти варианты решают разные задачи. Изменение внешнего имени через & — побочное действие; возвращённое число — отдельный результат.
| Вызов в опыте | Переданное значение / местная связь | Результат снаружи |
|---|---|---|
| discountedPrice($price) | В местный price поступает 1000; он уменьшается на 100. | return отдаёт 900 в newPrice; внешний price остаётся 1000. |
| applyDiscount($price) | Местный price связан с внешним именем. | Внешний price становится 900; полезного возвращённого значения нет. |
| applyDiscount(1000) | Передано временное число, а не изменяемая переменная. | Error: присваивать значение такому аргументу нельзя. |
Возврат новой цены и изменение существующей цены — разные договоры. По умолчанию функция работает со своим значением параметра. &$price делает параметр псевдонимом внешней переменной: присваивание внутри меняет её снаружи. Ссылка в PHP не является арифметическим указателем памяти.
Начните с return: вызывающий явно решит, сохранять ли результат. Ссылка уместна, когда изменяемый вход — осознанная часть API, например выходной параметр количества замен у встроенной функции. Не добавляйте & только ради предполагаемой скорости.
<?php
function discountedPrice($price) {
$price -= 100;
return $price;
}
function applyDiscount(&$price) {
$price -= 100;
}
$price = 1000;
$newPrice = discountedPrice($price);
echo 'Исходная: ', $price, '; результат: ', $newPrice, "\n";
applyDiscount($price);
echo 'После функции с &: ', $price, "\n";
try {
applyDiscount(1000);
} catch (Error $error) {
echo "Литерал нельзя изменить по ссылке\n";
}
Ожидаемый результат
Исходная: 1000; результат: 900
После функции с &: 900
Литерал нельзя изменить по ссылке
У discountedPrice своя переменная $price, поэтому новое значение возвращается через return. В applyDiscount амперсанд стоит в определении параметра: тот становится псевдонимом переданной переменной. При вызове & не добавляют. Для ссылки нужна переменная, а не число, которое негде перезаписать.
У некоторых API есть отдельный выходной параметр по ссылке. В takeValidSkus первый результат — массив принятых артикулов через return, второй — изменённое число rejected. ?int &$rejected = null разрешает null, позволяет изменить переданную переменную и позволяет пропустить аргумент; это три самостоятельные части записи.
<?php
function takeValidSkus(array $values, ?int &$rejected = null): array
{
$rejected = 0;
$result = [];
foreach ($values as $value) {
if (!is_string($value) || $value === '') {
$rejected = $rejected + 1;
continue;
}
$result[] = $value;
}
return $result;
}
$rejected = 0;
$accepted = takeValidSkus(['TEA-01', '', 42, 'COF-02'], $rejected);
foreach ($accepted as $sku) {
echo 'Принят артикул: ', $sku, "\n";
}
echo 'Строк требуют исправления: ', $rejected, "\n";
$withoutCounter = takeValidSkus(['COCOA-03']);
echo 'Принят без внешнего счётчика: ', $withoutCounter[0], "\n";
Ожидаемый результат
Принят артикул: TEA-01
Принят артикул: COF-02
Строк требуют исправления: 2
Принят без внешнего счётчика: COCOA-03
Пустая строка и число 42 отклонены по договору: артикул должен быть непустой строкой. accepted получает два значения через return, а внешний rejected — 2 через ссылку. Последний вызов не передал второй аргумент: используется местный выходной счётчик, наружу возвращается только список. Формат артикула, уникальность и существование товара этим простым фильтром ещё не проверяются.
Для передачи по ссылке нужен доступный изменяемый аргумент: переменная, элемент массива либо результат функции, которая сама возвращает ссылку. Литерал и обычный результат выражения не подходят. Параметры со ссылками могут иметь тип и default; тип проверяют на входе, но само объявление не гарантирует, что функция не присвоит переменной значение другого типа.
Почему объект меняется без & и где это заканчивается
Этот шаг — мост к объектам: в переменной хранится идентификатор объекта. При обычной передаче копируется этот идентификатор, обе переменные обозначают один экземпляр. Поэтому изменение его свойства видно обеим сторонам. Это не делает две переменные псевдонимами через &.
У массива по значению запись в его обычный элемент не меняет внешний массив. Но объект внутри массива по-прежнему может быть общим; существующие ссылки внутри сложных структур тоже требуют отдельного внимания. «По значению» не равно автоматической глубокой копии всего графа объектов.
<?php
class Stock {
public int $quantity = 8;
}
function inspectStock(Stock $stock): void {
$stock->quantity = 7;
$stock = new Stock();
$stock->quantity = 99;
}
$stock = new Stock();
inspectStock($stock);
echo $stock->quantity, "\n";
Ожидаемый результат
7
Сначала обе переменные обозначают один объект, поэтому изменение quantity видно снаружи. Затем местная $stock начинает обозначать новый объект; внешняя переменная остаётся с прежним. По ссылке через & параметр не передавался. class описывает тип объекта, new создаёт экземпляр, -> обращается к его свойству.
Если требуется независимый объект, разбирайте clone и вложенные объекты. Если требуется заменить именно переменную вызывающего кода, нужен явный return с присваиванием либо договор передачи по ссылке.
Значение по умолчанию: только когда аргумент пропущен
Поле города имеет три осмысленных состояния. Пропущенный аргумент означает «использовать настроенный стандарт Москва». Явный null означает «человек ещё не выбрал город». Строка Казань означает конкретный выбор. Не подменяйте null стандартом молча, если такой договор не согласован.
- deliveryLabel() → параметр city получает default Москва → возвращается Доставка: Москва.
- deliveryLabel(null) → city равен null → возвращается Город ещё не выбран.
- deliveryLabel(Казань) → city получает Казань → возвращается Доставка: Казань.
- Каждый внешний echo использует возвращённую строку.
В форме доставки есть три состояния: пользователь оставил стандартный город, ещё не выбрал город или выбрал другой. Default отвечает только за пропуск аргумента. Явный null, пустая строка, false и 0 — переданные значения; их смысл определяет функция.
function deliveryLabel($city = "Москва") задаёт правило на входе. Оно не пересчитывается, потому что внутри значение оказалось пустым, и не заменяет проверку пользовательской формы.
<?php
function deliveryLabel($city = 'Москва') {
if ($city === null) {
return 'Город ещё не выбран';
}
return 'Доставка: ' . $city;
}
echo deliveryLabel(), "\n";
echo deliveryLabel(null), "\n";
echo deliveryLabel('Казань'), "\n";
Ожидаемый результат
Доставка: Москва
Город ещё не выбран
Доставка: Казань
Без аргумента сработало значение по умолчанию. Явный null остался null и получил отдельный смысл «ещё не выбрано». Если нужен другой договор — например, null тоже означает Москву — его нужно реализовать явно, но не приписывать это механизму параметров.
Для нескольких настроек необязательные параметры ставят после обязательных. Чтобы пропустить один default, но задать более поздний, используйте именованный аргумент. Возвращать пустые запятые или копировать все defaults вручную не требуется.
Defaults: массивы, константные выражения и new
Заголовок экспортного файла — готовая строка из имён столбцов. В основном опыте columns содержит [sku, stock], settings хранит разделитель ;, а implode соединяет имена в sku;stock. Если массив передан явно, его значения заменяют весь default. Объект в default — позднее уточнение после создания экземпляра через new; его смысл здесь — отдельные настройки каждого вызова.
| Вызов | Какие данные используются | Что получает вызывающий код |
|---|---|---|
| exportHeader() | Стандартные столбцы sku, stock; новый settings с разделителем ;. | Строку sku;stock. |
| exportHeader([sku, price], $custom) | Явные столбцы; переданный settings с разделителем |. | Строку sku|price. |
| makeSettings() дважды | Каждый пропуск аргумента создаёт свой экземпляр. | Два отдельных объекта; это не общий кэш. |
В default подходят скалярные значения, null, массивы и разрешённые константные выражения. Например, набор колонок отчёта можно задать массивом. С PHP 8.1 разрешён new ClassName(...): отдельный объект создаётся при применении default, а не один раз на все вызовы.
Произвольную переменную окружения или вызов loadSettings() в default поставить нельзя. Для такой подготовки примите null, затем внутри тела явно вызовите фабрику, либо передайте уже подготовленные настройки снаружи. Имя класса и аргументы new тоже подчиняются ограничениям выражений инициализации.
<?php
class ExportSettings {
public string $separator = ';';
}
function exportHeader(
array $columns = ['sku', 'stock'],
ExportSettings $settings = new ExportSettings(),
): string {
return implode($settings->separator, $columns);
}
function makeSettings(ExportSettings $settings = new ExportSettings()): ExportSettings {
return $settings;
}
echo exportHeader(), "\n";
$custom = new ExportSettings();
$custom->separator = '|';
echo exportHeader(['sku', 'price'], $custom), "\n";
$first = makeSettings();
$second = makeSettings();
echo $first === $second ? "Один объект\n" : "Разные объекты\n";
Ожидаемый результат
sku;stock
sku|price
Разные объекты
Массив columns задаёт обычный стандарт отчёта. new в default доступен с PHP 8.1; при каждом вызове, где settings пропущен, создаётся отдельный объект. Это не общий кэш. Когда нужен заранее настроенный экземпляр, передайте его явно, как $custom.
Настройки здесь сделаны объектом, чтобы увидеть независимость экземпляров; для двух простых опций массив может быть достаточным. default — удобство вызова, не скрытая обязанность создавать клиента API, соединение с БД или дорогой сервис.
Необязательный, nullable и отсутствующий — три разных случая
В заметке заказа отсутствие текста допустимо, но вызывающий обязан сделать выбор: передать строку либо явно передать null. Именно это описывает обязательный ?string $note. Добавление = null меняет договор ещё раз: теперь аргумент можно пропустить. Следующие ошибки относятся к нехватке аргументов, а не к неверному содержимому заметки.
| Объявление | Можно передать null? | Можно пропустить? |
|---|---|---|
| ?string $note | Да | Нет |
| ?string $note = null | Да | Да |
| string $note = "" | Нет в принятом контракте | Да |
<?php
function describeNote(?string $note): string {
return $note === null ? 'Нет заметки' : $note;
}
function labelOrder(int $orderId, ?string $note = null): string {
return $orderId . ': ' . describeNote($note);
}
echo describeNote(null), "\n";
echo labelOrder(42), "\n";
try {
describeNote();
} catch (ArgumentCountError $error) {
echo "Нужно передать аргумент, хотя null разрешён\n";
}
try {
labelOrder();
} catch (ArgumentCountError $error) {
echo "Не хватает orderId\n";
}
Ожидаемый результат
Нет заметки
42: Нет заметки
Нужно передать аргумент, хотя null разрешён
Не хватает orderId
Знак ? разрешает null в типе, а = null разрешает пропустить аргумент. Это два независимых решения. В labelOrder обязательный номер стоит раньше необязательной заметки. Ошибки намеренно перехвачены, поэтому пример завершается без необработанного исключения.
Параметр с default перед обязательным фактически не даёт ожидаемой необязательности; такой порядок объявлений устарел с PHP 8.0. Переставьте обязательные вперёд либо уберите бесполезный default. Не надейтесь «обойти» проблему именованным вызовом.
Старое string $note = null неявно разрешало null. С PHP 8.4 это объявление устарело; пишите явно ?string $note = null или string|null $note = null. Если после него ещё обязателен другой параметр — уберите default и используйте ?string $note.
Нехватка обязательного аргумента в PHP 7.1+ приводит к ArgumentCountError. Это ошибка договора вызова, а не сигнал автоматически подставить null. Неисполняемые антипримеры выше показаны как текст: рабочие файлы не производят deprecation-диагностику.
... в объявлении: принять ноль, один или много значений
Функция отчёта заранее не знает число счетов. Последний параметр int ...$amounts собирает оставшиеся аргументы в массив. До него можно оставить обычные параметры, например валюту. После variadic объявлять другие параметры нельзя.
Проверка типа применяется к каждой полученной сумме. Само наличие типа не гарантирует бизнес-правила: например, допустимость отрицательных сумм, один ли масштаб копеек и не переполнится ли сумма.
<?php
declare(strict_types=1);
function sumInvoices(int ...$amounts): int
{
$total = 0;
foreach ($amounts as $amount) {
$total = $total + $amount;
}
return $total;
}
$totalRubles = sumInvoices(100, 250, 300);
$paidRubles = 200;
echo 'Счета всего: ', $totalRubles, ' руб.', "\n";
echo 'Осталось оплатить: ', $totalRubles - $paidRubles, ' руб.', "\n";
echo 'Без счетов: ', sumInvoices(), ' руб.', "\n";
try {
sumInvoices(100, '250');
} catch (TypeError $error) {
echo "Счета не рассчитаны: каждая сумма должна быть целым числом\n";
}
Ожидаемый результат
Счета всего: 650 руб.
Осталось оплатить: 450 руб.
Без счетов: 0 руб.
Счета не рассчитаны: каждая сумма должна быть целым числом
Три аргумента становятся местным массивом amounts = [100, 250, 300]. Функция возвращает 650 в totalRubles, а вызывающий код вычитает уже оплаченные 200. Пустой вызов собирает [] и возвращает начальный 0. Тип int относится к каждому аргументу, а не к внешнему массиву; строгий файл не разрешил строку "250".
Если данные изначально приходят большим массивом/итератором, функция с параметром iterable $amounts может быть яснее распаковки огромного списка. Variadic удобен для интерфейса вызова; он не превращает накопленные аргументы в ленивый источник.
Если нужен обязательный префикс, например код валюты, его ставят перед variadic: function describeInvoices(string $currency, int ...$amounts). Вызов describeInvoices('RUB', 100, 250) отдаст RUB параметру currency, а [100, 250] — amounts. Этот синтаксис не переводит валюты и не проверяет допустимость отрицательных сумм.
... со ссылками: явная мутация нескольких входов
В этом редком договоре возвращаемого списка нет: trimFields меняет уже существующие name и city. string &...$fields означает «каждый дополнительный аргумент — изменяемая переменная со строкой». Внутренний ссылочный foreach нужен для записи в эти связанные элементы. После вызова дальнейший код должен читать изменённые исходные переменные, а не ждать строку от return.
Форма string &...$fields означает: каждый собранный аргумент должен подходить по типу и может быть изменён. Это редкий инструмент для API, где такая мутация ожидается. Не добавляйте его в обычный расчёт или форматирование только ради сокращения return.
<?php
function trimFields(string &...$fields): void {
foreach ($fields as &$field) {
$field = trim($field);
}
unset($field);
}
$name = ' Anna ';
$city = ' Kazan ';
trimFields($name, $city);
echo $name, '|', $city, "\n";
try {
trimFields(' Moscow ');
} catch (Error $error) {
echo "Нужна изменяемая переменная\n";
}
Ожидаемый результат
Anna|Kazan
Нужна изменяемая переменная
Амперсанд ставится перед ...: каждый аргумент становится ссылкой. Дополнительный & в foreach позволяет менять эти элементы, а unset снимает оставшуюся ссылку на последний. Приём подходит только явному договору «изменить входные переменные»; для обычной подготовки формы понятнее вернуть нормализованные данные.
Ссылочный foreach — отдельный механизм: он нужен, чтобы присваивание применялось к каждому элементу. Для объектов наличие ссылки обычно не требуется для вызова метода или изменения свойства. При распаковке Traversable не рассчитывайте получить ссылочную семантику элементов так же, как при передаче обычных переменных.
... в вызове: распаковать список и не перепутать ключи
Сборщик заказа уже имеет отдельный массив входов, а invoiceLabel ждёт отдельные параметры. ...$arguments раскладывает элементы массива по местам вызова: первый TEA попадает в sku, второй 2 — в quantity. Функция возвращает готовую подпись; прямой echo показывает её. Уведомление и запись в файл могли бы использовать ту же строку после присваивания.
| Форма входа | Параметры invoiceLabel | Возвращённая строка |
|---|---|---|
| [TEA, 2] | sku = TEA, quantity = 2, warehouse = main по умолчанию. | TEA:2@main |
| [quantity → 4, sku → TEA] | Строковые ключи назначают параметры по именам. | TEA:4@main |
| [TEA, 5] и warehouse: north | Два позиционных из распаковки, затем свободный именованный. | TEA:5@north |
У вас уже есть ["TEA", 2], а функция принимает два параметра. invoiceLabel(...$arguments) передаст элементы по отдельности. Без троеточия весь массив попадёт в первый параметр. Распаковывать можно array и Traversable, например ArrayIterator.
<?php
function invoiceLabel($sku, $quantity, $warehouse = 'main') {
return $sku . ':' . $quantity . '@' . $warehouse;
}
$arguments = ['TEA', 2];
echo invoiceLabel(...$arguments), "\n";
$iterator = new ArrayIterator(['COFFEE', 1]);
echo invoiceLabel(...$iterator), "\n";
echo invoiceLabel('TEA', ...[3]), "\n";
echo invoiceLabel(...['quantity' => 4, 'sku' => 'TEA']), "\n";
echo invoiceLabel(...['TEA', 5], warehouse: 'north'), "\n";
Ожидаемый результат
TEA:2@main
COFFEE:1@main
TEA:3@main
TEA:4@main
TEA:5@north
В объявлении ... собирает значения, а в вызове распаковывает их. ArrayIterator — готовый перебираемый объект, поэтому подходит как Traversable. Целочисленные ключи дают позиционные аргументы в порядке обхода; строковые ключи с PHP 8.0 становятся именами параметров. Именованный warehouse после распаковки допустим с PHP 8.1.
| Сочетание | Правило |
|---|---|
| Обычные позиционные, затем ... | Разрешено: invoiceLabel("TEA", ...[3]) |
| ... затем новый позиционный аргумент | Так писать нельзя; объедините позиционные данные до вызова |
| Строковые ключи массива в ... | С PHP 8.0 — имена параметров, а не случайные подписи |
| Числовой ключ после строкового при обходе | Ошибка: позиционный аргумент не может идти после именованного |
| ... затем именованный параметр | С PHP 8.1 разрешено, если он не заполняет уже занятый параметр |
Не распаковывайте JSON целиком в публичную функцию только потому, что ключи похожи: внешние поля могут быть лишними, отсутствовать или поменять смысл. Сначала проверьте структуру и явно выберите аргументы. В обычном массиве одинаковые строковые ключи уже перезаписываются при построении массива; это ещё не проверка дубликатов аргументов вызова.
Именованные аргументы: читаемый вызов без копирования defaults
У delivery два логических входа: express отвечает за срочность, door — за доставку до двери. Если нужны Казань и пункт выдачи, caller передаёт city и door, а express остаётся false. Все варианты ниже возвращают строку: функция не заказывает курьера и не создаёт отправление. Тернарное условие ? вариантДа : вариантНет выбирает фрагмент текста внутри этой строки.
У delivery("Казань", false, false) два одинаковых false трудно различить. Вызов delivery(city: "Казань", door: false) показывает смысл и оставляет express по умолчанию. Имена после двоеточия не выдумываются: они должны соответствовать объявлению функции.
Именованный аргумент — идентификатор door:, не переменная $door:. Для динамического набора используйте проверенный массив с последующей распаковкой. Даже зарезервированные слова могут быть именами параметров в таком контексте, например array: у подходящего API.
<?php
function delivery($city, $express = false, $door = true) {
return $city . ': ' . ($express ? 'срочно' : 'обычно')
. ', ' . ($door ? 'до двери' : 'пункт выдачи');
}
echo delivery(city: 'Казань', door: false), "\n";
echo delivery(door: false, city: 'Казань'), "\n";
echo delivery('Казань', express: true), "\n";
$selected = ['city' => 'Казань', 'door' => false];
echo delivery(...$selected), "\n";
echo implode(',', array_fill(value: 'TEA', count: 2, start_index: 0)), "\n";
Ожидаемый результат
Казань: обычно, пункт выдачи
Казань: обычно, пункт выдачи
Казань: срочно, до двери
Казань: обычно, пункт выдачи
TEA,TEA
После имени параметра стоит двоеточие, затем значение. Порядок двух именованных аргументов не изменил результат, а express удалось пропустить. Литерал имени не содержит $; динамический выбор передан через проверенный массив для распаковки. Встроенная array_fill демонстрирует такой же синтаксис.
Все обычные позиционные аргументы ставят раньше именованных. Между самими именованными порядок назначения параметрам неважен, но выражения аргументов всё равно вычисляются в порядке написания. Для продвинутого читателя: эти правила относятся и к методам, включая конструктор.
Ошибка имени — ошибка API; variadic меняет поведение
Ошибки связи аргументов возникают до выполнения полезного тела. В первой группе маленькие безымянные функции откладывают ошибочные вызовы: внешний foreach запускает их по одной через $call(), чтобы catch смог отдельно показать каждую причину. Если такое хранение действия ещё незнакомо, сначала изучите замыкание как значение. Здесь таблица ошибочных вызовов — средство изучения договора, не вход от посетителя.
<?php
function reserve($sku, $quantity = 1) {
return $sku . ':' . $quantity;
}
$calls = [
'Позиция и имя заняли sku' => function () { return reserve('TEA', sku: 'COFFEE'); },
'Распаковка и имя заняли quantity' => function () { return reserve(...['TEA', 2], quantity: 3); },
'Два именованных заняли sku' => function () { return reserve(sku: 'TEA', sku: 'COFFEE'); },
'Неизвестное имя count' => function () { return reserve(sku: 'TEA', count: 2); },
'Позиция после строкового ключа' => function () { return reserve(...['sku' => 'TEA', 2]); },
];
foreach ($calls as $label => $call) {
try {
$call();
} catch (Error $error) {
echo $label, ': Error', "\n";
}
}
Ожидаемый результат
Позиция и имя заняли sku: Error
Распаковка и имя заняли quantity: Error
Два именованных заняли sku: Error
Неизвестное имя count: Error
Позиция после строкового ключа: Error
Замыкания откладывают каждый неправильный вызов до отдельного try/catch. Так видно пять независимых проблем, а не только первую. Текст системной ошибки может меняться между версиями, поэтому выводим собственную метку и тип. Тело reserve до результата ни в одном случае не дошло.
Variadic меняет правило неизвестных имён: они могут попасть в details, поэтому опечатка не обязана автоматически стать Error. Покажем это без JSON: сохранённые именованные значения выводятся обычным foreach, затем собственный договор отклоняет лишнее имя.
<?php
function eventDetails(string $type, ...$details): array
{
foreach ($details as $name => $value) {
if ($name !== 'sku' && $name !== 'quantity') {
throw new InvalidArgumentException('Неизвестное поле: ' . $name);
}
}
return ['type' => $type, 'details' => $details];
}
$event = eventDetails('stock.changed', sku: 'TEA-01', quantity: 2);
echo 'Событие: ', $event['type'], "\n";
foreach ($event['details'] as $name => $value) {
echo $name, ': ', $value, "\n";
}
try {
eventDetails('stock.changed', quanity: 2);
} catch (InvalidArgumentException $error) {
echo 'Событие не принято: ', $error->getMessage(), "\n";
}
Ожидаемый результат
Событие: stock.changed
sku: TEA-01
quantity: 2
Событие не принято: Неизвестное поле: quanity
Второй и третий именованные аргументы собраны в details под строковыми ключами. Возвращённый массив event используется для сообщения и перебора полей. Ошибочное quanity тоже попало бы в variadic; собственный if отклонил его. Проверка разрешённых имён ещё не проверяет обязательность sku, тип quantity и его диапазон: это отдельные части полного договора события.
Названия параметров становятся частью внешнего контракта, если код вызывает их по имени. Переименование $quantity в $count может сломать вызовы, даже если типы и тело не изменились. Это важно для библиотек, интерфейсов и обновления SDK; согласовывайте имена в реализациях.
Неизвестный именованный аргумент вызывает Error только когда его не принимает variadic. С variadic опечатки превращаются в дополнительные ключи — проверяйте их самостоятельно. Дубликат значения одного параметра всегда ошибка, независимо от того, пришло первое значение позиционно, по имени или распаковкой.
Возврат — одно значение; несколько результатов помещают в структуру
Ниже invoiceStats возвращает одну структуру с двумя назначениями: count понадобится в подписи объёма, total — в финансовом результате. Выражение вызова сначала получает весь массив, затем левая деструктуризация читает поля. Это не два отдельных return и не несколько результатов, самостоятельно появившихся снаружи.
- amounts получает список двух чисел.
- Местный total накапливает 0 → 100 → 350.
- Возвращается один массив [count → 2, total → 350].
- Левая раскладка присваивает count = 2 и total = 350.
- Вызывающий echo оформляет строку «Строк: 2; сумма: 350».
return завершает текущий вызов и отдаёт значение месту вызова. Это может быть число, строка, массив, объект или другое значение; печать при этом не происходит. Без return и при пустом return обычная завершившаяся функция возвращает null.
Чтобы отдать и сумму, и число строк, верните один массив с именами полей. Для доменного API позже можно выбрать объект результата. void документирует действие без полезного результата; never — более строгий договор «вызов не завершится обычным возвратом», например он всегда бросает исключение.
<?php
function invoiceStats(array $amounts): array {
$total = 0;
foreach ($amounts as $amount) {
$total += $amount;
}
return ['count' => count($amounts), 'total' => $total];
}
['count' => $count, 'total' => $total] = invoiceStats([100, 250]);
echo 'Строк: ', $count, '; сумма: ', $total, "\n";
function writeImportMarker(): void {
echo "Импорт проверен\n";
}
$result = writeImportMarker();
echo $result === null ? "Результат вызова: null\n" : "Другой результат\n";
Ожидаемый результат
Строк: 2; сумма: 350
Импорт проверен
Результат вызова: null
Функция возвращает одно значение — массив, а деструктуризация извлекает его поля в две переменные. writeImportMarker выполняет действие и имеет тип void: полезного результата не возвращает, значение завершившегося вызова — null. echo не подменяет return.
Если внутри try есть finally, он выполняется перед фактическим уходом из функции. Возврат из finally способен заменить прежний результат или скрыть ошибку, поэтому очистка ресурса обычно не должна содержать return. Подробности — обработка исключений.
Возврат по ссылке: выдаём доступ к ячейке, а не ускоряем результат
| Где стоит & | Какую связь создаёт |
|---|---|
array &$cart в параметрах | Тело получает возможность менять исходный массив cart. |
function &quantitySlot | Разрешает вернуть наружу ссылку на изменяемую ячейку. |
$quantity =& quantitySlot(...) | Вызывающий действительно связывает своё имя с найденной ячейкой. |
$copy = quantitySlot(...) | Обычное присваивание берёт значение, не сохраняет ссылочную связь. |
Таким образом, присваивание quantity = 5 изменяет cart[TEA], а copy = 99 после обычного присваивания не меняет корзину. Возврат ссылки нужен только когда вызывающий должен получить именно право менять ячейку; он не делает возвращённое число «более правильным» и не является универсальной оптимизацией.
Иногда функция должна найти конкретную изменяемую ячейку, после чего вызывающий хочет присваивать ей значения. Это отдельный контракт: function &quantitySlot(...) плюс $quantity =& quantitySlot(...). Возвращают именно переменную/элемент, не временное арифметическое выражение.
Для обычной функции расчёта это излишне: используйте return. Не применяйте возврат по ссылке как универсальный способ ускорить массивы — он меняет возможность мутации и усложняет контроль состояния.
<?php
function &quantitySlot(array &$cart, string $sku): int {
if (!array_key_exists($sku, $cart)) {
$cart[$sku] = 0;
}
return $cart[$sku];
}
$cart = ['TEA' => 2];
$quantity =& quantitySlot($cart, 'TEA');
$quantity = 5;
echo 'TEA=', $cart['TEA'], "\n";
unset($quantity);
$copy = quantitySlot($cart, 'TEA');
$copy = 99;
echo 'После обычного присваивания TEA=', $cart['TEA'], "\n";
Ожидаемый результат
TEA=5
После обычного присваивания TEA=5
Передача $cart по ссылке даёт функции доступ к внешнему массиву; & перед именем функции разрешает вернуть ссылку на найденную ячейку. В строке $quantity =& quantitySlot(...) выполняется присваивание по ссылке; не путайте порядок символов с побитовым &=. Без =& вызывающий получит обычное значение. Ссылка открывает обход любых будущих проверок количества — для публичного API чаще лучше операция setQuantity с проверкой.
Когда ссылка больше не нужна, unset удаляет этот псевдоним; сам элемент корзины остаётся. Для бизнес-объекта выдача такой ссылки обходит проверку допустимых значений — это причина предпочитать методы изменения с понятным договором.
Объявление, время доступности и ограничения имён
Следующий опыт нужен для чтения файлов, где объявления находятся ниже вызова или внутри ветки. Он проверяет время появления имени, а не предлагает регистрировать функции при каждом запросе вручную. normalLabel возвращает строку сразу; optionalLabel доступна только после выполненной ветви; legacyLabel становится именованной функцией после выполнения внешнего регистратора.
Имя пользовательской функции выбирают как ясное действие: calculateTotal, formatAddress. Оно начинается с буквы/подчёркивания, далее допустимы буквы, цифры и подчёркивания; проще придерживаться ASCII и одного стиля. ASCII-регистр при поиске функции не различается, но в проекте сохраняйте написание объявления.
Имя регистрируется в пространстве имён, а не создаёт перегрузку по типу/числу параметров. Две одноимённые функции с разными сигнатурами объявлять нельзя; переопределить или удалить обычную уже объявленную функцию тоже нельзя.
<?php
echo normalLabel(), "\n";
function normalLabel() { return 'Обычная функция доступна'; }
echo function_exists('optionalLabel') ? "Есть optional\n" : "Пока нет optional\n";
if (true) {
function optionalLabel() { return 'Условие пройдено'; }
}
echo optionalLabel(), "\n";
function registerLegacyFormatter() {
function legacyLabel() { return 'Вложенное имя доступно снаружи'; }
}
registerLegacyFormatter();
echo legacyLabel(), "\n";
Ожидаемый результат
Обычная функция доступна
Пока нет optional
Условие пройдено
Вложенное имя доступно снаружи
Обычное объявление в загруженном файле доступно до строки объявления. Условное объявление появляется после выполнения ветки. Именованная функция внутри другой не становится её частной локальной функцией: имя регистрируется в соответствующем пространстве имён. Повторный вызов registerLegacyFormatter попытался бы объявить его ещё раз — так вспомогательные функции обычно не строят.
Для локального помощника внутри другой функции обычно подходит замыкание, а не вложенное именованное объявление. Функции могут вызывать друг друга и себя: рекурсия требует условия остановки и контроля глубины; бесконечная рекурсия — программная ошибка, не способ ожидания.
Функция по имени и метод в переменной: разрешённый выбор поведения
В настройках выбран режим loud. Чтобы разрешить только два известных действия, таблица formatters связывает режимы со своими функциями. Значение выбора не становится именем произвольной команды. $format получает строку loudTitle, а запись $format(...) вызывает именно эту функцию; вернувшуюся строку можно сохранить в готовую подпись.
| Действие | Вход | Выход или эффект |
|---|---|---|
| plainTitle | Строка с возможными пробелами. | Новая строка после trim. |
| loudTitle | Та же строка. | Новая строка после trim и strtoupper; этот пример использует ASCII-код tea. |
| printLine | Уже готовая строка. | Вывод строки и перевода; полезного возвращённого текста нет. |
strtoupper в этом опыте не обещает менять регистр произвольного кириллического текста; для такой задачи выбирается соответствующая Unicode-функция. Методы объектов в следующем опыте — отдельное позднее применение после объекта и метода.
Сервис поддерживает два формата заголовка. Вместо множества условий можно выбрать известный обработчик из таблицы. $format(...) — вызов значения, которое допускает вызов, то есть callable. Проверка is_callable подтверждает возможность вызова в текущем контексте, но не право пользователя запускать его.
<?php
function plainTitle($title) { return trim($title); }
function loudTitle($title) { return strtoupper(trim($title)); }
$formatters = ['plain' => 'plainTitle', 'loud' => 'loudTitle'];
$choice = 'loud';
if (!array_key_exists($choice, $formatters)) {
throw new InvalidArgumentException('Неизвестный формат');
}
$format = $formatters[$choice];
echo $format(' tea '), "\n";
function printLine($text) { echo $text, "\n"; }
$writer = 'printLine';
$writer('Готово');
Ожидаемый результат
TEA
Готово
Круглые скобки после $format вызывают функцию, имя которой хранится в строке. Это удобная таблица допустимых действий. echo — конструкция языка, поэтому строка "echo" не заменит функцию; для неё мы сделали обычную обёртку printLine. Нельзя брать имя вызываемой функции прямо из запроса посетителя.
<?php
class Labels {
public static string $format = 'поле';
public static function format(string $sku): string { return '[' . $sku . ']'; }
public function item(string $sku): string { return 'Товар ' . $sku; }
}
$labels = new Labels();
$method = 'item';
echo $labels->$method('TEA'), "\n";
$staticMethod = 'format';
echo Labels::$format, "\n";
echo Labels::$staticMethod('COFFEE'), "\n";
$callbacks = [[$labels, 'item'], [Labels::class, 'format'], 'Labels::format'];
foreach ($callbacks as $callback) {
echo $callback('TEA'), "\n";
}
Ожидаемый результат
Товар TEA
поле
[COFFEE]
Товар TEA
[TEA]
[TEA]
Массив [объект, имя] вызывает метод экземпляра, [класс, имя] — статический метод. Labels::$format без скобок читает статическое свойство; Labels::$staticMethod(...) берёт имя метода из текущей переменной и вызывает его. Это синтаксис чтения чужого кода; обычный прямой вызов понятнее, когда выбор не динамический.
Формы callable: имя функции строкой, имя статического метода строкой, массив [объект/класс, метод], Closure и объект с __invoke. Приведённые методы публичные; private/protected зависят от области доступа. Современный способ сохранить допустимый вызов — first-class callable.
Встроенная функция: сначала контракт, потом пример вызова
При знакомстве с каждой встроенной функцией определяйте, куда уйдёт результат и меняется ли вход. Важна сигнатура, а не предположение по имени. У str_replace результатом является новая строка; у sort полезное изменение находится в самом переданном массиве, а возвращается только true при нормальном завершении в PHP 8.2+. Это различие решает, нужно ли присваивание результата.
| Функция в опыте | Вход | Где результат |
|---|---|---|
| str_replace | Искомый фрагмент, замена, исходная строка. | Возвращённая новая строка; используем updatedCaption. |
| sort | Изменяемый массив prices. | Упорядоченные значения остаются в prices; числовые ключи переиндексированы. |
| strlen | Строка TEA-01. | Возвращённый int 6 в bytes; это байты, не универсальное число букв. |
| function_exists | Строка с именем функции. | bool для ветви if; наличие не означает доверие входным данным. |
Функции ядра и расширений уже реализованы в PHP, однако их договоры различаются. Читайте вход, результат, изменение аргументов, возможные false/исключения и версии. Не принимайте любой «ложный» результат за ошибку: например, найденная позиция 0 — корректна.
Доступность функции может зависеть от модуля: GD для изображений, cURL для HTTP-клиента, mbstring для части Unicode-операций. Проверьте окружение приложения, включая различие CLI и веб-PHP; phpinfo не стоит выставлять публичной страницей.
<?php
$caption = 'tea-old';
$updatedCaption = str_replace('-old', '-new', $caption);
echo 'Исходная подпись: ', $caption, "\n";
echo 'Готовая новая подпись: ', $updatedCaption, "\n";
$prices = [300, 100, 200];
sort($prices);
foreach ($prices as $index => $price) {
echo 'Цена ', $index + 1, ': ', $price, ' руб.', "\n";
}
try {
strlen([]);
} catch (TypeError $error) {
echo "Длина не рассчитана: strlen ожидает строку\n";
}
if (function_exists('strlen')) {
$bytes = strlen('TEA-01');
echo 'Артикул TEA-01 занимает ', $bytes, ' байт', "\n";
}
Ожидаемый результат
Исходная подпись: tea-old
Готовая новая подпись: tea-new
Цена 1: 100 руб.
Цена 2: 200 руб.
Цена 3: 300 руб.
Длина не рассчитана: strlen ожидает строку
Артикул TEA-01 занимает 6 байт
str_replace вернула новую строку в updatedCaption и сохранила caption. sort изменила prices на месте; возвращаемый признак успеха нельзя присваивать вместо массива. strlen вернула длину ASCII-артикула в байтах, а function_exists разрешила ветвь вызова по факту доступности имени. Ошибка неправильного типа массива для strlen показана отдельно.
В PHP 8 ошибочные типы у встроенных функций часто приводят к TypeError, значения вне допустимой области — к ValueError; у каждой функции всё равно проверяйте собственный контракт. Нельзя рассчитывать на старое поведение «вернёт null, если передали что угодно».
Полный расчёт: готовые суммы строк, доставка и понятная неудача
Функция как отдельная операция с входом и выходом
Сайт много раз считает сумму заказа. Один раз описываем правило и вызываем его для разных корзин.
- Передать данные
Суммы строк: 10000 и 25000 копеек. Доставка: 29900 копеек.
- Выполнить calculateTotal
Проверяет готовые суммы строк и складывает их с доставкой. Количества здесь уже учтены.
- Получить результат
return передаёт вызывающему коду число 64900 — итог в копейках.
Аргументы — данные конкретного вызова, параметры — имена для их получения внутри функции. return отдаёт результат коду, который вызвал функцию; echo выводит текст. Это разные действия.
В корзине уже рассчитаны отдельные строки: 10000 и 25000 копеек. Требуется прибавить доставку 29900 копеек и получить 64900 копеек к оплате. У этой функции другой вход, чем у первого lineTotal: она не умножает цену на количество, а складывает уже рассчитанные суммы. Все входы и выход измеряются в копейках одной валюты.
| Часть договора calculateTotal | Конкретный смысл |
|---|---|
array $lineTotalsKopecks | Один аргумент-массив; каждый элемент обязан быть неотрицательным int в копейках. Тип array сам не проверяет элементы. |
int $deliveryKopecks = 0 | Доставка в копейках. Если аргумент пропущен — бесплатная доставка, иначе переданное значение; отрицательная сумма запрещена. |
: int | При успехе возвращается целая сумма. Функция ничего не печатает и не читает форму. |
InvalidArgumentException | Неверный по смыслу вход прерывает расчёт; частичный итог не возвращается как будто он полный. |
Добавим обработку неудачи. is_int($lineTotal) возвращает true только для целого числа. ! меняет true на false и наоборот; || означает «хотя бы одно условие истинно». Поэтому проверка отклоняет нецелый тип или отрицательное число. is_int: контракт и примеры.
throw new InvalidArgumentException(...) создаёт объект ошибки с сообщением и прекращает обычное выполнение этого пути. try { ... } catch (InvalidArgumentException $error) { ... } вызывает расчёт и обрабатывает именно этот вид неудачи. Имя error получает объект; $error->getMessage() вызывает его метод, возвращающий текст сообщения. Это применение исключений после базового return; без catch ошибка передалась бы дальше вызывающему коду.
<?php
declare(strict_types=1);
function calculateTotal(array $lineTotalsKopecks, int $deliveryKopecks = 0): int
{
if ($deliveryKopecks < 0) {
throw new InvalidArgumentException('Доставка не может быть отрицательной');
}
$total = $deliveryKopecks;
foreach ($lineTotalsKopecks as $lineTotal) {
if (!is_int($lineTotal) || $lineTotal < 0) {
throw new InvalidArgumentException('Сумма строки должна быть целым неотрицательным числом');
}
$total = $total + $lineTotal;
}
return $total;
}
$payable = calculateTotal([10000, 25000], deliveryKopecks: 29900);
echo 'К оплате: ', $payable, ' коп.', "\n";
try {
$invalidTotal = calculateTotal([10000, -500]);
echo 'К оплате: ', $invalidTotal, ' коп.', "\n";
} catch (InvalidArgumentException $error) {
echo 'Заказ не рассчитан: ', $error->getMessage(), "\n";
}
Ожидаемый результат
К оплате: 64900 коп.
Заказ не рассчитан: Сумма строки должна быть целым неотрицательным числом
В первом вызове total проходит 29900 → 39900 → 64900 и только после цикла возвращается в payable. Во втором доставка равна 0 по умолчанию; первая строка принята, но -500 прерывает функцию. Присваивание invalidTotal не завершается, echo после него пропускается, catch показывает причину.
Пустой массив строк по этому договору возвращает только доставку. Если пустой заказ запрещён, это отдельное правило оформления. Переполнение суммы тоже требует отдельного предела: пример рассчитан на значения, сумма которых помещается в int. Ошибка скалярного типа при вызове — TypeError, а не InvalidArgumentException; показанный catch намеренно отвечает за описанные смысловые проверки.
- Проверенный массив строк и доставка → calculateTotal.
- При успехе return отдаёт полную сумму в payable.
- Обработчик заказа может сохранить payable и отдельно сформировать HTML или другой ответ.
- При неудаче нет разрешённого итога для оплаты: обработчик показывает ошибку и предлагает исправить ввод.
Вернуть обработанную копию списка и сохранить исходные данные
Редактор прислал теги: « PHP », пустую строку, повтор PHP и «0». Нужно получить PHP и 0, сохранив исходный список для отчёта. Передадим array по значению, построим новый result и вернём его. Функция не будет печатать JSON или менять внешний input: вызывающий код увидит готовые строки и сам оформит результат.
| Инструмент внутри normalizeTags | Вход → результат |
|---|---|
is_string($tag) | Любое значение → bool: можно ли обращаться с ним как с текстом. |
trim($tag) | Строка → новая строка без стандартных пробельных/служебных байтов с краёв; исходная внешняя ячейка не меняется. Это не полная обработка всех Unicode-пробелов. |
$tag !== '' | Строгое отличие от пустой строки: текст «0» сохраняется. |
array_unique($result) | Массив строк → массив без повторных строк; сохранившиеся ключи могут иметь промежутки. |
array_values(...) | Массив → список значений с ключами 0, 1, 2…; удобно для следующего обычного обхода. |
<?php
declare(strict_types=1);
function normalizeTags(array $tags): array
{
$result = [];
foreach ($tags as $tag) {
if (!is_string($tag)) {
throw new InvalidArgumentException('Каждый тег должен быть строкой');
}
$tag = trim($tag);
if ($tag !== '') {
$result[] = $tag;
}
}
return array_values(array_unique($result));
}
$input = [' PHP ', '', 'PHP', '0'];
$tags = normalizeTags($input);
foreach ($tags as $index => $tag) {
echo 'Тег ', $index + 1, ': ', $tag, "\n";
}
echo 'Исходное первое значение: [', $input[0], "]\n";
Ожидаемый результат
Тег 1: PHP
Тег 2: 0
Исходное первое значение: [ PHP ]
После trim и удаления пустоты result содержит PHP, PHP, 0. array_unique оставляет PHP и 0, array_values собирает плотный список, return отдаёт его в tags. Скобки в последней строке показывают сохранённые пробелы исходного input[0]. Это проверяемый результат преобразования, а не только проверка типа массива.
Каждый этап имеет отдельную роль: trim обрабатывает края строки; array_unique убирает повторы; array_values переиндексирует. Ни один не экранирует HTML. Перед показом пользовательских тегов на веб-странице применяется подходящий вывод, например htmlspecialchars для текста HTML.
Здесь независимость относится к обычным массивам строк. Передача объекта по значению сохраняет его идентификатор: изменение свойства может быть видно снаружи. Параметр с & явно разрешает изменение самой переданной переменной. Эти отдельные договоры подробно разобраны выше; не добавляйте ссылку ради предположения о скорости.
Собрать несколько групп сообщений: что именно попадает в ...$groups
Проверка адреса вернула одну ошибку, проверка доставки ни одной, проверка телефона ещё одну. Нужно получить единый список для показа. Запись array ...$groups — последний параметр: принимает ноль или больше отдельных аргументов, каждый обязан быть массивом; внутри groups они становятся элементами нового внешнего массива.
| Вызов | Что получает местная $groups |
|---|---|
collectErrors(['Нет адреса']) | Один элемент: внутренний массив с одним сообщением. |
collectErrors(['Нет адреса'], [], ['Нет телефона']) | Три элемента-массива, включая пустой. |
collectErrors() | Пустой внешний массив; итог тоже пустой. |
<?php
function collectErrors(array ...$groups): array
{
$result = [];
foreach ($groups as $group) {
foreach ($group as $message) {
$result[] = $message;
}
}
return $result;
}
$errors = collectErrors(['Не указан адрес'], [], ['Нет телефона']);
foreach ($errors as $message) {
echo 'Нужно исправить: ', $message, "\n";
}
Ожидаемый результат
Нужно исправить: Не указан адрес
Нужно исправить: Нет телефона
Внешний цикл функции берёт очередную группу; внутренний переносит все её сообщения. Пустая группа не добавляет ничего. return стоит после обоих циклов и отдаёт две строки в errors. Затем отдельный внешний foreach добавляет понятную подпись. Договор предполагает, что сообщения внутри групп уже являются строками: array проверяет только каждую группу.
Обратное действие пишется при вызове: collectErrors(...$groups) распаковывает уже существующий список групп в отдельные аргументы. Троеточие в объявлении собирает, в вызове распаковывает. Следующий полный файл показывает это на тех же данных.
<?php
function collectErrors(array ...$groups): array
{
$result = [];
foreach ($groups as $group) {
foreach ($group as $message) {
$result[] = $message;
}
}
return $result;
}
$groups = [['Не указан адрес'], [], ['Нет телефона']];
$errors = collectErrors(...$groups);
foreach ($errors as $message) {
echo 'Нужно исправить: ', $message, "\n";
}
Ожидаемый результат
Нужно исправить: Не указан адрес
Нужно исправить: Нет телефона
Здесь список имеет числовые ключи 0, 1, 2; они дают позиционные аргументы. Без ... функция получила бы один аргумент, содержащий все группы внутри, и форма результата оказалась бы другой. Строковые ключи при распаковке имеют правила именованных аргументов; произвольный JSON не следует напрямую превращать в вызов чужого API.
Память для groups и result нужна уже в этом простом решении. Если вход очень большой или должен обрабатываться по мере поступления, выбирайте другой договор, например iterable и поэлементную обработку. Variadic описывает удобство вызова, а не гарантирует потоковую обработку.
Согласовать вход, результат, ошибки и дальнейшее действие
Теперь можно читать сигнатуру вместе с полезным результатом. Наличие имени и типа недостаточно: укажите единицы, допустимую форму, смысл пустого значения, побочные действия и способ отказа. Для каждой функции предыдущие примеры показали место, которое получает результат; в собственной программе называйте его столь же явно.
| Функция этой главы | Что приходит и где используется |
|---|---|
| lineTotal | Цена одной упаковки и количество → сумма строки; дальше прибавляется доставка или сумма другой строки. |
| calculateTotal | Массив готовых сумм в копейках и доставка → полный int к оплате; некорректное значение прерывает расчёт описанным исключением. |
| normalizeTags | Массив строк → новый список непустых уникальных строк; исходный набор сохраняется для отчёта. |
| collectErrors | Несколько массивов сообщений → один список; интерфейс показывает его обычным foreach. |
| showPickupReminder | Нет входа → выполненный вывод; полезного возвращаемого значения нет, поэтому : void. |
never обещает, что функция вообще не завершится обычным возвратом, например всегда бросит исключение. Это не то же, что void: функция void может закончиться и вернуть управление. Union-тип допустим для осмысленных альтернатив; подробно объясняйте, какой вариант при каком условии получает вызывающий код. Тип int у ID не проверяет право пользователя на этот ID.
Упражнение: функция оформления читает форму, считает сумму, отправляет письмо и печатает ответ. Выделите чистый расчёт, который принимает проверенные значения и возвращает сумму. Получение HTTP-данных, сохранение заказа и отправка сообщения имеют свои договоры и обработку сбоев. Решение начинается с массива входных сумм и результата payable, а не с добавления ещё одного echo внутрь расчёта.
Проверьте самостоятельно пустой список, один элемент, несколько элементов и недопустимый элемент. Сначала определите ожидаемое поведение словами. Только после этого добавляйте проверку и выбирайте, какой результат или ошибка вернётся в точное место вызова.
Источники и обсуждения
- PHP Manual: functions.arguments
- PHP Manual: functions.returning-values
- PHP Manual: language.types.declarations
- PHP Manual: language.references.pass
- PHP Manual: functions.user-defined
- PHP Manual: function.return
- PHP Manual: function.echo
- PHP: параметры и аргументы — полная тематическая основа
- PHP: пользовательские функции
- PHP: возврат значений
- PHP: передача по ссылке
- PHP: возврат ссылок
- PHP: объекты и ссылки
- PHP: функции переменных
- PHP: встроенные функции
- PHP: анонимные функции
- PHP: стрелочные функции
- PHP: first-class callable
- PHP: callable
- PHP: func_get_args
- PHP: func_num_args
- PHP: func_get_arg
- PHP Manual: language.variables.basics
- PHP Manual: language.variables.scope
- PHP Manual: language.types.boolean
- PHP Manual: language.types.array
- PHP Manual: language.operators.arithmetic
- PHP Manual: language.operators.comparison
- PHP Manual: control-structures.if
- PHP Manual: control-structures.foreach
- PHP Manual: function.is-array
- PHP Manual: function.array-fill
- PHP Manual: function.array-replace-recursive
- PHP Manual: function.array-multisort
- PHP Manual: function.array-udiff