Laravel’s mysteriously macroable paginators
Laravel’s Macroable
trait is a very neat way to add new functionality to built-in classes.
I recently found myself with the need to add a custom method to the LengthAwarePaginator
class. Not a problem, I thought, I’ll write a quick macro.
A fine solution, but for the fact the LengthAwarePaginator
isn’t “macroable”. Or so it would seem at first glance.
How to fix a problem that doesn’t exist
Upon discovering that the LengthAwarePaginator
doesn’t use the Macroable
trait, my first thought was that I should add this functionality, and submit a pull request. It’s trivially easy to do, and requests to make classes macroable are generally met with fairly swift approval.
Being the good little developer that I am, I started by writing a test:
<?php
namespace Illuminate\Tests\Pagination;
use PHPUnit\Framework\TestCase;
use Illuminate\Pagination\LengthAwarePaginator;
class LengthAwarePaginatorTest extends TestCase
{
public function setUp()
{
$this->p = new LengthAwarePaginator($array = ['item1', 'item2', 'item3', 'item4'], 4, 2, 2);
}
public function tearDown()
{
unset($this->p);
}
// Existing tests...
public function testLengthAwarePaginatorIsMacroable()
{
$this->p->macro('foo', function () {
return 'bar';
});
$this->assertEquals('bar', $this->p->foo());
}
}
Then I ran my test, watched it fail, and… it passed. Something was clearly afoot.
After confirming that neither the LengthAwarePaginator
, nor the AbstractPaginator
use the Macroable
trait, I started digging.
Confirming the impossible
The following code confirms that neither the LengthAwarePaginator
, nor any other class in its inheritance tree, uses the Macroable
trait:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use Illuminate\Pagination\LengthAwarePaginator;
$reflector = new ReflectionClass(LengthAwarePaginator::class);
dd(getTraits($reflector)); // Outputs []
function getTraits(ReflectionClass $reflector, array $traits = []): array
{
if ($reflector->getParentClass()) {
$traits = getTraits($reflector->getParentClass(), $traits);
}
return array_merge($traits, $reflector->getTraitNames());
}
And yet, somehow, the LengthAwarePaginator
has all of the methods inherited from the Macroable
trait.
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use Illuminate\Pagination\LengthAwarePaginator;
$paginator = new LengthAwarePaginator(['a', 'b', 'c', 'd'], 4, 2, 2);
$paginator->macro('foo', function () {
return 'foo';
});
$paginator->hasMacro('foo'); // Outputs true
$paginator->hasMacro('bar'); // Outputs false
$paginator->foo(); // Outputs 'foo'
The magic macro method
Further code spelunking finally led me to the __call
magic method on the AbstractPaginator
class:
/**
* Make dynamic calls into the collection.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->getCollection()->$method(...$parameters);
}
/**
* Get the paginator's underlying collection.
*
* @return \Illuminate\Support\Collection
*/
public function getCollection()
{
return $this->items;
}
As you can see, AbstractPaginator::__call
passes any requests for unknown methods to the paginator’s $items
property. $items
is an instance of the Illuminate\Support\Collection
class, which is “macroable.”
So, behind the scenes, this is what’s happening:
- We call the
macro
method on theLengthAwarePaginator
- The
macro
method doesn’t exist on theLengthAwarePaginator
class, and neither does the__call
magic method, so PHP checks the parentAbstractPaginator
class - The
macro
method doesn’t exist on theAbstractPaginator
class either, but the__call
method does AbstactPaginator::__call
attempts to call themacro
method on the$items
property, which is an instance ofIlluminate\Support\Collection
- The
macro
method doesn’t exist directly on theIlluminate\Support\Collection
class, but it does exist on theMacroable
trait, which theCollection
class uses - Finally, at long last, the
Macroable::macro
method runs
I’m accustomed to encountering TaylorMagic™ when working with Laravel, but I can’t imagine this was intentional. It’s far too obtuse.
That said, it does work perfectly, so if you ever wished the LengthAwarePaginator
was macroable, I have good news; it is.
Sign up for my newsletter
A monthly round-up of blog posts, projects, and internet oddments.