Mit PHP 8.1 wurde die First-class Callable Syntax eingeführt, eine syntaktische Erweiterung, die es ermöglicht, Methoden und Funktionen als Callable-Werte in einer prägnanten und ausdrucksstarken Weise zu referenzieren. Diese Funktionalität verbessert die Arbeitsweise mit Funktionen als Werte erheblich und bringt PHP näher an die funktionalen Programmiermöglichkeiten anderer moderner Sprachen.
In PHP-Versionen vor 8.1 war die Referenzierung von Methoden und Funktionen als Callable-Werte oft umständlich und wenig intuitiv:
// Vor PHP 8.1
// Funktion als Callable übergeben
$callable = 'strtoupper';
$result = array_map($callable, ['a', 'b', 'c']);
// Objektmethode als Callable übergeben
$formatter = new NumberFormatter();
$callable = [$formatter, 'format'];
$formattedNumbers = array_map($callable, [1000, 2000, 3000]);
// Statische Methode als Callable
$callable = ['Math', 'sqrt'];
$results = array_map($callable, [4, 9, 16]);
// Mit Closure (umständlich für einfache Weiterleitungen)
$callable = function($value) use ($formatter) {
return $formatter->format($value);
};Diese Ansätze führten zu Code, der schwer zu lesen war, insbesondere bei der Übergabe von Callables als Parameter oder Rückgabewerte.
Mit der First-class Callable Syntax in PHP 8.1 können wir nun
Funktionen und Methoden direkt mit dem neuen ... Operator
referenzieren:
// Ab PHP 8.1
// Funktion als Callable referenzieren
$callable = strtoupper(...);
$result = array_map($callable, ['a', 'b', 'c']);
// Objektmethode als Callable
$formatter = new NumberFormatter();
$callable = $formatter->format(...);
$formattedNumbers = array_map($callable, [1000, 2000, 3000]);
// Statische Methode als Callable
$callable = Math::sqrt(...);
$results = array_map($callable, [4, 9, 16]);Der ... Operator erzeugt einen Closure, der die
Argumente an die referenzierte Funktion oder Methode weiterleitet.
Die First-class Callable Syntax unterstützt verschiedene Arten von Callables:
// Globale Funktion
$upperCase = strtoupper(...);
echo $upperCase('hello'); // "HELLO"
// Funktion mit Namespace
$parseUrl = parse_url(...);
$components = $parseUrl('https://example.com/path?query=value');
// Mit Namespace-Qualifizierung
$jsonEncode = \json_encode(...);
$json = $jsonEncode(['key' => 'value']);
// Mit importiertem Namespace
use function MyNamespace\myFunction;
$callable = myFunction(...);class StringProcessor {
public function process(string $input): string {
return "Processed: $input";
}
}
$processor = new StringProcessor();
$processFn = $processor->process(...);
echo $processFn('test data'); // "Processed: test data"class MathUtils {
public static function square(int $number): int {
return $number * $number;
}
}
$squareFn = MathUtils::square(...);
echo $squareFn(5); // 25$className = 'MathUtils';
$squareFn = $className::square(...);
echo $squareFn(6); // 36class Calculator {
public function add(int $a, int $b): int {
return $a + $b;
}
public function subtract(int $a, int $b): int {
return $a - $b;
}
}
$calculator = new Calculator();
$operation = 'add';
$operationFn = $calculator->$operation(...);
echo $operationFn(10, 5); // 15Die First-class Callable Syntax eignet sich besonders gut für die Verwendung mit Array-Funktionen:
$numbers = [1, 2, 3, 4, 5];
// Mit First-class Callable Syntax
$doubled = array_map(fn($n) => $n * 2, $numbers);
// Oder noch eleganter mit einer eigenen Funktion
function double($n) {
return $n * 2;
}
$doubled = array_map(double(...), $numbers);
// Mit Methoden einer Klasse
class Transformer {
public function multiply(int $value, int $factor): int {
return $value * $factor;
}
}
$transformer = new Transformer();
$tripled = array_map(fn($n) => $transformer->multiply($n, 3), $numbers);
// Eleganter mit First-class Callable
$multiplyByThree = fn($n) => $transformer->multiply($n, 3);
$tripled = array_map($multiplyByThree, $numbers);
// Noch eleganter mit partieller Anwendung (siehe unten)
$multiplyByThree = fn($n) => $transformer->multiply($n, 3);
$tripled = array_map($multiplyByThree, $numbers);First-class Callables machen Event-Handler lesbarer und prägnanter:
class EventDispatcher {
private array $listeners = [];
public function addListener(string $event, callable $listener): void {
$this->listeners[$event][] = $listener;
}
public function dispatch(string $event, mixed $data = null): void {
if (!isset($this->listeners[$event])) {
return;
}
foreach ($this->listeners[$event] as $listener) {
$listener($data);
}
}
}
class Logger {
public function logEvent(string $message): void {
echo "LOG: $message\n";
}
}
class EmailNotifier {
public function sendNotification(string $message): void {
echo "EMAIL: $message\n";
}
}
// Verwendung
$dispatcher = new EventDispatcher();
$logger = new Logger();
$notifier = new EmailNotifier();
// Vor PHP 8.1
$dispatcher->addListener('user.created', [$logger, 'logEvent']);
$dispatcher->addListener('user.created', [$notifier, 'sendNotification']);
// Mit PHP 8.1
$dispatcher->addListener('user.created', $logger->logEvent(...));
$dispatcher->addListener('user.created', $notifier->sendNotification(...));
$dispatcher->dispatch('user.created', 'Neuer Benutzer: Max Mustermann');Das Strategie-Muster kann mit First-class Callables eleganter umgesetzt werden:
class ShippingCalculator {
private $strategy;
public function setStrategy(callable $strategy): void {
$this->strategy = $strategy;
}
public function calculate(array $package): float {
return ($this->strategy)($package);
}
}
class ShippingStrategies {
public function standardShipping(array $package): float {
return $package['weight'] * 2.5;
}
public function expressShipping(array $package): float {
return $package['weight'] * 5.0;
}
public function internationalShipping(array $package): float {
return $package['weight'] * 8.5 + 20;
}
}
// Verwendung
$calculator = new ShippingCalculator();
$strategies = new ShippingStrategies();
$package = ['weight' => 2.5, 'destination' => 'Deutschland'];
// Standard-Versand
$calculator->setStrategy($strategies->standardShipping(...));
echo $calculator->calculate($package); // 6.25
// Express-Versand
$calculator->setStrategy($strategies->expressShipping(...));
echo $calculator->calculate($package); // 12.5
// Abhängig von Bedingungen
$calculator->setStrategy(
$package['destination'] === 'Deutschland'
? $strategies->standardShipping(...)
: $strategies->internationalShipping(...)
);Die First-class Callable Syntax eignet sich gut für die Injektion von Verhaltensabhängigkeiten:
class UserService {
private $passwordHasher;
public function __construct(callable $passwordHasher) {
$this->passwordHasher = $passwordHasher;
}
public function registerUser(string $username, string $password): array {
$hashedPassword = ($this->passwordHasher)($password);
return [
'id' => uniqid(),
'username' => $username,
'password' => $hashedPassword
];
}
}
class PasswordService {
public function hashPassword(string $password): string {
return password_hash($password, PASSWORD_BCRYPT);
}
public function hashMD5(string $password): string {
// Veraltet, nur als Beispiel
return md5($password);
}
}
// Verwendung
$passwordService = new PasswordService();
// Moderne Verschlüsselung verwenden
$userService = new UserService($passwordService->hashPassword(...));
$user = $userService->registerUser('max', 'sicher123');
// Für Legacy-Systeme
$legacyUserService = new UserService($passwordService->hashMD5(...));
$legacyUser = $legacyUserService->registerUser('legacy_user', 'altespasswort');PHP bietet keine direkte Unterstützung für partielle Anwendung, aber die First-class Callable Syntax kann mit Closures kombiniert werden, um ähnliche Funktionalität zu erreichen:
// Manuelle partielle Anwendung
class Calculator {
public function add(int $a, int $b): int {
return $a + $b;
}
}
$calculator = new Calculator();
// Partielle Anwendung erstellen
$addFive = function(int $b) use ($calculator): int {
return $calculator->add(5, $b);
};
echo $addFive(3); // 8
// Alternativ mit einem Helferfunktion
function partial(callable $fn, ...$args): callable {
return function(...$moreArgs) use ($fn, $args) {
return $fn(...array_merge($args, $moreArgs));
};
}
$add = fn($a, $b) => $a + $b;
$addFive = partial($add, 5);
echo $addFive(3); // 8
// Oder mit First-class Callable Syntax und der Helferfunktion
$addMethod = $calculator->add(...);
$addFive = partial($addMethod, 5);
echo $addFive(3); // 8Die First-class Callable Syntax respektiert die Zugriffsmodifizierer von Methoden:
class RestrictedAccess {
private function privateMethod(): string {
return "Private Methode aufgerufen";
}
public function publicWrapper(): callable {
// Dies ist möglich, da wir innerhalb der Klasse sind
return $this->privateMethod(...);
}
}
$obj = new RestrictedAccess();
// Dies würde einen Fehler verursachen:
// $privateCall = $obj->privateMethod(...);
// Dies funktioniert, da die Methode über einen öffentlichen Wrapper zugänglich gemacht wird
$privateCall = $obj->publicWrapper();
echo $privateCall(); // "Private Methode aufgerufen"Die First-class Callable Syntax bindet Methoden automatisch an die Objekte, von denen sie stammen:
class Counter {
private int $count = 0;
public function increment(): int {
return ++$this->count;
}
public function getCount(): int {
return $this->count;
}
}
$counter = new Counter();
$incrementFn = $counter->increment(...);
echo $incrementFn(); // 1
echo $incrementFn(); // 2
echo $counter->getCount(); // 2PHP bietet seit Version 7.4 auch Arrow Functions (Pfeilfunktionen) an. Es ist wichtig zu verstehen, wann welche Syntax zu bevorzugen ist:
$numbers = [1, 2, 3, 4, 5];
// Arrow Function, wenn wir Logik hinzufügen
$doubled = array_map(fn($n) => $n * 2, $numbers);
// First-class Callable Syntax, wenn wir nur eine bestehende Funktion/Methode aufrufen
function double($n) {
return $n * 2;
}
$doubled = array_map(double(...), $numbers);
// Arrow Functions sind besser für einfache inline Transformationen
$evenNumbers = array_filter($numbers, fn($n) => $n % 2 === 0);
// First-class Callable ist besser, wenn wir eine bestehende Prüfmethode haben
class NumberFilter {
public function isEven(int $n): bool {
return $n % 2 === 0;
}
}
$filter = new NumberFilter();
$evenNumbers = array_filter($numbers, $filter->isEven(...));Verwenden Sie First-class Callable für Methodenreferenzen: Wenn Sie eine bestehende Funktion oder Methode direkt als Callable übergeben möchten, ist die First-class Callable Syntax die beste Wahl.
Verwenden Sie Arrow Functions für inline Logik: Wenn Sie einfache Transformationen oder Bedingungen direkt im Kontext ausdrücken möchten, sind Arrow Functions (fn) oft lesbarer.
Achten Sie auf die Lesbarkeit: Wählen Sie die Syntax, die den Code am klarsten macht. Manchmal kann eine explizite Closure die Absicht besser kommunizieren.
Kombinieren Sie mit Constructor Property Promotion: First-class Callables lassen sich gut mit Constructor Property Promotion kombinieren:
class Service {
public function __construct(
private readonly callable $logger = null,
private readonly callable $validator = strtolower(...)
) {}
public function process(string $input): void {
$normalizedInput = ($this->validator)($input);
// Verarbeitung...
if ($this->logger) {
($this->logger)("Verarbeitet: $normalizedInput");
}
}
}