- Finish Mixable tests.
- Macros need to support arguments by reference.
- Blog post.
- Finish that meme.
Nicer mixins for Macroable
classes in Laravel.
[ TODO: Link to blog post explaining the Macorable
trait; how it does mixins; and what this package does differently. ]
You can install this package via composer:
composer require adrianb93/mixable
There are two traits in this package, Mixin
and Mixable
. They make public methods available to Macroable
classes.
Mixin
is used on a plain PHP class. You specify whichMacroable
classes it mixes into.Mixable
is for a subclass of aMacroable
. It mixes into theMacroable
class it extends.
You register the mixins in your AppServiceProvider
like this:
// Mixin: You specify which Macroable classes it mixes into.
\App\Mixins\CollectionMixin::mix([
\Illuminate\Support\Collection::class,
]);
// Mixable: It mixes into the Macroable class it extends.
\App\Models\Builders\Builder::mix();
AdrianBrown\Mixable\Mixin
is used on a plain PHP class. It macros public methods into Macroable classes you specify.
Example Mixin:
namespace App\Mixins;
use AdrianBrown\Mixable\Mixin;
class LoggerMixin
{
use Mixin;
/**
* Logs $this then returns $this.
*
* @param array $context
* @return $this
*/
public function info($context = [])
{
$message = match (true) {
method_exists($this, 'toSql') => $this->toSql(),
method_exists($this, 'toArray') => $this->toArray(),
default => $this,
};
logger()->info($message, $context);
return $this;
}
}
The package will throw an exception if the Mixin is a subclass of the Macroable. It will instruct you to use the Mixable
trait instead.
Quick Mixin Facts:
-
The
Mixin
trait is a decorator for the Macroable. Methods calls and attributes gets and sets are possible for private, protected, and public visibility. -
When returning
$this
(Mixin), the registered macro switches the return value to the Macroable. -
A decorator on the most part feels like it is Macroable, but it’s not in it’s scope. If you need to be in the Macroable scope, you can use
$this->inScope($callback)
. Example:LoggerMixin::mix(Collection::class) class LoggerMixin { use Mixin; public function whoami(): string { static::class; // => "App\Mixins\LoggerMixin" (the mixin) return $this->inScope(function () { return static::class; // => "Illuminate\Support\Collection" (the macroable) }); } }
AdrianBrown\Mixable\Mixable
is for a subclass of a Macroable
. It macros public methods into the parent class.
Example Mixable:
namespace App\Models\Collections;
use AdrianBrown\Mixable\Mixable;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
class Collection extends EloquentCollection
{
use Mixable;
public function whereBelongsTo($related, $relationshipName = null)
{
...
return $this;
}
}
The package will throw an exception if the Mixable is not a subclass of the Macroable. It will instruct you to use the Mixin
trait instead.
Quick Mixable Facts:
When you call a "Mixable macro":
- The subclass (Mixable) is instantiated without a constructor.
- The state of the parent (Macroable) is copied to the subclass (Mixable).
- The macro’d subclass method is called.
- The registered macro has the return value:
- The state of the subclass (Mixable) is copied to the parent (Macroable).
- If the return value is
$this
(Mixable), it is switched to the parent (Macroable).
- The registered macro returns the value.
When you call a method on an instance of the subclass (Mixable) (not via a "Mixable macro"):
- The
Mixable
trait does nothing. You’re in a normal ol’ instance.
The called method is scoped to the subclass (Mixable) in either case. If parent (Macroable) scope matters, then you can use $this->inScope($callback)
:
\App\Models\Collections\Collection::mix();
namespace \App\Models\Collections;
class Collection extends EloquentCollection
{
use Mixable;
public function whoami()
{
static::class; // => "App\Models\Collections\Collection" (the mixable)
return $this->inScope(fn () => static::class);
}
}
When the method is called from a parent instance (Macroable):
\Illuminate\Database\Eloquent\Collection::make()->whoami();
// => "Illuminate\Database\Eloquent\Collection"
When the method is called from a subclass instance (Mixable):
\App\Models\Collections\Collection::make()->whoami();
// => "App\Models\Collections\Collection"
If there is no parent (Macroable), inScope($callback)
will not change the scope of the callback.
Registering Mixables
The Macroable class the Mixable extends is what it registers the macros to. You register the Mixable in your AppServiceProvider
like this:
\App\Models\Collections\Collection::mix();
Registering Mixins
In your AppServiceProvider
, call the mix()
function on each class that uses the Mixin
trait.
use App\Mixins\LoggerMixin;
public function register()
{
LoggerMixin::mix(Collection::class);
// or
LoggerMixin::mix([
Builder::class,
Request::class,
Collection::class,
]);
}
You can also keep the Macroables inside the class which uses the Mixin
trait. All you would need to do in AppServiceProvider
is:
public function register()
{
LoggerMixin::mix();
}
...and the Mixin can hold the Macroables it should register itself onto:
class LoggerMixin
{
use Mixin;
public $macroable = Collection::class;
// or
public $macroable = [
Builder::class,
Request::class,
Collection::class,
];
...
}
[Mixable] My Mixin isn’t returning an instance of the parent (Macroable), it is returning an instance of the child/subclass (Mixable).
A good example of this is an immutable Macroable like Illuminate\Support\Collection
. Most methods return a new Collection instance. This is not the same instance.
If the return value is not the same instance as the initial Mixable instance, then we do not copy its values to the Macroable instance and we do not swap the return value to the Macroable.
Mixin
is a decorator meaning it uses the magic methods __get()
and __set()
to interact with the Macroable’s class attributes. When passing an attribute to a function that accepts a reference to a value, you run into this warning that the reference is indirect modification.
There are a couple of ways around this issue:
-
Use
$this->inScope($callback)
placing your code within the callback. Property gets and sets within the callback are directly on the Macroable. -
Copy the attribute to a local variable, then set that local variable to the attribute.
$items = $this->items; array_walk($items, fn (&$item) => $items = $item * 2); $this->items = $items;
A Mixable, when called from a macro/Macroable, instantiates the subclass/Mixable without a constructor. The parent’s state is then copied to the child instance.
If you had logic in your constructor that is not getting triggered, then here are some solutions:
-
You could add a
bootMixable()
method to trigger the same setup code you do in your constructor. -
Override the “in” and “out” methods
Mixable
implements and do it your way. The following example is how to make an eloquent query builder instance using another eloquent query builder instance.protected static function newMixableInstance($parent): self { // IN: Create an instance of the mixable subclass which has the methods // we mixed into the parent class. return (new \App\Models\Builders\Builder($parent->getQuery())) ->setModel($parent->getModel()) ->mergeConstraintsFrom($parent); } public function newMacroableInstance(): BaseBuilder { // OUT: Return the macroable instance which the macro was called from. // You could also return `$this` if you're fine with switching // to an instance of the mixable subclass. return (new \Illuminate\Database\Eloquent\Builder($this->getQuery())) ->setModel($this->getModel()) ->mergeConstraintsFrom($this); }
-
Use a
Mixin
instead.Mixable
might not be the right fit for theMacroable
you’re extending.
Mixin
is a decorator of the Macroable. It is a different class.Mixable
is a subclass of the Macroable. It is a different class.
If you need static::class
to give you the Macroable class, then use $this->inScope($callback)
.
public function whoami(): string
{
// Before: return static::class;
return $this->inScope(fn () => static::class);
}
Mixin
is a decorator of the Macroable. It is a different class.Mixable
is a subclass of the Macroable. It is a different class.
If you need $this
to be the Macroable instance, then use $this->inScope($callback)
.
public function notify(): void
{
// Before: ExampleNoticiation::notify($this);
$this->inScope(fn () => ExampleNoticiation::notify($this));
}
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
I like to use dedicated query builders and collections for my models. I have a base collection and query builder with some awesome methods. I use the Mixable
trait on them and created this package because of them. You can learn more about dedicated collections and query builders for eloquent models on Tim MacDonald's blog:
The MIT License (MIT). Please see License File for more information.