Руководство по замыканиям и стрелочным функциям в PHP

Источник: «A Beginner's Guide to Closures and Arrow Functions in PHP»
Прочтите о разнице между замыканиями и стрелочными функциями в PHP. В этой статье рассказывается, что они из себя представляют, чем отличаются, и как вы можете использовать их в своём коде.

Целевая аудитория

Эта статья предназначена для разработчиков, которые ещё плохо знакомы с PHP или программированием в целом. Мы рассмотрим основы того, что такое замыкания и стрелочные функции, как они работают, и когда вы можете использовать их своём коде. Поэтому, если вы уже знакомы с любой из этих функций, возможно, эта статья не для вас; но это может быть хорошим шансом повторить некоторые основы PHP.

Введение

Как PHP веб-разработчик, почти неизбежно, что в какой-то момент вы столкнётесь с замыканиями и стрелочными функциями. Это удобные функции, которые можно использовать для написания довольно мощного кода при правильном использовании.

Но признаюсь, когда я впервые начал изучать PHP (и программирование в целом), я не имел особого представления, что такое замыкания. Я знал, что они нужны, но не совсем понимал, ка они работают, когда их использовать и чего они достигают.

Итак, в этой статье я расскажу об основах замыканий и стрелочных функций в PHP. Мы обсудим, что делает каждый из них и где вы можете их использовать. А затем рассмотрим различия между ними.

Замыкания в PHP

Замыкания — это анонимные функции, которые можно хранить в переменных, передавать в качестве аргументов другим функциям или возвращать в виде значений из других функций.

Если вы знакомы с Laravel, то возможно использовали замыкания при добавлении транзакций базы данных.

Замыкания определяются с помощью ключевого слова function, за которым следует список параметров и тело, заключённое в фигурные скобки. Давайте рассмотрим очень простой пример. Мы создадим замыкание, которое складывает два числа вместе, а затем возвращает их:

$addition = function($a, $b) {
return $a + $b;
};

В приведённом выше коде мы создали замыкание и сохранили его в переменную с именем $additional. Однако на самом деле мы ещё не вызывали функцию, поэтому ничего не произошло. Чтобы вызвать функцию, мы можем сделать следующее:

$result = $addition(2, 3);

echo $result; // Output: 5

Как видим, функция была вызвана, а результат сохранён в переменной $result. Затем мы выводим результат на экран.

Подобно тому, как вы могли бы указать подсказку типа в обычной функции, вы также можете использовать его в параметрах замыкания. Мы также можем добавить возвращаемый тип замыкания, чтобы гарантировать, что функция возвращает правильный тип. Давайте добавим подсказки типов и возвращаемый тип в наше замыкание:

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

Мы добавили подсказки типа к параметрам и тип возвращаемого значения к функции. Это означает, что если мы передадим в функцию что-либо кроме целого числа, PHP выдаст ошибку. Точно так же, если функция не возвращает целое число, PHP выдаёт ошибку.

Поскольку замыкания не запускаются до тех пор, пока не будут вызваны, они идеально подходят для использования в качестве обратных вызовов. Это замечательно, если вы используете такие функции, как array_map или array_filter, где вы можете определить, как должен обрабатываться каждый элемент массива. Давайте рассмотрим пример:

$names = ['john', 'jane', 'joe'];

$greeting = 'Hello, ';

$sentences = array_map(
callback: function (string $name) use ($greeting): string {
return $greeting.$name;
},
array: $names,
);

// $sentences would be equal to:
// [
// 'Hello, John',
// 'Hello, Jane',
// 'Hello, Joe',
// ]

Давайте быстро взглянем на то, что мы делаем с приведённым выше кодом. Мы создаём массив имён (можно предположить, что этот массив исходит из базы данных или какого-либо другого источника). Затем мы создаём переменную с именем $greeting и присваиваем ей строковое значение Hello. Опять, мы можем предположить, что это исходит из базы данных или другого источника. Затем мы создаём новый массив с именем $sentences и используем функцию array_map для сопоставления массива $names.

Затем функция array_map перебирает каждое из имён в массиве, который мы ей передали, и передаёт имя замыканию. Таким образом, замыкание будет выполняться один раз для каждого элемента массива.

Вы могли заметить, что мы используем use в объявлении замыкания. Но для чего это? Замыкания могут обращаться только к переменным, которые передаются как параметры функции или передаются из окружающей области видимости с использованием ключевого слова use. Если бы мы не использовали ключевое слово use, переменная $greeting не была бы доступна внутри замыкания, и при попытке запустить код возникала бы ошибка.

Стрелочные функции в PHP

Стрелочные функции аналогичны замыканиям, но они обеспечивают более короткий синтаксис для написания анонимных функций с более короткими и простыми выражениями.

Вместо того чтобы определять их с помощью ключевого слова function и заключать тело в фигурные скобки, как в случае с замыканием, стрелочные функции используют ключевое слово fn и одно выражение. Также важно отметить, что стрелочные функции должны возвращать значение. Поэтому их нельзя использовать для функций, которые не возвращают значения, в отличие от замыканий.

Давайте посмотрим, как мы могли бы написать замыкание из предыдущего примера, которое складывает два числа в виде стрелочной функции:

$addition = fn($a, $b) => $a + $b;

$result = $addition(2, 3);

echo $result; // Output: 5

Мы можем добавить подсказки типов параметров и задать возвращаемый тип так же, как мы делали с замыканиями:

$addition = fn(int $a, int $b): int => $a + $b;

$result = $addition(2, 3);

echo $result; // Output: 5

Как видите, синтаксис стрелочной функции короче синтаксиса замыкания. Это может значительно упростить их написание, особенно с более короткими функциями.

Разрешается только одно выражение

У стрелочных функций есть обратная сторона по сравнению с замыканиями. Они ограничены одним оператором, тогда как замыкания могут содержать несколько операторов.

Например, давайте представим, что у нас есть базовое замыкание, которое принимает имя, переводит первую букву переданного имени в верхний регистр, а затем возвращает с приветствием. Но если в замыкание передано имя Ashly, вместо этого мы вернём строку Hello, Ash Allen. Замыкание может выглядеть примерно так:

$greeting = 'Hello, ';

$greet = function (string $name) use ($greeting): string {
if ($name === 'Ashley') {
return 'Hello, Ash Allen';
}

$upperCasedName = ucfirst($name);

return $greeting.$upperCasedName;
};

echo $greet('Ashley'); // Output: Hello, Ash Allen
echo $greet('john'); // Output: Hello, John

Поскольку замыкание содержит два оператора, мы не можем просто отключить function для fn, чтобы преобразовать её в стрелочную функцию. Чтобы заставить её работать, потребуется рефакторинг. Мы могли бы сделать что-то вроде этого:

$greeting = 'Hello, ';

$greet = fn (string $name): string =>
$name === 'Ashley'
? 'Hello, Ash Allen'
: $greeting.ucfirst($name);

echo $greet('Ashley'); // Output: Hello, Ash Allen
echo $greet('john'); // Output: Hello, John

Как видно из приведённого выше кода, нам пришлось изменить способ написания кода внутри функции, чтобы его можно было выполнить одним выражением. Хотя я думаю, что, то, как мы его переписали, выглядит намного чище и легче для чтения. Я надеюсь, что это подчеркнуло тот факт, что стрелочные функции иногда могут быть более ограничивающими, чем замыкания.

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

В конце концов, гораздо важнее писать код, который легко понять и поддерживать, чем писать нечитаемый код только для того, чтобы попытаться уместить его в одну строку.

Однако, если вы пишете простые функции, содержащие только одно выражение, стрелочные функции — отличный способ для их написания. Например, мне нравиться использовать их для добавления ограничений к моим запросам к базе данных в Laravel приложениях. Предположим я хочу написать запрос, чтобы получить всех авторов (и некоторые статистические данные о них) из моей базы данных, опубликованных книг. Я мог бы написать запрос, используя замыкание, чтобы написать что-то вроде этого:

$authors = Author::query()
->whereHas(
'books',
function (Builder $query): Builder {
return $query->where('is_published', true);
}
)
->withCount([
'reviews' => function (Builder $query): Builder {
return $query->where('rating', 5);
},
'sales' => function (Builder $query): Builder {
return $query->where('purchased_at', '>=', now()->subDays(7));
}
])
->get();

В качестве альтернативы я мог бы написать тот же запрос, используя стрелочные функции, например:

$authors = Author::query()
->whereHas(
'books',
fn (Builder $query): Builder => $query->where('is_published', true)
)
->withCount([
'reviews' => fn (Builder $query): Builder => $query->where('rating', 5),
'sales' => fn (Builder $query): Builder => $query->where('purchased_at', '>=', now()->subDays(7)),
])
->get();

Автоматический захват переменных

Одно из самых больших различий между стрелочными функциями и замыканиями, о котором вы должны знать, заключает в том, что стрелочные функции автоматически захватывают переменные из родительской области. Это означает, что вам не нужно использовать ключевое слово use для захвата переменных, как при замыкании. Вы могли это заметить в приведённых выше примерах, где нам не нужно было использовать ключевое слово use для захвата переменной $greeting внутри стрелочной функции.

Когда использовать замыкания, а когда стрелочные функции

Вы должны всегда выбирать наиболее подходящий инструмент для работы. Таким образом, нет правильного или неправильного ответа, когда дело доходит до выбора между замыканиями и стрелочными функциями. Это больше о выборе того, кто больше всего подходит для поставленной задачи.

Я настоятельно рекомендую использовать тот, который позволяет писать наиболее читаемый и удобный код. Например, если вы обнаружили, что пытаетесь втиснуть много логики в стрелочные функции, то это может быть признаком того, что вместо этого следует переключиться на замыкания. Однако если вы пишете простые функции, содержащие только одно выражение, то стрелочные функции могут оказаться более подходящим подходом.

Вот несколько советов, которые помогут решить, что использовать…

Используйте замыкания когда:

Используйте стрелочные функции когда:

Заключение

Надеюсь, что эта статья дала вам представление о замыканиях и стрелочных функциях в PHP. Теперь вы должны знать достаточно, чтобы использовать их в своих проектах.

Дополнительные материалы

Предыдущая Статья

Новое в Symfony 6.3 — Улучшения WebProfiler

Следующая Статья

Новое в Symfony 6.3 — Улучшения DX (Часть 3)