MidТеория9 min

Callable и Callback

Типы callable, замыкания, first-class callable, Closure, практические паттерны

Что такое Callable

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

<?php
declare(strict_types=1);

// All of these are callable:
$a = 'strlen';                                    // Function name as string
$b = function(int $x): int { return $x * 2; };   // Anonymous function (Closure)
$c = fn(int $x): int => $x * 2;                  // Arrow function (Closure)
$d = [new DateTime(), 'format'];                  // Instance method
$e = [DateTime::class, 'createFromFormat'];       // Static method
$f = 'DateTime::createFromFormat';                // Static method as string

// Verify with is_callable()
var_dump(is_callable($a));  // true
var_dump(is_callable($b));  // true
var_dump(is_callable($c));  // true
var_dump(is_callable($d));  // true
var_dump(is_callable($e));  // true
var_dump(is_callable($f));  // true

Все формы Callable

1. Имя функции (строка)

<?php
declare(strict_types=1);

// Built-in functions
$fn = 'strtoupper';
echo $fn('hello');  // HELLO

// User-defined functions
function double(int $n): int
{
    return $n * 2;
}

$fn = 'double';
echo $fn(21);  // 42

// Namespaced function — must use full name
namespace App\Utils;

function slugify(string $text): string
{
    return strtolower(str_replace(' ', '-', $text));
}

// From another namespace:
$fn = 'App\Utils\slugify';
echo $fn('Hello World');  // hello-world

2. Замыкание (Closure / Anonymous Function)

<?php
declare(strict_types=1);

// Anonymous function
$multiply = function(int $a, int $b): int {
    return $a * $b;
};
echo $multiply(6, 7);  // 42

// Closure capturing variables with `use`
$factor = 3;
$tripler = function(int $n) use ($factor): int {
    return $n * $factor;
};
echo $tripler(14);  // 42

// Capture by reference
$counter = 0;
$increment = function() use (&$counter): void {
    $counter++;
};
$increment();
$increment();
echo $counter;  // 2

// Arrow function (PHP 7.4+) — auto-captures by value
$factor = 3;
$tripler = fn(int $n): int => $n * $factor;
echo $tripler(14);  // 42

// Arrow functions are ALWAYS single-expression
// Cannot use multiple statements or modify captured variables
$factor = 3;
$fn = fn(int $n): int => $n * $factor;
// $factor is captured by VALUE — changing $factor later has no effect
$factor = 10;
echo $fn(14);  // 42 (still uses 3)

Запомни: Arrow functions (fn() =>) автоматически захватывают переменные из внешней области по значению. Обычные анонимные функции (function() use()) требуют явного use и могут захватывать по ссылке через &.

3. Методы объекта

<?php
declare(strict_types=1);

class Calculator
{
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    public static function multiply(int $a, int $b): int
    {
        return $a * $b;
    }

    protected function subtract(int $a, int $b): int
    {
        return $a - $b;
    }
}

$calc = new Calculator();

// Instance method as callable — array syntax
$callable = [$calc, 'add'];
echo $callable(20, 22);  // 42

// Static method — array syntax
$callable = [Calculator::class, 'multiply'];
echo $callable(6, 7);  // 42

// Static method — string syntax
$callable = 'Calculator::multiply';
echo call_user_func($callable, 6, 7);  // 42
// NOTE: $callable(6, 7) does NOT work with string syntax for static methods!

// From inside a class
class Service
{
    public function process(): void
    {
        // self methods
        $fn1 = [$this, 'helperMethod'];
        $fn2 = [self::class, 'staticHelper'];
        $fn3 = [static::class, 'staticHelper'];  // Late static binding

        $fn1();  // OK
    }

    private function helperMethod(): void {}
    private static function staticHelper(): void {}
}

4. Invokable объекты (__invoke)

<?php
declare(strict_types=1);

class Validator
{
    public function __construct(
        private readonly int $minLength,
        private readonly int $maxLength,
    ) {
    }

    public function __invoke(string $value): bool
    {
        $length = mb_strlen($value);
        return $length >= $this->minLength && $length <= $this->maxLength;
    }
}

$validate = new Validator(3, 50);

// Call as function
var_dump($validate('Hello'));   // true
var_dump($validate('Hi'));      // false (too short)

// is_callable returns true for __invoke objects
var_dump(is_callable($validate));  // true

// Works in array functions
$names = ['Al', 'Bob', 'Catherine', 'Di'];
$valid = array_filter($names, $validate);
// Result: ['Bob', 'Catherine'] — 3-50 chars

Практика: Invokable классы отлично подходят для реализации паттерна Стратегия, middleware, валидаторов и любых callback-ов с конфигурацией.

is_callable() и callable_exists()

<?php
declare(strict_types=1);

// is_callable() — checks if value can be called
var_dump(is_callable('strlen'));          // true
var_dump(is_callable('nonExistentFn'));   // false
var_dump(is_callable([new DateTime(), 'format']));  // true

// is_callable with syntax_only parameter
// When true — only checks format, NOT if function exists
var_dump(is_callable('anything', true));     // true — string is valid syntax
var_dump(is_callable([1, 2], true));         // false — invalid syntax

// Get callable name
$name = '';
is_callable('strlen', false, $name);
echo $name;  // 'strlen'

is_callable([new DateTime(), 'format'], false, $name);
echo $name;  // 'DateTime::format'

// function_exists() — only checks regular functions
var_dump(function_exists('strlen'));     // true
var_dump(function_exists('array_map')); // true

// method_exists() — checks methods
var_dump(method_exists(DateTime::class, 'format'));      // true
var_dump(method_exists(DateTime::class, 'nonExistent')); // false

// NOTE: method_exists returns true even for private methods!
class Secret
{
    private function hidden(): void {}
}
var_dump(method_exists(Secret::class, 'hidden'));  // true
var_dump(is_callable([new Secret(), 'hidden']));   // false — not accessible

call_user_func() и call_user_func_array()

<?php
declare(strict_types=1);

// call_user_func — calls callable with arguments
echo call_user_func('strtoupper', 'hello');  // HELLO
echo call_user_func('substr', 'Hello World', 0, 5);  // Hello

// call_user_func_array — arguments as array
echo call_user_func_array('substr', ['Hello World', 0, 5]);  // Hello

// With instance methods
$date = new DateTime('2024-01-15');
echo call_user_func([$date, 'format'], 'Y-m-d');  // 2024-01-15

// With static methods
echo call_user_func([DateTime::class, 'createFromFormat'], 'Y-m-d', '2024-01-15')
    ->format('d.m.Y');  // 15.01.2024

// Modern PHP — direct invocation is preferred
$fn = 'strtoupper';
echo $fn('hello');  // HELLO — simpler, same result

// Spread operator replaces call_user_func_array
$args = ['Hello World', 0, 5];
echo substr(...$args);  // Hello

Рекомендация: В современном PHP (8.0+) предпочитайте прямой вызов и spread-оператор вместо call_user_func(). Они быстрее и читаемее.

First-Class Callable Syntax (PHP 8.1+)

PHP 8.1 ввел синтаксис func(...) для создания Closure из любого callable.

<?php
declare(strict_types=1);

// Creating Closure from function
$fn = strlen(...);
echo $fn('Hello');  // 5

// From built-in functions
$upper = strtoupper(...);
$lower = strtolower(...);
echo $upper('hello');  // HELLO
echo $lower('WORLD');  // world

// From static methods
$fn = DateTime::createFromFormat(...);
$date = $fn('Y-m-d', '2024-01-15');

// From instance methods
$date = new DateTime();
$format = $date->format(...);
echo $format('Y-m-d');  // current date

// From closures (identity — returns same Closure)
$multiply = fn(int $a, int $b): int => $a * $b;
$ref = $multiply(...);  // Same closure

// In array functions — clean and readable
$numbers = [-3, -1, 0, 2, 5];
$absolute = array_map(abs(...), $numbers);
// [3, 1, 0, 2, 5]

$words = ['hello', 'WORLD', 'PHP'];
$lower = array_map(strtolower(...), $words);
// ['hello', 'world', 'php']

Преимущества перед Closure::fromCallable()

<?php
declare(strict_types=1);

// BEFORE PHP 8.1
$fn = Closure::fromCallable('strlen');
$fn = Closure::fromCallable([DateTime::class, 'createFromFormat']);

// PHP 8.1+ — cleaner syntax
$fn = strlen(...);
$fn = DateTime::createFromFormat(...);

// Key advantage: preserves scope
class Service
{
    private function secret(): string
    {
        return 'hidden';
    }

    public function getCallable(): Closure
    {
        // This Closure retains access to private method
        return $this->secret(...);
    }
}

$svc = new Service();
$fn = $svc->getCallable();
echo $fn();  // 'hidden' — works because Closure keeps scope

Closure::fromCallable() и Closure::bind()

<?php
declare(strict_types=1);

// Closure::fromCallable — converts callable to Closure
$fn = Closure::fromCallable('strlen');
var_dump($fn instanceof Closure);  // true

// Why convert to Closure?
// 1. Type safety — Closure is a specific class
function process(Closure $callback): void
{
    echo $callback('test');
}

// 2. Closure::bind — rebind $this
class Logger
{
    private string $prefix = 'LOG';

    public function getFormatter(): Closure
    {
        return function(string $message): string {
            return "[{$this->prefix}] {$message}";
        };
    }
}

$logger = new Logger();
$formatter = $logger->getFormatter();
echo $formatter('Hello');  // [LOG] Hello

// Rebind to different object
class ErrorLogger
{
    private string $prefix = 'ERROR';
}

$rebound = Closure::bind($formatter, new ErrorLogger(), ErrorLogger::class);
echo $rebound('Something failed');  // [ERROR] Something failed

// Closure::call — temporary rebind (PHP 7.0+)
$fn = function(): string {
    return $this->prefix;
};

$result = $fn->call(new Logger());
echo $result;  // 'LOG'

Типизация Callable

<?php
declare(strict_types=1);

// callable type hint — accepts any callable
function apply(callable $fn, int $value): int
{
    return $fn($value);
}

echo apply('abs', -42);                           // 42
echo apply(fn(int $n): int => $n * 2, 21);       // 42
echo apply(new class { public function __invoke(int $n): int { return $n + 1; } }, 41);  // 42

// Closure type hint — only accepts Closure objects
function transform(Closure $fn, array $items): array
{
    return array_map($fn, $items);
}

// transform('strtoupper', ['a', 'b']);  // TypeError! string is not Closure
transform(strtoupper(...), ['a', 'b']);  // OK — first-class callable creates Closure

// callable CANNOT be used as property type
class Handler
{
    // public callable $callback;  // Fatal error!
    // Workaround:
    public Closure $callback;

    public function __construct(callable $callback)
    {
        // Convert callable to Closure for storage
        $this->callback = $callback(...);
    }
}

$handler = new Handler('strtoupper');
echo ($handler->callback)('hello');  // HELLO

Важно: callable нельзя использовать как тип свойства! Используйте Closure для свойств. callable допускается только в параметрах и return types.

Callable в функциях массивов

array_map

<?php
declare(strict_types=1);

$numbers = [1, 2, 3, 4, 5];

// With built-in function
$doubled = array_map(fn(int $n): int => $n * 2, $numbers);
// [2, 4, 6, 8, 10]

// First-class callable
$squared = array_map(fn(int $n): int => $n ** 2, $numbers);
// [1, 4, 9, 16, 25]

// Multiple arrays
$names = ['Alice', 'Bob'];
$ages = [30, 25];
$combined = array_map(
    fn(string $name, int $age): string => "{$name} ({$age})",
    $names,
    $ages,
);
// ['Alice (30)', 'Bob (25)']

// null callback — creates array of arrays (zip)
$zipped = array_map(null, $names, $ages);
// [['Alice', 30], ['Bob', 25]]

array_filter

<?php
declare(strict_types=1);

$numbers = [0, 1, '', null, 'hello', false, 42, [], [0]];

// No callback — removes falsy values
$truthy = array_filter($numbers);
// [1 => 1, 4 => 'hello', 6 => 42, 8 => [0]]

// With callback
$positive = array_filter(
    [1, -2, 3, -4, 5],
    fn(int $n): bool => $n > 0,
);
// [0 => 1, 2 => 3, 4 => 5]

// Filter by key
$data = ['name' => 'Alice', 'age' => 30, 'email' => '[email protected]'];
$allowed = ['name', 'email'];
$filtered = array_filter(
    $data,
    fn(string $key): bool => in_array($key, $allowed, true),
    ARRAY_FILTER_USE_KEY,
);
// ['name' => 'Alice', 'email' => '[email protected]']

// Filter by key and value
$filtered = array_filter(
    $data,
    fn(mixed $value, string $key): bool => $key !== 'age' && $value !== '',
    ARRAY_FILTER_USE_BOTH,
);

usort / uasort / uksort

<?php
declare(strict_types=1);

$users = [
    ['name' => 'Charlie', 'age' => 35],
    ['name' => 'Alice', 'age' => 30],
    ['name' => 'Bob', 'age' => 25],
];

// Sort by age
usort($users, fn(array $a, array $b): int => $a['age'] <=> $b['age']);
// Alice(30), Bob(25), Charlie(35) → Bob(25), Alice(30), Charlie(35)

// Sort by name descending
usort($users, fn(array $a, array $b): int => $b['name'] <=> $a['name']);
// Charlie, Bob, Alice

// Multi-field sort
usort($users, function(array $a, array $b): int {
    return $a['age'] <=> $b['age']
        ?: $a['name'] <=> $b['name'];
});

Практические паттерны

Паттерн Стратегия

<?php
declare(strict_types=1);

final class PriceCalculator
{
    /** @var array<string, Closure(float): float> */
    private array $strategies = [];

    public function addStrategy(string $name, Closure $strategy): void
    {
        $this->strategies[$name] = $strategy;
    }

    public function calculate(string $strategy, float $price): float
    {
        if (!isset($this->strategies[$strategy])) {
            throw new InvalidArgumentException("Unknown strategy: {$strategy}");
        }

        return ($this->strategies[$strategy])($price);
    }
}

$calculator = new PriceCalculator();

// Register strategies
$calculator->addStrategy('regular', fn(float $price): float => $price);
$calculator->addStrategy('vip', fn(float $price): float => $price * 0.8);
$calculator->addStrategy('wholesale', fn(float $price): float => $price * 0.6);

echo $calculator->calculate('vip', 100.0);        // 80.0
echo $calculator->calculate('wholesale', 100.0);   // 60.0

Middleware Pipeline

<?php
declare(strict_types=1);

final class Pipeline
{
    /** @var list<Closure(mixed, Closure): mixed> */
    private array $middleware = [];

    public function pipe(Closure $middleware): static
    {
        $this->middleware[] = $middleware;
        return $this;
    }

    public function process(mixed $payload): mixed
    {
        // Build pipeline from right to left
        $pipeline = array_reduce(
            array_reverse($this->middleware),
            fn(Closure $next, Closure $middleware): Closure =>
                fn(mixed $payload): mixed => $middleware($payload, $next),
            fn(mixed $payload): mixed => $payload,  // Final handler
        );

        return $pipeline($payload);
    }
}

// Usage
$result = (new Pipeline())
    ->pipe(function(string $text, Closure $next): string {
        return $next(trim($text));
    })
    ->pipe(function(string $text, Closure $next): string {
        return $next(strtolower($text));
    })
    ->pipe(function(string $text, Closure $next): string {
        return $next(str_replace(' ', '-', $text));
    })
    ->process('  Hello World  ');

echo $result;  // 'hello-world'

Event Dispatcher

<?php
declare(strict_types=1);

final class EventDispatcher
{
    /** @var array<string, list<Closure>> */
    private array $listeners = [];

    public function on(string $event, Closure $listener): void
    {
        $this->listeners[$event][] = $listener;
    }

    public function dispatch(string $event, mixed ...$args): void
    {
        foreach ($this->listeners[$event] ?? [] as $listener) {
            $listener(...$args);
        }
    }
}

$dispatcher = new EventDispatcher();

$dispatcher->on('user.created', function(string $name, string $email): void {
    echo "Welcome email sent to {$email}\n";
});

$dispatcher->on('user.created', function(string $name, string $email): void {
    echo "User {$name} logged to analytics\n";
});

$dispatcher->dispatch('user.created', 'Alice', '[email protected]');
// Welcome email sent to [email protected]
// User Alice logged to analytics

Проверь себя

5 из 11

Какой флаг в `array_filter` позволяет фильтровать по ключу?

Что произойдет при `array_map(null, ['a', 'b'], [1, 2])`?

В чем ключевое преимущество `func(...)` перед `Closure::fromCallable()`?

Что делает `Closure::bind()`?

Чем arrow function отличается от обычной анонимной функции?