В 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) //InvalidArgumentException

Это все база, от которой будем далее отталкиваться. Но если хочется проверить свои знания функций в 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) use ($multiplier) {  
    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? Какие подходы используете для работы в процедурном и функциональном стилях?

Комментарии (16)


  1. JhaoDa
    25.07.2024 10:38
    +2

    есть проблема – анонимную функцию никак не вызвать

    Серьёзно?


    1. Daniel217D Автор
      25.07.2024 10:38

      Серьёзно. В примере с этим комментарием функция бесполезна


      1. arokettu
        25.07.2024 10:38
        +3

        а как же?

        (function ($s) { 
            echo $s;
        })('123');
        


  1. bolk
    25.07.2024 10:38
    +3

    1) У вас форматирование не по PSR.
    2) Никакой путаницы с замыканиями нет. Посмотрите что такое замыкание: https://ru.wikipedia.org/wiki/Замыкание_(программирование)
    3) стрелочные функции тоже бывают static


    1. Daniel217D Автор
      25.07.2024 10:38
      +1

      По PSR кроме того, что открывающая кавычка не на новой строке. Это сделано для удобства чтения кода

      Путаница есть, среди тех, кто считает, что у замыкания только одно определение. В статье, как и в википедии этот непонимание разрушается


      1. bolk
        25.07.2024 10:38

        Оно непривычное, поэтому неудобное.


        1. Daniel217D Автор
          25.07.2024 10:38
          +2

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

          Я пишу код под разные стандарты и поддерживаю единообразие с помощью инструментов стат анализа.

          В рамках данной статьи обсуждение стандартов написания кода вообще не имеет смысла.


  1. aleksejs1
    25.07.2024 10:38
    +1

    Очень крутая статья.

    Правда много объектных нюансов.

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

    Вспомнился небольшой забавный проектик, когда я пробовал переписать Symfony на процедурный стиль с целью более глубокого изучения symfony и попыткой доказать, что без ООП тоже можно жить: git


    1. Daniel217D Автор
      25.07.2024 10:38

      Спасибо!

      А можете подробнее расписать про комбинацию функций и областей видимости? Мне только обычный импорт вспоминается


  1. FanatPHP
    25.07.2024 10:38
    +7

    Статья, на мой взгляд в целом хорошая и нужная.

    Вот только я бы написал про про строгую типизацию поподробнее. Она всё-таки, целиком относится именно к функциям. Тем более что запустив пример createMessage(123, false)читатель не получит никакого InvalidArgumentException, а в статье не объясняется, как его получить.

    Заголовок "Вызываемый класс" я бы поменял на "Объект как функция" или "Обращение к объекту, как к функции", поскольку в примере у вас не класс, а объект, и непонятно, что имеется в виду под словом "вызываемый". Плюс обычно говорят "экземпляр" класса, а не "сущность".


    1. Rsa97
      25.07.2024 10:38
      +1

      Тем более что запустив пример createMessage(123, false) читатель не получит никакого InvalidArgumentException

      Читатель получит syntax error, unexpected end of file, поскольку отсутствует точка с запятой.


    1. Daniel217D Автор
      25.07.2024 10:38

      Спасибо за содержательный комментарий!

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

      Про точность терминов - согласен. Поправлю


      1. FanatPHP
        25.07.2024 10:38

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


      1. supercat1337
        25.07.2024 10:38

        Полезная публикация! Спасибо.

        И про типизацию я бы почитал с удовольствием. Думаю многим будет интересно когда целесообразно использовать phpdoc.


        1. Rsa97
          25.07.2024 10:38

          Типизация в современном PHP не имеет никакого отношения к phpdoc.
          При включённом strict_types заданные в контракте функции типы аргументов и результата контролируются в момент исполнения кода и их несоответствие приводит к ошибке.
          Комментарии phpdoc нужны для разработчика. Они общепринятым способом описывают не только типы, но и назначение аргументов и результата функции и могут содержать информацию о поведении функции. Эта информация может использоваться IDE для показа подсказок. При этом несоответствие типов в phpdoc самим PHP не контролируется и проверяется, как правило, статическим анализатором (возможно, встроенным в IDE).


          1. supercat1337
            25.07.2024 10:38

            Спасибо, полностью согласен с вами. Конечно, в первую очередь интересует контроль типов статическим анализатором из IDE (пользуемся VSCode). Ловить исключение после запуска - это не так интересно) А что касается phpdoc, то там есть где можно мысли развернуться, например, описание возвращаемой структуры данных из функции. В общем, пусть автор знает, что мне это интересно))