Passing an Eloquent (or query) Builder as a parameter

I have a use-case where I want to pass an Eloquent Buider object to a Livewire component: I’m trying to make a generic datatable, that I can re-use for different models. The component shouldn’t care about where the data is coming from, as long as it’s a Builder object (where it can then use it’s orderBy and paginate function on).

The problem here is, there is (to my knowledge) not a way to do this at the moment. I can pass the Builder object, and save it to a private property, but whenever the sorting gets changed, or the paging changes, the component “forgets” the Builder object.

I experimented with casting and uncasting with unserialize and serialize, but as the Bulder object contains a PDO instance, it cannot be serialized.

Does anyone have a solution for this issue? I’m getting desparate here :slight_smile:

Thanks in advance

Can you post your component code and show an example of how you want to pass it to the component and where you are passing it from? Also should it be user specific or site specific?

Sure. For example, in my CompanyController:

public function index()
{
    return view('companies.index', ['companies' => User::current()->companies()]);
}

Where ->companies() is a relationship on a User
In companies.index view:

@livewire('datatable', $companies)

And my Component:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithPagination;

class Datatable extends Component
{
    use WithPagination;

    /** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $data */
    private $data;
    public $perPage = 10;
    public $sortField = 'id';
    public $sortAsc = true;
    public $search = '';

    public function mount($data)
    {
        $this->data = $data;
    }

    public function sortBy($field)
    {
        if ($this->sortField === $field) {
            $this->sortAsc = !$this->sortAsc;
        } else {
            $this->sortAsc = true;
        }
        $this->sortField = $field;
    }

    public function render()
    {
        return view(
            'livewire.datatable',
            [
                'pagination' => $this->data->orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->paginate($this->perPage)
            ]
        );
    }
}

This works in the initial load, but when I change the orderBy for example, $this->data will be empty…

Cool. When you are passing $data into the component, will it ever be dynamic or will it be hard coded into each page and you’re just reusing the component?

It should always be a Querybuilder object, in this controller I’m using User::current()->companies() (relationships are also valid Querybuilder objects), but it could as well be Product::where('active', true); to give an example.

Man, this is an awesome question. This is a use case nobody would have ever thought of needing to solve before livewire; because why would a builder need to go outside of one request?

The quick answer is you can’t. Yet.

As you found out, you can’t serialize a PDO statement because it has an active database connection. The solution here is to build an object before that connection is made so that it can be serialized and passed between requests. I’m going to eventually build it, I just have to find a chuck of time to actually sit and do it. That’s why I asked a bunch of questions, I was hoping to be able to work around the builder object, and do something like pass the namespace into the component. But the example Product::where('active', true); kills that idea. I was also trying to figure out an easy way to pull things from the builder to be able to pass, but that would just end up a hack job the more I looked at it.

Deleted garbage workaround, see the next reply.

I’ll post back when I make progress.

Alright, I threw on a pot of coffee and here we are. Throw these into your component or a trait:

public function putBuilderInSession($builder)
{
    $query = $builder->getQuery();
    session(['builder.connectionName' => $query->connection->getConfig('name')]);
    unset($query->connection);
    session(['builder.query' => $query]);
    $model = $builder->getModel();
    session(['builder.model' => $model]);
    $eagerLoads = array_keys($builder->getEagerLoads());
    session(['builder.eagerLoads' => $eagerLoads]);
}
public function getBuilderFromSession()
{
    $query = session()->pull('builder.query');
    $connectionName = session()->pull('builder.connectionName');
    $query->connection = DB::connection($connectionName);
    $model = session()->pull('builder.model');
    $builder = new EloquentBuilder($query);
    $builder->setModel($model);
    $eagerLoads = session()->pull('builder.eagerLoads');
    foreach ($eagerLoads as $eagerLoad) {
        $builder = $builder->with($eagerLoad);
    }
    return $builder;
}

In your hydrate() method:
$this->data = $this->getBuilderFromSession()
And in your render() method before you return the view:
$this->putBuilderInSession($this->data)

When your component hydrates, it will have an almost exact copy of the builder object you passed into mount(). The only caveat is I can’t figure out how to get any of the constraints for an eager loaded relationship, so unfortunately it’s going to load the whole relationship.

For example, if you have something like ->with('author:id,name') somewhere in the pipeline, or if you are registering that in your model in the $with array, the relation won’t contain only the id and name.

That constraint information is stored in a closure which you can’t serialize. I’m assuming there is some way to make use of it, but I don’t know how. If someone does, I can fix that issue. This closure is the only place I can find where you have access to the constraints without creating a modified Eloquent\Builder class. It looks something like this:

"author" => Closure($query) {
  class: "Illuminate\Database\Eloquent\Builder"
  use: {
    $name: "author:id,name"
  }
  file: ".../laravel/framework/src/Illuminate/Database/Eloquent/Builder.php"
  line: "1137 to 1139"
}

Let me know if you run into any issues.

Hi Dale

Thanks a lot for the help. Sadly, it didn’t solve the issue. The thing is, when you have joins, they also include an instance of the PDOConnection (and they also have a parentConnection property, which is also PDOConnection, and is private, so not changable). I was also looking for an easier implementation than practically rewriting building up an EloquentBuilder. Therefor, if there isn’t an easy way to fix it, or Livewire itself is not getting changed to include this kind of functionality, I think I’ll need to take another route to tackle this problem, or not use Livewire alltogether :frowning:.

Anyway, I love how enthusiastic you handled this problem. Continue being awesome :wink:

Ahh, I see what you’re saying. Of course the parent connection is protected for no reason. It’s the same instance in memory that is public higher up…

I don’t see that happening, at least any time soon. The functionality to persist protected properties out of the box was already removed, so I don’t see it going in that direction. (Maybe if casting protected properties was made a little easier. -Cough cough- @calebporzio lol)

Anyway, there’s still enough information to rebuild it. I think you will catch where I’m going with this.
Boom Joins:

public function putBuilderInSession($builder)
{
    $query = $builder->getQuery();
    session(['builder.connectionName' => $query->connection->getConfig('name')]);
    unset($query->connection);

    foreach($query->joins as $join) {
        $params = [
            'table' => $join->table,
            'first' => $join->wheres[0]['first'],
            'operator' => $join->wheres[0]['operator'],
            'second' => $join->wheres[0]['second'],
            'type' => $join->type
        ];
        session()->push('builder.joins', $params);
    }
    unset($query->joins);

    session(['builder.query' => $query]);
    $model = $builder->getModel();
    session(['builder.model' => $model]);
    $eagerLoads = array_keys($builder->getEagerLoads());
    session(['builder.eagerLoads' => $eagerLoads]);
}

public function getBuilderFromSession()
{
    $query = session()->pull('builder.query');
    $connectionName = session()->pull('builder.connectionName');
    $query->connection = DB::connection($connectionName);
    $model = session()->pull('builder.model');
    $builder = new EloquentBuilder($query);
    $builder->setModel($model);
    $eagerLoads = session()->pull('builder.eagerLoads');
    foreach ($eagerLoads as $eagerLoad) {
        $builder = $builder->with($eagerLoad);
    }

    $joins = session()->pull('builder.joins');
    foreach ($joins as $join) {
        $builder = $builder->join($join['table'], $join['first'], $join['operator'], $join['second'], $join['type']);
    }

    return $builder;
}

In the end, probably will want to refactor this into a class instead of a trait.

Well, throw me another. I’ll wait here for you to come back and say it doesn’t handle your crazy sub joins and on clauses, or whatever else craziness you have going on over there :smile:

If anyone comes across this problem again, I think I ended up figuring this out. I’ll put up a gist if anyone needs it.