javoscript/laravel-macroable-models
Adds Macroable support to Eloquent models, letting you define and register reusable model macros for dynamic methods at runtime. Great for extending models cleanly across packages and projects without touching the model class directly.
Installation Add the package via Composer:
composer require javoscript/laravel-macroable-models
Publish the config (if needed) and register the service provider in config/app.php under providers:
Javoscript\MacroableModels\MacroableModelsServiceProvider::class,
First Use Case
Define a macro on a model to extend its functionality dynamically. For example, add a fullName method to a User model:
use Javoscript\MacroableModels\Macroable;
class User extends Model implements Macroable
{
public static function bootMacroable()
{
static::addMacro('fullName', function () {
return "{$this->first_name} {$this->last_name}";
});
}
}
Now use it in your code:
$user = User::find(1);
echo $user->fullName(); // Outputs: "John Doe"
Where to Look First
README.md for examples.Macroable.php to understand how macros are registered and invoked.MacroableModelsServiceProvider to see if it adds global macros or modifies Laravel’s behavior.Model-Specific Macros
Define macros directly in the model’s bootMacroable() method (as shown above). This keeps logic scoped to the model.
class Post extends Model implements Macroable
{
public static function bootMacroable()
{
static::addMacro('isPublished', function () {
return $this->published_at !== null;
});
}
}
Usage:
if ($post->isPublished()) { ... }
Global Macros Register macros globally in the service provider for reuse across models:
// MacroableModelsServiceProvider.php
public function register()
{
Model::addGlobalMacro('toArraySafe', function () {
return $this->toArray() ?? [];
});
}
Usage:
$model = new Model();
$model->toArraySafe(); // Works on any model
Dynamic Macros with Closures Use closures to create flexible macros that accept parameters:
class Product extends Model implements Macroable
{
public static function bootMacroable()
{
static::addMacro('applyDiscount', function ($percentage) {
return $this->price * (1 - $percentage / 100);
});
}
}
Usage:
$discountedPrice = $product->applyDiscount(20); // 80% of original price
Macros for Query Scopes Extend Eloquent queries dynamically:
class User extends Model implements Macroable
{
public static function bootMacroable()
{
static::addMacro('scopeActive', function (Builder $query) {
return $query->where('active', true);
});
}
}
Usage:
$activeUsers = User::active()->get(); // Equivalent to `User::where('active', true)->get()`
Conditional Macros Add logic to macros to conditionally alter behavior:
class Order extends Model implements Macroable
{
public static function bootMacroable()
{
static::addMacro('getStatus', function () {
if ($this->completed_at) {
return 'completed';
}
return 'pending';
});
}
}
public function test_user_full_name_macro()
{
$user = new User(['first_name' => 'Jane', 'last_name' => 'Doe']);
$this->assertEquals('Jane Doe', $user->fullName());
}
/**
* @return string
*/
public function fullName() { ... }
static::addMacro('expensiveCalculation', function () {
return cache()->remember("expensive_{$this->id}", now()->addHours(1), function () {
return $this->price * 1.2; // Simulate expensive logic
});
});
Macro Overwriting If two macros share the same name, the last one registered will overwrite the previous. Avoid naming conflicts:
// Bad: Both define 'isActive'
static::addMacro('isActive', fn() => true);
static::addMacro('isActive', fn() => false); // Overwrites the first
Scope Macros vs. Global Macros
scopeActive) must follow Eloquent’s naming convention (scope*) to work with query builders.Late Static Binding in Macros
Macros use static:: for method calls, which can cause issues if the model is extended or replaced. Ensure macros are defined in the base model class:
// Good: Defined in User model
class User extends Model { ... }
// Bad: Defined in a trait or extended class (may break static binding)
Serialization Issues Macros are not serialized with the model. If you serialize a model (e.g., for caching), macros won’t persist. Avoid relying on macros in serialized contexts.
Debugging Macro Calls
Macros don’t appear in dd($model) output. Use get_method() to inspect available macros:
dd(get_class_methods($user)); // Check if 'fullName' is listed
Or log macro invocations:
static::addMacro('debugMacro', function () {
\Log::info("Macro called on ID: {$this->id}");
return $this->id;
});
Model::addGlobalMacro('__debugMacroCall', function ($macroName) {
\Log::debug("Macro '$macroName' called on " . static::class);
return $this;
});
Usage:
$user->__debugMacroCall('fullName')->fullName();
// Fails if macro is 'fullName' but called as 'FullName'
$user->FullName(); // Error: Method does not exist
php artisan optimize:clear
Custom Macro Storage
Override the macro storage mechanism by extending the Macroable trait or implementing your own macro registry:
class CustomMacroable
{
protected static $macros = [];
public static function addMacro($name, $macro)
{
static::$macros[$name] = $macro;
}
public function __call($method, $parameters)
{
if (isset(static::$macros[$method])) {
return call_user_func_array(static::$macros[$method], [$this, ...$parameters]);
}
throw new \BadMethodCallException("Method {$method} does not exist.");
}
}
Macro Events Trigger events when macros are called (e.g., for analytics):
static::addMacro('trackMacro', function ($eventName) {
event(new MacroCalled($this, $eventName));
return $this;
});
Macro Validation Add validation to macros to ensure they’re used correctly:
static::addMacro('safeDivide', function ($divisor) {
if ($divisor === 0) {
throw new \InvalidArgumentException("Divisor cannot be zero.");
}
return $this->value / $divisor;
});
**
How can I help you explore Laravel packages today?