PHPackages                             alexcrawford/lexorank-sortable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. alexcrawford/lexorank-sortable

ActiveLibrary[Database &amp; ORM](/categories/database)

alexcrawford/lexorank-sortable
==============================

Adds sortable behavior and ordering to Laravel Eloquent models. Grouping and many to many supported.

8.1.0(3d ago)817.2k—0.7%1MITPHPPHP &gt;=8.1CI failing

Since Jun 28Pushed 3d agoCompare

[ Source](https://github.com/alexcrawford/lexorank-sortable)[ Packagist](https://packagist.org/packages/alexcrawford/lexorank-sortable)[ RSS](/packages/alexcrawford-lexorank-sortable/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (1)Dependencies (14)Versions (45)Used By (0)

[![Tests](https://github.com/AlexCrawford/lexorank-sortable/actions/workflows/tests.yml/badge.svg)](https://github.com/AlexCrawford/lexorank-sortable/actions/workflows/tests.yml)[![Code Formatting](https://github.com/AlexCrawford/lexorank-sortable/actions/workflows/pint.yml/badge.svg)](https://github.com/AlexCrawford/lexorank-sortable/actions/workflows/pint.yml)[![Static Analysis](https://github.com/AlexCrawford/lexorank-sortable/actions/workflows/phpstan.yml/badge.svg)](https://github.com/AlexCrawford/lexorank-sortable/actions/workflows/phpstan.yml)

Install
-------

[](#install)

Install package through Composer

```
composer require alexcrawford/lexorank-sortable
```

### Version Compatibility

[](#version-compatibility)

LaravelSortable41.2.x (branch laravel4)&lt;=5.33.2.x5.43.4.x5.54.2.x5.74.7.x6.06.0.x7.x, 8.x8.x.x9.x, 10.x, 11.x, 12.x9.x.xSortable Trait
--------------

[](#sortable-trait)

Adds sortable behavior to Eloquent (Laravel) models

### Usage

[](#usage)

Add `position` field to your model (see below how to change this name):

```
// schema builder example
public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        // ... other fields ...
        $table->string('position'); // Your model must have position field
    });
}
```

#### Important: Database Collation for Position Column

[](#important-database-collation-for-position-column)

This package uses **LexoRank** for position management, which relies on lexicographic (ASCII-based) string sorting. The position values use characters in the range `'0'` to `'z'` (ASCII 48-122).

**For SQLite and PostgreSQL:**

- ✅ Default text collation works correctly
- No additional configuration needed

**For MySQL/MariaDB (CRITICAL):**

- ⚠️ You **must** specify binary collation for the position column
- Default collations like `utf8mb4_unicode_ci` will sort incorrectly

```
// MySQL/MariaDB - REQUIRED for correct sorting
public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        // ... other fields ...
        $table->string('position')->charset('utf8mb4')->collation('utf8mb4_bin');
        // or use binary type for guaranteed ASCII sorting:
        // $table->binary('position', 255);
    });
}
```

**Why this matters:**

- Unicode collations (like `utf8mb4_unicode_ci`) apply language-specific sorting rules that don't preserve ASCII ordering
- Binary collation ensures positions sort in correct lexicographic order (e.g., `'U'` &lt; `'V'` &lt; `'X'` &lt; `'Z'`)
- Without this, drag-and-drop reordering may produce unexpected results on MySQL/MariaDB

Add `\AlexCrawford\Sortable\SortableTrait` to your Eloquent model.

```
class Article extends Model
{
    use \AlexCrawford\Sortable\SortableTrait;
}
```

if you want to use custom column name for position, set `$sortableField`:

```
class Article extends Model
{
    use \AlexCrawford\Sortable\SortableTrait;

    protected static $sortableField = 'somefield';
}
```

Now you can move your entities with methods `moveBefore($entity)` and `moveAfter($entity)` (you dont need to save model after that, it has saved already):

```
$entity = Article::find(1);

$positionEntity = Article::find(10)

$entity->moveAfter($positionEntity);

// if $positionEntity->position is aaa, then $entity->position is aab now
```

Also this trait automatically defines entity position on the `create` event, so you do not need to add `position` manually, just create entities as usual:

```
$article = new Article();
$article->title = $faker->sentence(2);
$article->description = $faker->paragraph();
$article->save();
```

This entity will be at position `entitiesMaximumPosition + 1`

To get ordered entities use the `sorted` scope:

```
$articles = Article::sorted()->get();
```

> \*\* Note \*\*: Resorting does not take place after a record is deleted. Gaps in positional values do not affect the ordering of your lists. However, if you prefer to prevent gaps you can reposition your models using the `deleting` event. Something like:

```
// YourAppServiceProvider

YourModel::deleting(function ($model) {
    $model->next()->decrement('position');
});
```

> You need alexcrawford-sortable &gt;=2.3 to use `->next()`

### Sortable groups

[](#sortable-groups)

if you want group entity ordering by field, add to your model

```
protected static $sortableGroupField = 'fieldName';
```

now moving and ordering will be encapsulated by this field.

If you want group entity ordering by many fields, use as an array:

```
protected static $sortableGroupField = ['fieldName1','fieldName2'];
```

### Sortable many to many

[](#sortable-many-to-many)

Let's assume your database structure is

```
posts
    id
    title

tags
    id
    title

post_tag
    post_id
    tag_id

```

and you want to order *tags* for each *post*

Add `position` column to the pivot table (you can use any name you want, but `position` is used by default)

```
post_tag
    post_id
    tag_id
    position (string with binary collation for MySQL/MariaDB)

```

**Migration example:**

```
Schema::create('post_tag', function (Blueprint $table) {
    $table->unsignedInteger('post_id');
    $table->unsignedInteger('tag_id');
    $table->string('position')->charset('utf8mb4')->collation('utf8mb4_bin'); // MySQL/MariaDB
    // or for SQLite/PostgreSQL: $table->string('position');

    $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
    $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
    $table->primary(['post_id', 'tag_id']);
});
```

⚠️ **Remember:** The `position` column must use binary collation on MySQL/MariaDB for correct sorting.

Add `\AlexCrawford\Sortable\BelongsToSortedManyTrait` to your `Post` model and define `belongsToSortedMany` relation provided by this trait:

```
class Post extends Model {

    use BelongsToSortedManyTrait;

    public function tags()
    {
        return $this->belongsToSortedMany('\App\Tag');
    }
}
```

> Note: `$this->belongsToSortedMany` has different signature then `$this->belongsToMany` -- the second argument for this method is `$orderColumn` (`'position'` by default), next arguments are the same

Attaching tags to post with `save`/`sync`/`attach` methods will set proper position

```
    $post->tags()->save($tag) // or
    $post->tags()->attach($tag->id) // or
    $post->tags()->sync([$tagId1, $tagId2, /* ...tagIds */])
```

Getting related model is sorted by position

```
$post->tags; // ordered by position by default
```

You can reorder tags for given post

```
    $post->tags()->moveBefore($entityToMove, $whereToMoveEntity); // or
    $post->tags()->moveAfter($entityToMove, $whereToMoveEntity);
```

Many to many demo:  ([code](https://github.com/boxfrommars/alexcrawford-sortable-demo5))

You can also use polymorphic many to many relation with sortable behavour by using the `MorphsToSortedManyTrait` trait and returning `$this->morphToSortedMany()` from relation method.

By following the Laravel polymorphic many to many table relation your tables should look like

```
posts
    id
    title

tags
    id
    title

taggables
    tag_id
    position (string with binary collation for MySQL/MariaDB)
    taggable_id
    taggable_type

```

**Migration example:**

```
Schema::create('taggables', function (Blueprint $table) {
    $table->unsignedInteger('tag_id');
    $table->string('position')->charset('utf8mb4')->collation('utf8mb4_bin'); // MySQL/MariaDB
    // or for SQLite/PostgreSQL: $table->string('position');
    $table->unsignedInteger('taggable_id');
    $table->string('taggable_type');

    $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
    $table->index(['taggable_id', 'taggable_type']);
});
```

⚠️ **Remember:** The `position` column must use binary collation on MySQL/MariaDB for correct sorting.

And your model like

```
class Post extends Model {

    use MorphToSortedManyTrait;

    public function tags()
    {
        return $this->morphToSortedMany('\App\Tag', 'taggable');
    }
}
```

Sortable Controller
-------------------

[](#sortable-controller)

Also this package provides `\AlexCrawford\Sortable\SortableController`, which handle requests to sort entities

### Usage

[](#usage-1)

Add the service provider to `config/app.php`

```
'providers' => array(
    // providers...

    'AlexCrawford\Sortable\SortableServiceProvider',
)
```

publish the config:

```
php artisan vendor:publish
```

Add models you need to sort in the config `config/sortable.php`:

```
'entities' => array(
     'articles' => '\App\Article', // entityNameForUseInRequest => ModelName
     // or
     'articles' => ['entity' => '\App\Article'],
     // or for many to many
     'posts' => [
        'entity' => '\App\Post',
        'relation' => 'tags' // relation name (method name which returns $this->belongsToSortedMany)
     ]
),
```

Add route to the `sort` method of the controller:

```
Route::post('sort', '\AlexCrawford\Sortable\SortableController@sort');
```

Now if you post to this route valid data:

```
$validator = \Validator::make(\Input::all(), array(
    'type' => array('required', 'in:moveAfter,moveBefore'), // type of move, moveAfter or moveBefore
    'entityName' => array('required', 'in:' . implode(',', array_keys($sortableEntities))), // entity name, 'articles' in this example
    'positionEntityId' => 'required|numeric', // id of relative entity
    'id' => 'required|numeric', // entity id
));

// or for many to many

$validator = \Validator::make(\Input::all(), array(
    'type' => array('required', 'in:moveAfter,moveBefore'), // type of move, moveAfter or moveBefore
    'entityName' => array('required', 'in:' . implode(',', array_keys($sortableEntities))), // entity name, 'articles' in this example
    'positionEntityId' => 'required|numeric', // id of relative entity
    'id' => 'required|numeric', // entity id
    'parentId' => 'required|numeric', // parent entity id
));
```

Then entity with `\Input::get('id')` id will be moved relative by entity with `\Input::get('positionEntityId')` id.

For example, if request data is:

```
type:moveAfter
entityName:articles
id:3
positionEntityId:14

```

then the article with id 3 will be moved after the article with id 14.

### jQuery UI sortable example

[](#jquery-ui-sortable-example)

> Note: Laravel 5 has csrf middleware enabled by default, so you should setup ajax requests:

Template

```

    @foreach ($articles as $article)

        {{{ $article->id }}}
        {{{ $article->title }}}

    @endforeach

```

Template for many to many ordering

```

    @foreach ($post->tags as $tag)

        {{ $tag->id }}
        {{ $tag->title }}

    @endforeach

```

```
    /**
     *
     * @param type string 'insertAfter' or 'insertBefore'
     * @param entityName
     * @param id
     * @param positionId
     */
    var changePosition = function(requestData){
        $.ajax({
            'url': '/sort',
            'type': 'POST',
            'data': requestData,
            'success': function(data) {
                if (data.success) {
                    console.log('Saved!');
                } else {
                    console.error(data.errors);
                }
            },
            'error': function(){
                console.error('Something wrong!');
            }
        });
    };

    $(document).ready(function(){
        var $sortableTable = $('.sortable');
        if ($sortableTable.length > 0) {
            $sortableTable.sortable({
                handle: '.sortable-handle',
                axis: 'y',
                update: function(a, b){

                    var entityName = $(this).data('entityname');
                    var $sorted = b.item;

                    var $previous = $sorted.prev();
                    var $next = $sorted.next();

                    if ($previous.length > 0) {
                        changePosition({
                            parentId: $sorted.data('parentid'),
                            type: 'moveAfter',
                            entityName: entityName,
                            id: $sorted.data('itemid'),
                            positionEntityId: $previous.data('itemid')
                        });
                    } else if ($next.length > 0) {
                        changePosition({
                            parentId: $sorted.data('parentid'),
                            type: 'moveBefore',
                            entityName: entityName,
                            id: $sorted.data('itemid'),
                            positionEntityId: $next.data('itemid')
                        });
                    } else {
                        console.error('Something wrong!');
                    }
                },
                cursor: "move"
            });
        }
    });
```

Development
-----------

[](#development)

### Setup

[](#setup)

Build the Docker image:

```
docker build -t alexcrawford-sortable .
```

### Running Tests

[](#running-tests)

```
docker run --volume $PWD:/project --rm --user $(id -u):$(id -g) alexcrawford-sortable vendor/bin/phpunit
```

### Code Quality Tools

[](#code-quality-tools)

This project includes modern PHP development tools:

**Code Formatting (Laravel Pint):**

```
vendor/bin/pint              # Check and fix code style
vendor/bin/pint --test       # Check only (don't modify)
```

**Static Analysis (PHPStan):**

```
vendor/bin/phpstan analyse   # Run type safety checks
```

**Code Coverage (Xdebug):**

```
docker run --volume $PWD:/project --rm --user $(id -u):$(id -g) alexcrawford-sortable vendor/bin/phpunit --coverage-text
```

Current code coverage:

- **Lines:** 84.43% (206/244)
- **Methods:** 76.19% (32/42)
- **Classes:** 55.56% (5/9)

**Running All Tests:**

```
docker run --volume $PWD:/project --rm --user $(id -u):$(id -g) alexcrawford-sortable bash -c "vendor/bin/phpunit && vendor/bin/phpstan analyse && vendor/bin/pint --test"
```

###  Health Score

63

—

FairBetter than 99% of packages

Maintenance99

Actively maintained with recent releases

Popularity33

Limited adoption so far

Community18

Small or concentrated contributor base

Maturity84

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 76% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~115 days

Recently: every ~575 days

Total

39

Last Release

3d ago

Major Versions

2.4.0 → 3.0.02015-12-19

3.5.0 → 4.02017-08-31

4.7.1 → 6.0.02019-09-16

6.0.0 → 7.0.02020-03-14

7.2.0 → 8.0.02025-10-18

PHP version history (6 changes)0.9PHP &gt;=5.4.0

3.4.0PHP &gt;=5.6.4

4.0PHP &gt;=7

4.7PHP &gt;=7.1.3

7.0.0PHP &gt;=7.2.5

8.0.0PHP &gt;=8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/22d88c45e26ab0ea01e2916177814fad067220a4d5dd1d28d223eca36f855ef2?d=identicon)[alexcrawford](/maintainers/alexcrawford)

---

Top Contributors

[![boxfrommars](https://avatars.githubusercontent.com/u/81930?v=4)](https://github.com/boxfrommars "boxfrommars (174 commits)")[![alexcrawford](https://avatars.githubusercontent.com/u/4070024?v=4)](https://github.com/alexcrawford "alexcrawford (16 commits)")[![vluzrmos](https://avatars.githubusercontent.com/u/450848?v=4)](https://github.com/vluzrmos "vluzrmos (7 commits)")[![giordanolima](https://avatars.githubusercontent.com/u/8314190?v=4)](https://github.com/giordanolima "giordanolima (5 commits)")[![mvrkljan](https://avatars.githubusercontent.com/u/694929?v=4)](https://github.com/mvrkljan "mvrkljan (4 commits)")[![Chrissi2812](https://avatars.githubusercontent.com/u/6141652?v=4)](https://github.com/Chrissi2812 "Chrissi2812 (4 commits)")[![timothyasp](https://avatars.githubusercontent.com/u/707699?v=4)](https://github.com/timothyasp "timothyasp (4 commits)")[![dath](https://avatars.githubusercontent.com/u/2124173?v=4)](https://github.com/dath "dath (3 commits)")[![peterbabic](https://avatars.githubusercontent.com/u/3952052?v=4)](https://github.com/peterbabic "peterbabic (2 commits)")[![lkrempler](https://avatars.githubusercontent.com/u/5972830?v=4)](https://github.com/lkrempler "lkrempler (2 commits)")[![scrutinizer-auto-fixer](https://avatars.githubusercontent.com/u/6253494?v=4)](https://github.com/scrutinizer-auto-fixer "scrutinizer-auto-fixer (1 commits)")[![SRWieZ](https://avatars.githubusercontent.com/u/1408020?v=4)](https://github.com/SRWieZ "SRWieZ (1 commits)")[![stevebauman](https://avatars.githubusercontent.com/u/6421846?v=4)](https://github.com/stevebauman "stevebauman (1 commits)")[![fabianmu](https://avatars.githubusercontent.com/u/89517?v=4)](https://github.com/fabianmu "fabianmu (1 commits)")[![tonystatic](https://avatars.githubusercontent.com/u/5255487?v=4)](https://github.com/tonystatic "tonystatic (1 commits)")[![christoph-kluge](https://avatars.githubusercontent.com/u/1446269?v=4)](https://github.com/christoph-kluge "christoph-kluge (1 commits)")[![YOzaz](https://avatars.githubusercontent.com/u/5859318?v=4)](https://github.com/YOzaz "YOzaz (1 commits)")[![ZAZmaster](https://avatars.githubusercontent.com/u/7642406?v=4)](https://github.com/ZAZmaster "ZAZmaster (1 commits)")

---

Tags

laraveleloquentsortablesortlaravel5laravel4sortingorderorderingorderable

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/alexcrawford-lexorank-sortable/health.svg)

```
[![Health](https://phpackages.com/badges/alexcrawford-lexorank-sortable/health.svg)](https://phpackages.com/packages/alexcrawford-lexorank-sortable)
```

###  Alternatives

[rutorika/sortable

Adds sortable behavior and ordering to Laravel Eloquent models. Grouping and many to many supported.

2891.1M17](/packages/rutorika-sortable)[spatie/eloquent-sortable

Sortable behaviour for eloquent models

1.5k25.7M319](/packages/spatie-eloquent-sortable)[jedrzej/pimpable

Laravel 4/5/6 package that allows to dynamically filter, sort and eager load relations for your models using request parameters

105188.2k1](/packages/jedrzej-pimpable)[jedrzej/sortable

Sortable trait for Laravel's Eloquent models - sort your models using request parameters

54279.0k1](/packages/jedrzej-sortable)[lemaur/eloquent-publishing

218.1k1](/packages/lemaur-eloquent-publishing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
