Summer Sale! All accounts are 50% off this week.

me_macfly's avatar

Dynamic Table name

Hi,

I have looked in the forum but could not apply the solutions I have found.

On a sample database like this:

categorya_products
categoryb_products
categoryc_products
.
.
.
users

Instead of creating an Eloquent model for each category, I would like to have one model and change the database table dynamically:

Product Model

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{

    // protected $table = '';

    public function users()
    {
        return $this->belongsTo( User::class );
    }

}

User Model

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{

    public function products()
        {
            return $this->hasMany( Product::class );
        }
}

Controller

        $categories = Category::orderBy( 'name', 'desc' )->get();

        foreach ($categories as $category)
        {
            $products[] = \Auth::user()->products()->get();
        }

How do I set the table in the product model? I have tried different ways but all failed.

Right now I am using one Model for each category table but this is kinda messy.

Cheers

0 likes
12 replies
me_macfly's avatar

Thanks @bobbybouwmann

I have seen this post and looked into it.

The issue I have is that I call the newRelatedInstance from hasMany and I do not know how to set the table for the related table with this method.

my entry point to get all products related to a user is:

\Auth::user()->products()->get();

I have tried:

    \Auth::user()->products( $category )->get();


    public function products( $category )
    {
        return $this->hasMany( new Product( $category.'_products' ) );
    }

    public function __construct($table)
    {
        $this->setTable($table);
    }

Also tried:

    public function products( $category )
    {
        $product = new Product();
        $product->setTable( $category.'_products' );

        return $this->hasMany( $product );
    }

Do I need to write my own relation between the tables and cannot use eloquents RelatedInstance or what am I missing here?

bobbybouwmann's avatar

I don't think you can do this in the model itself. I think you need some other class for this. Also I don't think this will work on relationships at all! You simply need to pass the class reference and not the class itself. It won't use that class for fetching the results.

I think what you're trying to do is impossible! Sorry about that

me_macfly's avatar

Thanks @bobbybouwmann

So my only workaround would be to create the relationship between each model by my self or using a model for each category?

Snapey's avatar

Could you add a user scope to the products model and then switch the query around

$product = new Product($category);

$products = $product->forUser(Auth::user())->get();

Product model

public function __construct($category)
{
    $this->table = $category;
}

public function scopeForUser($query, $user)
{
    return $query->where('user_id', $user->id);
}

IDK - something like that...

I seem to remember an issue though where $products = $product->forUser(Auth::user())->get(); ignores the model created initially and news up a brand new model.

BezhanSalleh's avatar

the post that @bobbybouwmann mentioned earlier is your solution... you just didn't look in the right place. the selected answer has a problem which is already explained in the one just after that. give this a try:

// BindsDynamically.php
trait BindsDynamically
{
    protected $connection = null;
    protected $table = null;

    public function bind(string $connection, string $table)
    {
        $this->setConnection($connection);
        $this->setTable($table);
    }

    public function newInstance($attributes = [], $exists = false)
    {
        // Overridden in order to allow for late table binding.

        $model = parent::newInstance($attributes, $exists);
        $model->setTable($this->table);

        return $model;
    }

}
//Product.php 
namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{

    use BindsDynamically;

    public function user()
    {
        return $this->belongsTo( User::class );
    }

}
// YourController.php
        $categories = Category::orderBy( 'name', 'desc' )->get();

        foreach ($categories as $category)
        {
        $product = auth()->user()->products()->first();
            $product->getTable();
            //replace connection with your db connection name 'mysql' if you are using it...
        $product->bind('connection', $category.'_products'); 
        //$product->get(); //select * from $category_products [categorya_products...]
            // not 100% sure about this auth()->user()->products()->get(); so
            $products[] = $product->where('user_id',auth()->id())->get();
        }

this should work...

1 like
me_macfly's avatar

@Snapey Thanks. A user scope could work. I will have to play around with this idea.

@BezhanSalleh You are correct, I only looked at the accepted answer and overlooked the bottom part of it. Thanks for this. However, I still struggle to make it work even with your solution and here is why:

$product = auth()->user()->products()->first();

This already cause an issue as there is no database table "products" and the product model will try to use its own name scope as database table.

I would need to use the binding trait beforehand but than I am working on a product object and cannot go back to the user model as the table is only set within its own scope.

The only way I have right now is:

// Controller.php
$product = new Product;
$product->bind('mysql', $category->name.'_products' );

$products[] = $product->where('user_id',auth()->id())->get();

However, this makes the hasMany relationship redundant as I create the relation myself.

Can you also please explain the following part? I cannot get my head around what the use-case would be for this:

    public function newInstance($attributes = [], $exists = false)
    {
        // Overridden in order to allow for late table binding.

        $model = parent::newInstance($attributes, $exists);
        $model->setTable($this->table);

        return $model;
    }
BezhanSalleh's avatar

@me_macfly yeah the controller should be what you said and yeah it will since you are changing the table for the model.

// Controller.php
$product = new Product;
$product->bind('mysql', $category->name.'_products' );

$products[] = $product->where('user_id',auth()->id())->get();

the newInstance method part is for when you want to perform any insert on the model after you dynamically change the table. for situations like

$product = new Product;
$product->getTable(); // products
$product->bind('mysql', $category_product);
$product->get(); // select * from $category_product
$product->first(); // select * from $category_product limit 1

// with the newInstance method you will be able to do the following.
$product->column = 'test'; //$category_product->column
$product->save();
// if you just wanna perform selects then you can remove that part but if you later on need to change something leave it as it is.

biishmar's avatar

The way of relating category and product kinda weird or might say problematic, can u share the category model and how u relating it...

Artistan's avatar

per @BezhanSalleh you probably want to override the replication method as well


    /**
     * Clone the model into a new, non-existing instance.
     *
     * @param  array|null  $except
     * @return \Illuminate\Database\Eloquent\Model
     * @throws \Exception
     */
    public function replicate(array $except = null)
    {
        // Overridden in order to allow for late table binding.
        $instance = parent::replicate($except);
        $instance->setTable($this->table);

        return $instance;
    }

I have not fully tested this yet...

Sairahcaz's avatar

I created a package for this: Laravel Dynamic Model

Feel free to use it: https://github.com/laracraft-tech/laravel-dynamic-model

This basically allows you to do something like this:

$foo = App::make(DynamicModel::class, ['table_name' => 'foo']);

$foo->create([
    'col1' => 'asdf',
    'col2' => 123
]);

$faz = App::make(DynamicModel::class, ['table_name' => 'faz']);
$faz->create([...]);

Please or to participate in this conversation.