В PHP становится все больше способов работы с функциями. Хотя ООП и является основной парадигмой для этого языка, процедурный и функциональный подходы тоже имеет право на жизнь в PHP. Давайте рассмотрим различные примеры работы с функциями в PHP 8.3. Данная статья подойдет для новичков и продолжающих.
Обычные функции
Начнем с самого простого способа создания функций, который существует с момента появления языка. Зарегистрируем константную функцию с помощью ключевого слова function:
function hello() {
return 'Hello World!';
}
Теперь эту функцию можно вызвать в любой части программы. А константная она, потому что всегда возвращает одно и то же значение. Давайте сделаем ее чуть интереснее и добавим параметры:
function createMessage($verb, $subject) {
return "$verb $subject!";
}
Теперь она не константная, ведь при различных параметрах будут получены реальные значения. В современном PHP данную функцию можно сделать еще лучше, добавив типизацию. На вход предполагаются две строки и возвращается также строка, укажем это:
function createMessage(string $verb, string $subject): string {
return "$verb $subject!";
}
Получаем вот такие результаты вызова:
createMessage('Hello', 'World'); // 'Hello World!'
createMessage(123, false); // '123 !'
В функцию все еще можно передать параметры неверного типа. Спасибо приведению типов, которое преобразует и числовое и булево значение в строковое. Но можно включить режим строгой типизации, добавив в начало файла строку declare(strict_types=1)
:
declare(strict_types=1);
function createMessage(string $verb, string $subject): string {
return "$verb $subject!";
}
createMessage('Hello', 'World'); // 'Hello World!'
createMessage(123, false); //Fatal error: Uncaught TypeError...
Вот теперь все будет точно работать как надо. Только объявление строгой типизации нужно добавлять в каждый файл – компромисс для обратной совместимости PHP с предыдущими версиями.
Это все база, от которой будем далее отталкиваться. Но если хочется проверить свои знания функций в PHP, то можно обратиться к разделу в официальной документации (EN/RU)
Вызываемый класс – Обращение к объекту, как к функции
Хотя в этой статье мы больше говорим о процедурном подходе, стоит упомянуть, что и экземпляры классов можно вызывать как обычные функции. Для этого в классе должен быть реализован метод __invoke
:
class Message {
public function __invoke(string $verb, string $subject): string {
return "$verb $subject!";
}
}
$message = new Message();
$message('Hello', 'World'); // Hello World!
Использование этого метода встречается редко, так как он сильно усложняет чтение кода, в котором экземпляр класса становится функцией. Но все таки встречается
Анонимные функции (Замыкания/Closures)
У функций описанных в предыдущем разделе есть важный общий момент – у них всех есть имя. Но мы можем создать и вариант функции без имени:
function(string $verb, string $subject): string {
return "$verb $subject!";
};
Такая функция называется анонимной ведь у нее нет имени. Еще в PHP такие функции называют замыканиями или closure. Это может сбить с толку людей, знакомых с JS, где замыкание – это про получение доступа к переменной вне функции. Но разработчики PHP не решили нас запутать, а просто взяли концепцию из C подобных языков, в которых замыкания – синтаксический сахар для создания анонимных функций. И в PHP тоже есть класс Closure
, и анонимная функция под капотом создает экземпляр этого класса, но о нем позднее, а пока вернемся к нашему примеру.
Хотя в нем синтаксически все верно, есть проблема – анонимную функцию никак не вызвать поэтому давайте запишем ее в переменную:
$createMessage = function(string $verb, string $subject): string {
return "$verb $subject!";
};
И теперь, хотя функция анонимна и не может быть вызвана самостоятельно, у нас есть переменная, которая указывает на функцию и дает возможность с ней работать:
$createMessage('Hello', 'World'); // 'Hello World!'
И самый частый способ использования анонимных функций – это их передача, в качестве параметра, в другие функции.
Callback функции
В PHP есть специальный тип callable
, который обозначает что-то, что можно вызвать как функцию. Он объединяет в себя различные типы, не все из которых выглядят очевидно:
Обычные функции / Строки
Функции созданные обычным способом можно вызвать, а значит они соответствуют типу callable. Давайте создадим такую:
function add(int $a, int $b): int {
return $a + $b;
}
И передадим их в функцию array_reduce, которая преобразует массив данных в соответствии с правилами, описанными в переданной callback функции:
array_reduce([3,2,1], add, 0);
Иииии получим ошибку Undefined constant "add"
, а все, потому что в PHP строка вне кавычек и без доллара в начале считается за константу. Но есть решение этой проблемы – нужно всего лишь обернуть имя функции в кавычки и она заработает:
array_reduce([3,2,1], 'add', 0); //6
В целом любую функцию в PHP можно вызывать используя кавычки: 'add'(1,2)
, но это только для тех, кому платят за каждый символ в коде.
Кстати таким образом можно использовать и встроенные в язык функции, например здесь функция trim
применилась ко всем
array_map('trim', [' foo ', 'bar ', ' baz']); //['foo', 'bar', 'baz']
Анонимные функции
В примере выше имя функции использовалось для указания на нее, но гораздо удобнее использовать анонимные функции, как callback функции. Доработаем предыдущий пример:
$add = function (int $a, int $b): int {
return $a + $b;
};
И теперь воспользуемся ей при вызове функции array_reduce:
array_reduce([3,2,1], $add, 0); //6
Но у анонимных функций есть еще одно преимущество, их необязательно присваивать переменной, можно сразу передать в функцию и сделать пространство имен еще чище:
array_reduce([3,2,1], function($a, $b) {
return $a + $b;
}, 0); //6
У анонимных функций есть и другие полезные особенности, о них поговорим чуть позже.
Массивы
Точнее один вид массива, состоящий из двух элементов, где первый элемент – это экземпляр класса, а второй – публичный метод класса:
class Math {
public function add (int $a, int $b): int {
return $a + $b;
}
}
В примере все та же функция add
, только теперь это уже метод класса Math
. Создадим экземпляр класса Math
и воспользуемся методом add
как callback функцией:
array_reduce([3,2,1], [new Math, 'add'], 0); //6
Проблема такой записи в том, что на ней может ломаться статический анализ кода текстового редактора и вместе с ним быстрый поиск метода. Хотя такое сейчас встречаю уже редко
Таким же способом можно вызывать и статические методы, только вместо экземпляра класса, нужно указать сам класс:
class Math {
public static function add (int $a, int $b): int {
return $a + $b;
}
}
array_reduce([3,2,1], [Math::class, 'add'], 0); //6
First class callable syntax
"Зачем нам вызывать функции из строк, если мы можем явно создавать ссылки на функции?" подумали разработчики PHP, готовя релиз версии 8.1 и добавили фичу со странным названием из заголовка, но оно имеет смысл. В программировании "Первоклассными сущностями" называются те элементы программы, которые могут быть присвоены переменной, переданы как параметр, возвращены из функции. То есть First class callable - это ничто иное как уже известная нам анонимная функция (Closure/Замыкание).
Сам синтаксис же состоит из конструкции (...)
, которую нужно использовать после callable сущности для создания анонимной функции на ее основе:
class Math {
public function add(int $a, int $b): int {}
public static function addStatic(int $a, int $b): int {}
public function __invoke(int $a, int $b): int {}
}
$math = new Math();
//Встроенные функция
strlen(...);
'strlen'(...);
//Массивы
[$math, 'add'](...);
[Math::class, 'addStatic'](...);
//Invokable объекты
$math(...);
//Методы объекта и класса
$math->add(...);
Math::addStatic(...);
В итоге если вы работаете с версией PHP 8.1+, то вместо оборачивания функций в кавычки и создания массивов для передачи метода, можно использовать единый синтаксис (...)
Use - получение данных из родительского контекста
Попробуем умножить массив чисел на переменную определенную вне анонимной функции. По умолчанию анонимные функции в PHP не имеют доступа к внешним переменным, поэтому следующий пример работать не будет:
$multiplier = 2;
array_map(function ($num) {
return $num * $multiplier; // Warning: Undefined variable $multiplier
}, [1, 2, 3]); // [0,0,0]
Анонимная функция должна захватить переменную из внешнего пространства имен в свое, но PHP в отличие от JS автоматически не захватывает переменные из родительских пространств имен. Поэтому переменные для захвата нужно указать вручную, воспользовавшись инструкцией use
, и указать в ней требуемые переменные, в нашем случае переменную $multiplier:
$multiplier = 2;
array_map(function ($num) use ($multiplier) {
return $num * $multiplier;
}, [1, 2, 3]); // [2,4,6]
Но на самом деле неверно про такой код говорить, что он захватывает переменную. Нет, он захватывает лишь значение переменной, что демонстрирует следующий пример:
$multiplier = 2;
$func = function ($num) use ($multiplier) {
return $num * $multiplier;
};
array_map($func, [1, 2, 3]); // Ожидаемые [2,4,6]
$multiplier = 4;
array_map($func, [1, 2, 3]); // Все еще [2,4,6]
Здесь работают такие же правила, как и с передачей аргументов в функцию. По умолчанию примитивные типы и массивы передаются по значению (опустим детали оптимизации), и только экземпляры классов передаются по ссылке. Это легко продемонстрировать, заменим значение $multiplier на класс:
final class Multiplier {
public function __construct(
private int $multiplier
) {}
public function getMultiplier(): int {
return $this->multiplier;
}
public function setMultiplier(int $multiplier): void {
$this->multiplier = $multiplier;
}
}
$multiplier = new Multiplier(2);
$func = function ($num) use ($multiplier) {
return $num * $multiplier->getMultiplier();
};
array_map($func, [1, 2, 3]); // Умножаем на 2: [2,4,6]
$multiplier->setMultiplier(4);
array_map($func, [1, 2, 3]); // А теперь на 4: [4,8,12]
Для того чтобы в функцию передавать аргументы по ссылке, требуется чтобы перед именем параметра в теле функции стоял знак &
. То же самое работает и с use
, вернемся к последнему примеру с неверной логикой и починим его:
$multiplier = 2;
$func = function ($num) use (&$multiplier) {
return $num * $multiplier;
};
array_map($func, [1, 2, 3]); // Ожидаемые [2,4,6]
$multiplier = 4;
array_map($func, [1, 2, 3]); // Тоже ожидаемые [4,8,12]
Теперь захватывается ссылка на переменную и ее изменения попадают в анонимную функцию.
Но все таки автоматический захват внешних переменных для анонимных функций существует. Точнее только одной переменной - $this
. Следующий пример будет работать даже без применения use
:
class Multiplier {
public function __construct(
private int $multiplier
) {}
public function getMultipliedArray(array $array): array {
return array_map(
function ($num) {
return $num * $this->multiplier; // Используем $this без use
},
$array
);
}
}
(new Multiplier(2))->getMultipliedArray([1,2,3]); // Результат - уже знакомый массив [2,4,6]
Если же внутри анонимной функции не предполагается использование переменной $this
, то функцию стоит объявить статической. Тогда автоматический захват $this
будет отключен. Это предотвращает потенциальную утечку памяти, в случае если указатель на экземпляр класса останется существовать внутри замыкания. Объявляется статическая анонимная функция следующим образом:
$f = static function() {}; // Больше никакого $this
Стрелочные функции
В PHP 7.4 появился способ автоматически захватывать внешние переменные в анонимных функциях. Для этого вместо привычного синтаксиса создания функции, нужно применить "стрелочный" или "сокращенный" вариант: fn (argument_list) => expr
. Вот так выглядит применение стрелочной функции в примере с умножением массива:
$multiplier = 2;
array_map(
fn($num) => $num * $multiplier,
[1, 2, 3]
); // [2,4,6]
Конструкция use
в данном случае не нужна. Захват происходит автоматически и только по значению. А также пропал оператор return
, значение вычисленное в функции автоматически возвращается из нее.
А что делать если нужно выполнить несколько действий в одной стрелочной функции? Не использовать стрелочную функцию! В стрелочной может быть только одно выражение, И хотя это является ограничением, оно становится менее серьезным если уметь в функциональный подход, использовать тернарный оператор, использовать функции для преобразования данных (array_map вместо foreach цикла, например)
Попытка снятия ограничения была в 2022 году. Тогда было создано RFC Short Closures 2.0 по внедрению многострочных стрелочных функций в PHP 8.2, но ему не хватило перевеса в 1-2 голоса из 43 полученных для принятия в стандарт языка. Первая же версия этого RFC от 2015 года провалилась полностью. Возможно в будущем будет и третья уже удачная попытка.
Closure - как создается анонимная функция
Как я уже упоминал, анонимные функции – это синтаксический сахар для создания экземпляра класса Closure
. Вот только создать экземпляр этого класса напрямую с помощью оператора new
не получится, ведь у него приватный конструктор и сам класс финальный. Это подтверждает структура класса из официальной документации:
final class Closure {
private __construct()
public static bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure
public bindTo(?object $newThis, object|string|null $newScope = "static"): ?Closure
public call(object $newThis, mixed ...$args): mixed
public static fromCallable(callable $callback): Closure
}
Но есть фабричный метод fromCallable
, который из callable
типа создает замыкание, например вот так:
function greet($name) {
return "Hello, " . $name;
}
$closure = Closure::fromCallable('greet');
$closure instanceof Closure; // true
На самом деле мы уже обсудили все варианты создание Closure
из callable
, когда говорили про First class callable syntax
, ведь конструкция (...)
– это тоже синтаксический сахар, только уже для метода Closure::fromCallable
.
BindTo, Bind и Call - указание контекста
В структуре класса Closure
еще осталось еще три метода. Все они нужны для изменения контекста вызова анонимной функции. Все три функции используются для подмены $this
на требуемый объект, а bindTo
и bind
еще могут менять статическую область видимости.
bindTo
Начнем с метода bindTo
. Так как Closure
– это класс с методами, а анонимная функция – это экземпляр класса Closure
, то у этой функции можно вызывать методы, как у обычного объекта. И метод bind
мы как раз можем вызвать для изменения контекста:
class Num5 {
public int $num = 5;
}
class Num10 {
public int $num = 10;
}
$func = function (int $a): int {
return $a * $this->num;
};
$func5 = $func->bindTo(new Num5());
$func10 = $func->bindTo(new Num10());
$func5(5); // 25
$func10(5); // 50
Функция $func
внутри себя обращается к объекту через $this
, но сразу этот объект не определен. Требуется с помощью метода bindTo
указать на требуемый $this
. В примере выше на место $this
становятся экземпляры классов Num5
и Num10
.
То же самое можно провернуть и со статическими свойствами и методами. Только теперь вместо передачи экземпляра класса в первый параметр, нужно передать сам класс во второй параметр:
class Num5 {
static int $num = 5;
}
class Num10 {
static int $num = 10;
}
$func = function (int $a): int {
return $a * static::$num;
};
$func5 = $func->bindTo(null, Num5::class);
$func10 = $func->bindTo(null, Num10::class);
$func5(5); // 25
$func10(5); // 50
Но что самое интересное, таким образом можно получить доступ и к приватным свойствам класса. Для этого нужно использовать сразу оба параметра. Первый укажет на конкретный экземпляр класса, а второй изменит видимость private
и protected
методов:
class Num5 {
private int $num = 5;
}
class Num10 {
protected int $num = 10;
}
$func = function (int $a): int {
return $a * $this->num;
};
$func5 = $func->bindTo(new Num5(), Num5::class);
$func10 = $func->bindTo(new Num10(), Num10::class);
$func5(5); // 25
$func10(5); // 50
Злоупотреблять таким подходом я крайне не рекомендую. Слишком сильно можно запутать код свободными обращениями к защищенным данным. Но как хак на крайний случай имеет место быть.
bind
Все что было рассказано про метод bindTo
верно и для bind
. Только вызывается он статически на классе Closure
и callable
переменная передается в него первым параметром, а $this
и контекст – вторым и третьим:
class Num5 {
private int $num = 5;
}
class Num10 {
protected int $num = 10;
}
$func = function (int $a): int {
return $a * $this->num;
};
$func5 = Closure::bind($func, new Num5(), Num5::class);
$func10 = Closure::bind($func, new Num10(), Num10::class);
$func5(5); // 25
$func10(5); // 50
call
Метод call
тоже во многом поход на bind
, но только он не возвращает обновленное замыкание, а сразу его вызывает и возвращает результат вызова. Первым аргументом call
принимает $this
, который требуется установить, а далее через запятую параметры функции:
class Num5 {
private int $num = 5;
}
class Num10 {
protected int $num = 10;
}
$func = function (int $a): int {
return $a * $this->num;
};
$func->call(new Num5(), 2); //10
$func->call(new Num10(), 2); //20
Также защищенные данные сразу станут доступны внутри функции вызванной с помощью call
Подводя итоги
В PHP уже с 7 версии есть множество способов работы с функциями. С версии 8 этих способов стало еще больше. Работая с современным PHP, не стоит везде использовать обычные (глобальные) функции. Нужно использовать еще анонимные и стрелочные, работать с областями видимости.
Для меня набор этих подходов стал хорошим толчком к улучшению качества кода. Теперь я стараюсь работать с данными, как с параметрами для математической функции, результат, которой может стать параметром еще одной функции, и еще одной и т.д. В результате данные преобразуются потоком, который приятен для чтения и понимания.
А как у вас с функциями в PHP? Какие подходы используете для работы в процедурном и функциональном стилях?