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.

2 Likes

Can you create a gist and share here?
Thanks.

I don’t remember where I was at on this, and it was on an old computer. I never actually had a use for it, I was just curious if it could be done, but I did find the files in one of my old github projects, and I can’t tell you if this is actually where I left off or not.

namespace App\Http\Livewire\Testing;

use Illuminate\Database\Query\JoinClause;

class CustomJoinClause extends JoinClause
{
    public function __construct() {}

    public function setConnection($connection): void
    {
        $this->connection = $connection;
        $this->grammar = $connection->getQueryGrammar();
        $this->processor = $connection->getPostProcessor();
        $this->parentConnection = $connection;
        $this->parentGrammar = $connection->getQueryGrammar();
        $this->parentProcessor = $connection->getPostProcessor();
        $this->parentClass = "Illuminate\Database\Query\Builder";
    }

    public function import(JoinClause $import): void
    {
        $this->type = $import->type;
        $this->table = $import->table;
        $this->aggregate = $import->aggregate;
        $this->columns = $import->columns;
        $this->distinct = $import->distinct;
        $this->from = $import->from;
        $this->joins = $import->joins;
        $this->wheres = $import->wheres;
        $this->groups = $import->groups;
        $this->havings = $import->havings;
        $this->orders = $import->orders;
        $this->limit = $import->limit;
        $this->offset = $import->offset;
        $this->unions = $import->unions;
        $this->unionLimit = $import->unionLimit;
        $this->unionOffset = $import->unionOffset;
        $this->unionOrders = $import->unionOrders;
        $this->lock = $import->lock;
        $this->useWritePdo = $import->useWritePdo;
    }
}
namespace App\Http\Livewire\Testing;

use Illuminate\Support\Facades\DB;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Builder;

class QueryBuilderSaver
{
    protected Builder $builder;
    protected QueryBuilder $query;
    protected array $eagers;

    public function set(Builder $builder): void
    {
        $this->query = $builder->getQuery();
        $this->builder = $builder;
        $this->builder->setQuery(null);

        $this->eagers = array_keys($builder->getEagerLoads());
        $this->builder->setEagerLoads([]);

        $this->query->connection = null;

        foreach ($this->query->joins as $key => $join) {
            $custom = new CustomJoinClause;
            $custom->import($join);
            $this->query->joins[$key] = $custom;
        }
    }

    public function get(): Builder
    {
        $this->query->connection = DB::connection();
        $this->builder->setQuery($this->query);

        foreach ($this->query->joins as $join) {
            $join->setConnection(DB::connection());
        }

        foreach ($this->eagers as $eager) {
            $this->builder->with($eager);
        }

        return $this->builder;
    }
}

All of the following is from memory, which may or may not be correct.

Illuminate\Database\Query\JoinClause is what was throwing the hiccup in to being able to hydrate/dehydrate a builder object. I think the QueryBuilderSaver class was meant to be an object that could be serialized (able to be saved to the session), and the CustomJoinClause extends the problematic JoinClause so that the protected properties could be changed.

So you should be able to new up a QueryBuilderSaver object, pass the builder into the set method and then save it to the session, and use the get method to retreive the builder object after you pull the QueryBuilderSaver object out of the session.

Good Luck.

If that does work, I also have no idea if I accounted for all the different configurations of models there could be, so you may have to add more to it.