MacroMix (Mixin Trait)#

Have you ever wished your PHP class had a certain method—even though it’s not defined in the class? MacroMix lets you dynamically add methods (macros) at runtime to any class without requiring a base class. It works purely via PHP’s __call / __callStatic magic under the hood.

Advantages#

  • Zero coupling — You don’t need to extend a base class. Just use MacroMix;.

  • Method chaining — If your macro returns null, it automatically returns $this so you can continue chaining.

  • Config or annotation loading — Bulk-load macros from arrays or @Macro("name") annotations.

  • Thread safety (optional) — Enable a lock if you care about concurrency.

Basic Usage#

Include the trait in any class:

namespace App;

use Infocyph\InterMix\Remix\MacroMix;

class House
{
    use MacroMix;

    // Optional: enable thread-safe macro registration
    public const ENABLE_LOCK = true;

    protected string $color = 'gold';
}

Registering a Macro#

public static function macro(string $name, callable|object $macro): void
  • $name: method name you want to add.

  • $macro: a callable or object. Closures are wrapped so that if they return null, $this is returned instead for chaining.

Example:

House::macro('fill', function (string $color) {
    $this->color = $color;
    return $this;
});

$house = new House();
$house->fill('blue')->fill('green');
echo $house->color;  // "green"

Checking and Removing Macros#

public static function hasMacro(string $name): bool
public static function removeMacro(string $name): void

Example:

House::macro('sayHello', fn() => 'Hello');
echo House::hasMacro('sayHello'); // true

House::removeMacro('sayHello');
echo House::hasMacro('sayHello'); // false

Calling Macros#

Any time you call an undefined method (static or instance), MacroMix checks its internal registry:

  • If the macro exists, it invokes it—binding $this if needed.

  • If the macro returns null, the trait yields $this (or the class name, if invoked statically).

  • If the macro is not found, it throws:

Exception: Method ClassName::missingMacro does not exist.

Example:

House::macro('floor', fn() => 'I am the floor');
echo House::floor();  // “I am the floor”

Loading from Configuration#

public static function loadMacrosFromConfig(array $config): void
  • $config: associative array of ['name' => callable, ...]

  • If ENABLE_LOCK is set, a lock is acquired during registration.

Example:

$config = [
    'toUpper' => fn($s) => strtoupper($s),
    'reverse' => fn($s) => strrev($s),
];
House::loadMacrosFromConfig($config);

$h = new House();
echo $h->toUpper('gtk');   // “GTK”
echo $h->reverse('php');   // “php”

Loading from Annotations#

public static function loadMacrosFromAnnotations(string|object $class): void
  • Scans public method PHPDoc for @Macro("name").

  • Registers the method under the given name.

Example:

class MyMixin
{
    /**
     * @Macro("shout")
     */
    public function shout(string $text): string
    {
        return strtoupper($text) . '!';
    }
}

House::loadMacrosFromAnnotations(MyMixin::class);

$h = new House();
echo $h->shout('hello');  // “HELLO!”

Retrieving All Macros#

public static function getMacros(): array

Returns a list of all registered macros in the form:

['macroName' => callable, ...]

Example:

House::macro('one', fn() => 1);
House::macro('two', fn() => 2);

$macros = House::getMacros();
// ['one' => callable, 'two' => callable]

Mixing in an Entire Class or Object#

public static function mix(object|string $mixin, bool $replace = true): void
  • $mixin: either an object instance or class name.

  • Public and protected methods are reflected.

  • Static methods wrap static invocation; non-static methods bind to instance.

  • $replace = false skips existing macro names.

Example:

$mixin = new class {
    public function greet(string $name): string {
        return "Hello, $name!";
    }
    protected function whisper(string $msg): string {
        return "psst... $msg";
    }
};

House::mix($mixin);
$h = new House();
echo $h->greet('World');  // “Hello, World!”
echo $h->whisper('John'); // “psst... John”

Error on Undefined Macro#

If you call a macro that doesn’t exist:

$house->nonexistent();

Results in:

Exception: Method App\House::nonexistent does not exist.

Thread Safety#

If you define the constant:

class House {
    use MacroMix;
    public const ENABLE_LOCK = true;
}

Then:

  • Write operations (macro(), removeMacro(), loadMacrosFromConfig()) acquire an exclusive file lock on the trait source.

  • Read operations (e.g. hasMacro(), getMacros(), macro calls) skip locking.