spatie/macroable: adding methods to a PHP class at runtime
A trait to dynamically add methods to a class
At a glance
- What is it?
- A single trait that lets you attach closures to a class after it exists, the same trick Laravel uses for its own macroable classes, packaged on its own for projects that are not Laravel. It is a small idea with a small set of sharp edges around state and typing.
- Who is it for?
- Adopt spatie/macroable if you are in PHP, want to extend a class or an object at runtime without subclassing it, and are comfortable with methods that exist only at runtime and are invisible to static analysis. Skip it if your codebase is typed strictly and relies on static analysis to catch calls to methods that do not exist, since that is exactly what this pattern removes.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem is a class you cannot edit
There are situations in PHP where you want a class to have a method it does not have, and you cannot change the class. It is a vendor class, so you are not going to fork it. It is a class you did not write, and the method you want is specific to your application.
The usual answers are all heavier than the problem. You can subclass it, which means the type is different and you have to inject the subclass rather than the original. You can use composition, which means every call site becomes a wrapper. You can write a decorator, which is the same idea with more code.
The approach spatie/macroable takes is to let the class itself grow a method at runtime. This package provides a trait that, when applied to a class, makes it possible to add methods to that class at runtime.
The example is a one liner:
$myClass = new class() {
use Spatie\Macroable\Macroable;
};
$myClass::macro('concatenate', function(... $strings) {
return implode('-', $strings);
});
$myClass->concatenate('one', 'two', 'three'); // returns 'one-two-three'Note the detail that surprises people first. The macro is registered with a static call on the class, but the method is then invoked on the instance. The registration is global to the class, not to the object.
The idea and the implementation are taken from the macroable trait of the Laravel framework, which is stated both in the intro and in the credits. This package exists so that the same capability is available outside Laravel, in a framework or a codebase that has no framework at all.
The closure is bound to the class, which is the whole trick
A macro registered as a plain closure would have no access to the object it is called on. The magic is in the binding.
Callables passed to the macro function will be bound to the class. The documentation demonstrates this with a protected property:
$macroableClass = new class() {
protected $name = 'myName';
use Spatie\Macroable\Macroable;
};
$macroableClass::macro('getName', function() {
return $this->name;
};
$macroableClass->getName(); // returns 'myName'So the closure runs with $this pointing at the object, and it can read protected and private state that a caller could not touch directly. That is what makes the pattern useful rather than merely cute: a macro can act like a method that was written into the class, including accessing its internals.
There is a subtlety in that snippet worth pausing on, because it is in the official example. The macro closure is registered with a semicolon inside the parentheses rather than a comma, so the call is written as a method call to the function macro with the closure as the argument and then a stray semicolon. The example still reads as passing a closure to macro, and the result is as documented. But it is a small illustration of how thin the documentation is on the exact calling convention, and it is worth checking the signature in the source when you adopt the package.
Binding is also why the trait is not a general decorator. The closure is not wrapped around the object, it is inside it, with the same access to private state that a real method would have.
Mixins register a whole batch of methods at once
Registering one closure at a time is fine for one method. For a family of related methods, the package offers a mixin class.
You can also add multiple methods in one go by using a mixin class. A mixin class contains methods that return callables. Each method from the mixin will be registered on the macroable class.
$mixin = new class() {
public function mixinMethod()
{
return function() {
return 'mixinMethod';
};
}
public function anotherMixinMethod()
{
return function() {
return 'anotherMixinMethod';
};
}
};
$macroableClass->mixin($mixin);So the shape is inverted relative to a normal macro registration. Instead of passing a closure directly, you pass an object whose methods each return a closure, and every one of those closures becomes a method on the target class. The mixin methods themselves are not copied, only the callables they return.
The value of that shape is organisational rather than technical. A mixin can be a class in your own codebase, so a set of related methods lives in one file with one namespace and one set of dependencies, and it can be registered in a service provider or a bootstrap file. That turns runtime method injection from something you scatter around the code into something with a defined home.
It also means the mixin is the unit of authorship. Whoever writes the mixin decides what gets attached to the class, and the target class does not need to know about it.
Installing and running the tests
Installation is a Composer package:
composer require spatie/macroableThe package is published on Packagist as spatie/macroable, under the MIT licence, and the repository has the usual shape for a small PHP library: a src directory, a tests directory, a phpunit.xml.dist file, a config directory and a CHANGELOG.md.
The tests run with:
composer testThe badge in the readme for the run-tests workflow points at a GitHub Actions workflow named run-tests, so the package is tested on every push through Composer scripts rather than through a bespoke CI file in the tree.
Two operational notes from the project documentation. Security bugs are handled by email rather than the issue tracker, at [email protected], which is a sensible arrangement for a library that other people install. And the readme carries the postcardware request, asking that if the package makes it to your production environment you send a postcard to the company's address in Antwerp, mentioning which of their packages you are using. That is the project's stated support model, and it also explains the sponsor links in the same file.
The contributing guide is the shared one from the Spatie organisation, and the changelog is the record of what changed recently.
The state of the package, and the gap in the release history
The repository is not archived, and the last push was on 2026-06-26. The licence is MIT, with the terms in LICENSE.md.
The release history is the interesting part. There are three recent releases: 2.1.0 on 2026-01-30, 2.0.0 on 2021-03-26, and 1.0.1 on 2020-11-03. So the previous major version shipped in March 2021 and the next one arrived in January 2026, roughly five years later.
What that gap means is worth stating plainly rather than glossing over. The package is not abandoned, since the code was pushed to in June 2026 and a new major version appeared in January. But the cadence has been irregular, and a five year gap between majors means the 2.0.0 to 2.1.0 diff is where the accumulated changes live. If you are on 2.0.0 and considering the upgrade, the changelog is the only place that tells you what changed.
The topic tags on the repository are macros, php, runtime and utility, which is an honest description of the scope. This is a utility, not a framework, and the fact that the whole implementation fits in a single trait means the upgrade risk is small in absolute terms even when the gap between releases is long.
Where runtime methods are the wrong tool
There are situations where this pattern is the wrong answer, and the reasons are not about taste.
The first is static analysis. A method that exists only at runtime does not exist as far as your IDE, your language server or your static analyser are concerned. Every macro call is a call to a method the analyser cannot see, so you lose the checking you would have had with a real method. If your project relies on PHPStan or Psalm to catch calls to methods that do not exist, this pattern removes that safety net by design.
The second is discoverability. A macro registered in a service provider is invisible to anyone reading the class. The person who opens the model and does not find getName has to know that some bootstrap file somewhere attaches it. That cost is real and it scales with the number of macros and the size of the team.
The third is state. Macros are registered on the class, which means they are global to that class for the lifetime of the process. Two parts of your application registering different implementations of the same macro name will collide, and which one wins depends on load order. This is manageable when the registration happens in exactly one place, and it is a genuine hazard when it happens in several.
The fourth is that the closure is bound, which means a macro can read private state. That is the feature, and it is also the risk. A macro that mutates protected state is invisible in the class's public surface and can surprise the next reader.
The fifth is the simple one. If you can edit the class, edit the class. A trait that exists to add methods to a class you control is a workaround for a problem you do not have.
Against Laravel's own Macroable, and against plain inheritance
Two comparisons settle the choice.
The first is against Laravel's Macroable trait. This package is that idea extracted: the idea and the implementation are taken from the macroable trait of the Laravel framework. So the difference is entirely about dependencies. If your application already runs Laravel, Illuminate's trait is already there, and adding spatie/macroable means having two implementations of the same pattern with two different static registries, since a macro registered on one is invisible to the other. The only reason to prefer the standalone package inside a Laravel application would be a codebase that has moved away from Illuminate support while keeping the macro pattern.
Outside Laravel, the picture reverses. Illuminate's macroable trait is not available as a standalone package in a meaningful way, so spatie/macroable gives you the pattern with a composer dependency and no framework. That is the package's actual use case, and the readme says as much by stating the implementation came from Laravel.
The second comparison is against plain inheritance. A subclass with a real method is visible, typed, statically analysable and overridable. A macro is none of those. Inheritance costs you a different type, which is why people reach for macros in the first place, but the situations where inheritance is genuinely unusable are narrower than they first appear: a class you cannot change, a class you want to extend without a subclass, or a framework API you want to extend without forking it.
If your reason for wanting macros is that you want to extend a third party class, macroable is a reasonable answer. If your reason is that you want to avoid writing a subclass for your own class, inheritance is the cheaper and clearer answer.
Editorial conclusion
Adopt spatie/macroable if you are in PHP, want to extend a class or an object at runtime without subclassing it, and are comfortable with methods that exist only at runtime and are invisible to static analysis. Skip it if your codebase is typed strictly and relies on static analysis to catch calls to methods that do not exist, since that is exactly what this pattern removes. Verify first that the callables you register are bound to the class you expect, since the binding is what makes $this work inside the closure, and check the changelog before upgrading, because the release history has a long gap between 2.0.0 and 2.1.0.
Frequently asked questions
What does the spatie/macroable trait do?
It provides a trait that, when applied to a class, makes it possible to add methods to that class at runtime. The methods are registered as closures and are callable on instances of the class.
How do I install spatie/macroable?
Install it with Composer using composer require spatie/macroable. The package is on Packagist as spatie/macroable, and its tests run with composer test.
Can a spatie/macroable closure access the class properties?
Yes. Callables passed to the macro function will be bound to the class, so inside the closure $this points at the object, including for protected properties such as the getName example in the readme.
How do I register several macro methods at once in spatie/macroable?
Use a mixin class. A mixin class contains methods that return callables, and each method from the mixin will be registered on the macroable class when you call the mixin method on it.
Where does the spatie/macroable implementation come from?
The idea and the implementation are taken from the macroable trait of the Laravel framework, and the package credits the Laravel framework for it. spatie/macroable packages the pattern for use outside Laravel.
How often is spatie/macroable released?
Irregularly. The recent releases are 2.1.0 on 2026-01-30, 2.0.0 on 2021-03-26 and 1.0.1 on 2020-11-03, so there is roughly a five year gap between the last two majors. The repository is not archived and the last push was on 2026-06-26.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/spatie-macroable)Was this useful?