Допустим, мы проектируем пакетный обработчик команд. Чтобы узнавать о каждой успешной операции, добавим простой аргумент-слушатель $onEach.



/**

* @template T of object

* @psalm-param iterable<T> $commands

* @psalm-param callable(T): void $onEach

*/

function handleBatch(iterable $commands, callable $onEach): void

{

foreach ($commands as $command) {

// ...



$onEach($command);

}

}




Теперь можно, например, инкрементировать прогресс-бар при вызове из консольной команды.



handleBatch($commands, static function () use ($progressBar): void {

$progressBar->advance();

});




Однако не всегда слушатель будет нужен, поэтому для простоты контракта сделаем его необязательным аргументом. Решение "в лоб": ?callable $onEach = null и потом if (null !== $onEach) { $onEach($command) }.



А теперь применим паттерн NullObject. Для этого добавим в проектный functions.php элементарную function void(): void {} и попробуем её в качестве значения по умолчанию в сигнатуре обработчика: callable $onEach = 'void'.



👹 Fatal error: Default value for parameters with callable type can only be NULL.



Эхх, видимо, без null здесь никак не обойтись. Но мы не сдаемся и красиво комбинируем.



function handleBatch(iterable $commands, ?callable $onEach = null): void

{

$onEach ??= 'void';



// ...

}




🎉 Ура, так работает.



Плюсы этого подхода по сравнению с if:

• лаконичность: 1 строка вместо 3;

• простота восприятия: одно выражение в начале функции имеет меньшую цикломатическую сложность, чем условие в цикле;

• универсальность: легко переиспользовать во всех подобных ситуациях.