Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel saving ordered list of eloquent models

I'm creating a food menu which the administrator can order/sort by dragging and dropping. This menu consists of multiple categories (ProductCategory) and products (Product).

I'm using HTML5Sortable on the client-side to allow nested d&d. The markup is pretty simple:

<div class="categories">
    @foreach($categories as $category)
    <div class="category">
        @foreach($category->products as $product)
        <div class="products">
            <div class=""product" data=id="{{ $product->id }}">
                 {{ $product->name }}
            </div>
        </div><!-- /products !-->
        @endforeach
    </div><!-- /category !-->
    @endforeach
</div>

And the corresponding javascript:

$('.categories').sortable({
    items: '.category'
});
$('.products').sortable({
    items: '.product'
});

// Will be called when the user is done repositioning the products and categories
function getOrderedList() {
    var data = {};

    $('.categories').find('.category').map(function(i) {
        var category = $(this);
        data[i] = {};
        data[i].id = category.data('id');
        data[i].products = category.find('.product').map(function() {
            return $(this).data('id');
        }).get();
    });

    data = JSON.stringify(data); // Send data to server
}

The function getOrderedList will send a JSON string back to Laravel, which contains the sorted category id's and product id's:

{"0":{"id":1,"products":[2,3,1,4,5,6,7,8,9,10]},"1":{"id":2,"products":[11,12,13,14]},"2":{"id":3,"products":[15,16,17,18]}}

How would I make this work on the back-end? I guess I must store this array somewhere in the database and later find and order the models by the id's?

In short: What is a clean and flexible solution for sorting (nested) models (within Laravel)?

like image 406
JasonK Avatar asked Jan 03 '16 17:01

JasonK


3 Answers

A common convention is Weight, add a field called (Int)Weight on the products table, which is used to define the order of the items.

Once a change in the order occurs you only update the weight field.

When you retrieve the items, you sort them by Weight.

it becomes similar to an Array

Id        Name            Weight
01        'product 1'     2
02        'product 2'     0
03        'product 3'     1

when you order it by weight you get

product 2
product 3
product 1

it's similar to an array because

$products[0] = 'product 2'
$products[1] = 'product 3'
$products[2] = 'product 1'

Note that if you want to make it even more dynamic, you can create a polymorphic model that can satisfy multiple models.

Please refer to https://laravel.com/docs/5.1/eloquent-relationships#many-to-many-polymorphic-relations

Polymorphic Relations example

Create table Weights (migration example)

$table->increments('id');
$table->integer('value');
$table->integer('weightable_id')->unsigned();
$table->string('weightable_type');

Create model Weight

class Weight extends Eloquent
{
    public function weightable()
    {
        return $this->morphTo();
    }
}

now with any other model

class Products extends Eloquent
{
    ...
    public function weight()
    {
        return $this->morphOne(Weight::class);
    }
}

this way you can just add that method to any model you want then you can sort your model with it.

P.S. make sure any model that uses it, creates that relation immediately after creating the model

i do not recommend this method, it's much better if you explicitly define the weight field in the Products table, i understand how much you want your code to be dynamic, but everything comes at a cost

Performance goes down, it's not easy to visualize your code once you establish polymorphic relations, its more like starting to use Jumps instead of Functions

like image 85
UX Labs Avatar answered Nov 19 '22 02:11

UX Labs


First, the JSON that you are producing shouldn't be an object where the keys are just array indices. Instead it should be an array of objects that looks like this:

[{"id":1,"products":[2,3,1,4,5,6,7,8,9,10]},{"id":2,"products":[11,12,13,14]},{"id":3,"products":[15,16,17,18]}]

Since the products table to product_categories table has an obvious many to one relationship, you'd just use the product_categories_id foreign key on the products table to represent the relationships laid out in your JSON.

In the nested objects of your JSON, every value in the products key array will have a foreign key that corresponds to the id key value in the same nested object (this is the product_category_id column on your products table).

Your API endpoint function would then look something like this:

public function myApiEndpoint(){

    $input = Input::get('input');
    $input = json_decode($input, true);
    foreach($input as $category){
        Product::whereIn('id', $category['products'])->update(array(
            'product_category_id' => $category['id']
        ));
    }

}

I am updating the model directly in the API controller here, but you should really do any model changes through a repository that's also implementing an interface.

The above will work if you only ever have one menu (with it's categories and products). If you want multiple menus, then you'll need a menus table along with a three way pivot table (with columns menu_id, product_id, and product_category_id).

like image 26
Lloyd Banks Avatar answered Nov 19 '22 03:11

Lloyd Banks


I just implement this behavior using this library: https://github.com/spatie/eloquent-sortable

It is very simple to implement, basically you need an extra column to keep the order and the library will do the rest, here is a part of the documentation:

  • Implement the Spatie\EloquentSortable\Sortable interface.
  • Use the trait Spatie\EloquentSortable\SortableTrait.
  • Optionally specify which column will be used as the order column. The default is order_column.

use Spatie\EloquentSortable\Sortable; use Spatie\EloquentSortable\SortableTrait;

class MyModel extends Eloquent implements Sortable {

use SortableTrait;

public $sortable = [
    'order_column_name' => 'order_column',
    'sort_when_creating' => true,
];

...
}
like image 1
NicuVlad Avatar answered Nov 19 '22 04:11

NicuVlad