gwangjinkim/php-multidispatch

Simple and elegant multiple dispatch for PHP with hierarchy resolution.

Installs: 2

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/gwangjinkim/php-multidispatch

v0.3.1 2025-07-04 05:56 UTC

This package is auto-updated.

Last update: 2025-10-16 18:35:32 UTC


README

Packagist Tests

A modern, readable, CLOS-inspired multiple dispatch library for PHP—now with support for :primary, :before, :after, :around, and call-next-method! Open-Closed Principle in action, no fuss.

What is Multiple Dispatch? (And Why Should You Care?)

Multiple dispatch lets you write functions whose behavior depends on the runtime types of all their arguments—not just the first one (like in classic OOP). Imagine polymorphism that’s not boxed into inheritance hierarchies, with the freedom to extend methods for new types, even across package boundaries.

Why is this awesome?

  • Write cleaner, more declarative code (no tangled if-else or switch statements).
  • Add new behaviors from outside existing packages—no patching or hacking core code.
  • Follow the Open-Closed Principle: your code is open for extension, closed for modification.
  • Real power: this is what makes Julia, CLOS (Common Lisp), and R so extensible.

Multiple Dispatch allows you to import a package (which uses Multiple Dispatch) and extend it without ever having to touch or change its internal code (tests, definitions), but just add more classes and more methods/functions. It allows you to follow open-closed principle which would be impossible with classical Java/C++ OOP systems.

Features

  • True Multiple Dispatch (not just single dispatch)
  • CLOS-style Method Combinations: :primary, :before, :after, :around, and $callNext
  • Inheritance & Interface Awareness: Works with classes, interfaces, built-in types
  • Fallbacks: Register wildcard methods for default cases
  • Simple API: Intuitive, modern PHP code
  • Configurable Dispatch Policy: Choose “first-wins” or “last-wins” for registration order.

Table of Contents

  1. Installation
  2. Classic Usage (No CLOS Extensions)
  3. CLOS Extensions: :primary, :before, :after, :around, and call-next-method
  4. Extended Example: CLOS in PHP
  5. Testing
  6. Troubleshooting
  7. Links & References

Installation

With Composer

composer require gwangjinkim/php-multidispatch:dev-main

Use :dev-main to get the newest version! Specify version numbers when you want to downgrade or pin a version.

Or add to your composer.json:

"require": {
    "gwangjinkim/php-multidispatch": "dev-main"
}

Eventually use "^0.3.1" e.g., to pin to a version.

Requirements

  • PHP 8.1 or newer recommended

PHP Installation Shortcuts

  • MacOS: See this detailed guide (insert your own article link here)
  • Linux (Ubuntu):
    sudo apt update
    sudo apt install -y php php-cli php-xml php-zip php-curl phpunit composer
  • Windows (with Scoop):
    scoop install php composer

Dispatch Policy: first-wins vs last-wins

You control which matching handler "wins" when there are multiple equally-specific possibilities (for example, two interfaces, or multiple wildcards).

  • First-wins: The first registered method for a type signature is chosen (classic OOP behavior).
  • Last-wins: The last registered method is chosen (suits override-style patterns).

Default: last-wins for maximum extensibility, but you can switch any dispatcher at any time:

$fn = multidispatch();
$fn->setDispatchPolicy(\GwangJinKim\Multidispatch\DispatchPolicy::FirstWins);
// ...or...
$fn->setDispatchPolicy(\GwangJinKim\Multidispatch\DispatchPolicy::LastWins);

Use DispatchPolicy::FirstWins for backwards compatibility or stricter method resolution.

Policy Which handler wins? When to use
first-wins First registered Strict/classic OOP, legacy, backward-compat
last-wins Last registered (default) Override style, plugin/extensible systems

Classic Usage: Multiple Dispatch the Simple Way (No CLOS Extensions)

If you just want plain, classic multiple dispatch—no :before, :after, or :around—your code stays as simple and clean as before. Everything you knew still works, and it’s all backwards-compatible!

Here’s how you use the package the “old-school” way (like before CLOS extensions):

require "vendor/autoload.php";
use function GwangJinKim\Multidispatch\multidispatch;

interface IA {}
interface IB {}
class CA implements IA, IB {}
class CB implements IA, IB {}

$fn = multidispatch();

// Register different handlers for types
$fn[['IA']] = fn($a) => "This is IA";
$fn[['IB']] = fn($a) => "This is IB";
$fn[['*']]  = fn($a) => "Default!";

// Usage: dispatch based on type
echo $fn(new CA()); // Depending on registration/interface, returns "This is IA" or "This is IB"
echo $fn(new CB()); // Ditto

// Also works for built-in types:
$scalarDispatch = multidispatch();
$scalarDispatch[['int', 'string']] = fn($x, $y) => "Int: $x, String: $y";
echo $scalarDispatch(1, "hello"); // Int: 1, String: hello

// You can always register a general handler:
$fn[['*']] = fn($a) => "Fallback/default handler";
echo $fn([]); // Fallback/default handler

How it works:

  • You register handlers for each type signature (single or multiple arguments, e.g. ['int', 'string']).
  • On call, the dispatcher figures out the most specific match (based on type, class, interface, and inheritance).
  • The handler runs.
  • No CLOS extensions means there’s one winner, no call_next, and no :before/:after/:around stacking.
  • This style is familiar if you know Python’s functools.singledispatch or Julia’s basic dispatch.

When to use:

  • You want simple, one-winner dispatch logic.
  • You’re porting code from a previous version of this package.
  • You don’t need CLOS-style hooks or method chaining (yet).

Note: You can always choose whether registration order is first-wins or last-wins, per-dispatcher, using setDispatchPolicy. The default is last-wins, but all older code is fully supported.

$fn = multidispatch();
$fn->setDispatchPolicy(\GwangJinKim\Multidispatch\DispatchPolicy::FirstWins);
// ...or...
$fn->setDispatchPolicy(\GwangJinKim\Multidispatch\DispatchPolicy::LastWins);

Why CLOS? (And What Makes It Different?)

Why all this talk about CLOS (the Common Lisp Object System)?
CLOS is the most advanced, flexible object system ever built—letting you combine, stack, override, and wrap behavior with surgical precision.
With these extensions, you can:

  • Dynamically stack hooks before/after/around any method, for any type (logging, validation, timing, security, profiling, tracing, etc).
  • Control composition: Choose which method(s) run and how their results are combined—no more black-box dispatch!
  • Open-Closed Principle, but supercharged: Extend any package, for any type, without touching its code.
  • Stay DRY: Cross-cutting concerns (security, logging, caching) are isolated, not tangled up in your logic.

It’s a friendlier, more composable way to program.
And it’s 100% optional—you can stick with classic style if you like!

CLOS-Style Method Combination

Inspired by the Common Lisp Object System (CLOS), you can now combine multiple method types:

  • :primary – The main methods, as before
  • :before – Run before the primaries (in specificity order)
  • :after – Run after the primaries (in reverse specificity)
  • :around – Can wrap everything (get $callNext as first argument; call it to proceed)
  • call-next-method – Provided to :around methods as $callNext (you must call it to continue the chain)

Registration Syntax:

$fn[ [Type1, ...], ':before' ] = function ($a, ...) { /* ... */ };
$fn[ [Type1, ...], ':primary' ] = function ($a, ...) { /* ... */ };
$fn[ [Type1, ...], ':after'  ] = function ($a, ...) { /* ... */ };
$fn[ [Type1, ...], ':around' ] = function ($callNext, $a, ...) { /* ... */ };

Example:

require "vendor/autoload.php";
use function GwangJinKim\Multidispatch\multidispatch;

interface IA {}
class A implements IA {}
class B extends A {}

$fn = multidispatch();

$fn[ [A::class], ':primary' ] = fn($a) => "Primary A";
$fn[ [A::class], ':before'  ] = fn($a) => print("Before A\n");
$fn[ [A::class], ':after'   ] = fn($a) => print("After A\n");
$fn[ [A::class], ':around'  ] = function($callNext, $a) {
    print("Around A (before)\n");
    $result = $callNext($a);
    print("Around A (after)\n");
    return $result;
};

echo $fn(new A());

Output:

Before A
Around A (before)
Primary A
Around A (after)
After A
Primary A

How the Method Combination Works:

  1. All matching :before methods are called, from most specific to least.
  2. All :around methods wrap the chain (most specific is outermost).
  3. The most specific :primary method is called.
  4. All matching :after methods are called, from least specific to most.

You can combine :before, :after, :primary, and :around on any type signature.

Extended CLOS Example

Here is a full, extended example to clarify usage and show best practices—great for both CLOS and non-CLOS users.

require "vendor/autoload.php";
use function GwangJinKim\Multidispatch\multidispatch;

// Interfaces & classes
interface Animal {}
class Dog implements Animal {}
class Cat implements Animal {}

// Create a dispatcher
$battle = multidispatch();

// Register primary methods
$battle[[Dog::class, Dog::class], ':primary']   = fn($a, $b) => "Dog vs Dog: Bark!";
$battle[[Animal::class, Animal::class], ':primary'] = fn($a, $b) => "Generic animal fight";

// Before and after hooks
$battle[[Dog::class, Dog::class], ':before'] = fn($a, $b) => print("Sniffing each other\n");
$battle[[Dog::class, Dog::class], ':after']  = fn($a, $b) => print("Wagging tails\n");

// Around (wrapping the dispatch, controlling the flow)
$battle[[Animal::class, Animal::class], ':around'] = function($callNext, $a, $b) {
    print("Arena lights up\n");
    $result = $callNext($a, $b);
    print("Audience cheers\n");
    return $result;
};

// Run dispatch!
echo $battle(new Dog(), new Dog());
echo $battle(new Cat(), new Dog());

What you'll see:

Sniffing each other
Arena lights up
Dog vs Dog: Bark!
Audience cheers
Wagging tails
Arena lights up
Generic animal fight
Audience cheers

How to Think About Each Method Type:

  • :primary: The "main event"—your usual handler, one per match.
  • :before: Side-effects you want to happen before the main event, e.g., logging, setup, animation, etc.
  • :after: Side-effects after the main event, e.g., cleanup, logging, summary.
  • :around: Like a decorator or middleware: can do stuff before/after and even decide not to call the rest at all (by not calling $callNext).
  • call-next-method: Only available inside :around. Lets you control the chain, e.g., by running or skipping underlying methods.

Detailed CLOS Example With Explanations

require "vendor/autoload.php";
use function GwangJinKim\Multidispatch\multidispatch;

// Define interfaces and classes
interface IA {}
class CA implements IA {}

// Make a dispatcher
$fn = multidispatch();

// :before hook (runs before primary)
$fn[[[CA::class], ':before']] = function($a) {
    echo "Logging before CA\n";
};
// :primary (main logic)
$fn[[[CA::class], ':primary']] = function($a) {
    echo "Handling CA\n";
    return "RESULT";
};
// :after hook (runs after primary)
$fn[[[CA::class], ':after']] = function($a) {
    echo "Cleanup after CA\n";
};
// :around wrapper (can see and control the whole call chain)
$fn[[[CA::class], ':around']] = function($callNext, $a) {
    echo "Start outer transaction\n";
    $result = $callNext($a); // This calls before/primary/after
    echo "End outer transaction\n";
    return "[[[$result]]]";
};

echo $fn(new CA()); // Try it out!

Output:

Logging before CA
Start outer transaction
Handling CA
Cleanup after CA
End outer transaction
[[[RESULT]]]

What’s happening?

  • All hooks (:before, :after, :around) are run in the right order.
  • $callNext in :around lets you run or skip the rest.
  • You can combine as many hooks and layers as you want!

Pro tip: Want several :around? Just register more! They stack “outside-in” (the last registered is the innermost).

Testing

Run your tests with PHPUnit:

./vendor/bin/phpunit tests

You’ll see all your dispatch rules and method combinations in action. Both classic and CLOS-style tests are included.

Troubleshooting

  • Function not found errors? Make sure you've run composer dump-autoload and are loading via vendor/autoload.php.
  • Order of registration: If you register multiple methods for the same types, the most specific (class, subclass, interface) wins.
  • :around method argument order: $callNext must come first.

If you get stuck, check out the examples or open an issue.

Links & References

Contributors / Authors

Call to Action

Ready to write cleaner, more extensible PHP—no more “if-else soup”?
Try it out!

  • Please star ⭐ the GitHub project!
  • Try it out in your next side project
  • File issues or request features or share feedback for improvements or contribute extensions

Whether you want classic dispatch, CLOS power features, or both, you’re covered.
Enjoy dispatching!