29 First-class Callable Syntax

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.

29.1 Das Problem: Umständliche Callable-Referenzen

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.

29.2 Die Lösung: First-class Callable Syntax

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.

29.3 Detaillierte Syntaxoptionen

Die First-class Callable Syntax unterstützt verschiedene Arten von Callables:

29.3.1 1. Globale Funktionen und Funktionen mit Namespace

// 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(...);

29.3.2 2. Instanzmethoden

class StringProcessor {
    public function process(string $input): string {
        return "Processed: $input";
    }
}

$processor = new StringProcessor();
$processFn = $processor->process(...);

echo $processFn('test data'); // "Processed: test data"

29.3.3 3. Statische Methoden

class MathUtils {
    public static function square(int $number): int {
        return $number * $number;
    }
}

$squareFn = MathUtils::square(...);
echo $squareFn(5); // 25

29.3.4 4. Methoden über variablen Klassennamen

$className = 'MathUtils';
$squareFn = $className::square(...);
echo $squareFn(6); // 36

29.3.5 5. Methoden mit dynamischem Namen

class 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); // 15

29.4 Praktische Anwendungsfälle

29.4.1 1. Callbacks an Array-Funktionen übergeben

Die 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);

29.4.2 2. Ereignisbehandlung (Event Handling)

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');

29.4.3 3. Strategie-Muster (Strategy Pattern)

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(...)
);

29.4.4 4. Dependency Injection mit Callbacks

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');

29.5 Einschränkungen und Lösungen

29.5.1 1. Partielle Anwendung (Partial Application)

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); // 8

29.5.2 2. Zugriff auf private/protected Methoden

Die 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"

29.5.3 3. Bindung an Objekte (Binding)

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(); // 2

29.6 First-class Callable Syntax vs. Arrow Functions

PHP 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(...));

29.7 Best Practices

  1. 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.

  2. 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.

  3. 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.

  4. 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");
        }
    }
}
  1. Nutzen Sie sie in Dependency Injection: First-class Callables eignen sich hervorragend für Strategiemuster und Dependency Injection von Verhalten.