Что такое 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