---
title: 'OctoberCMS и сортировка моделей'
url: 'https://graker.ru/news/2017/05/04/octobercms-sorting-models'
language: ru
type: post
author: 'Роман Грачев'
published: '2017-05-04T22:30:00+03:00'
updated: '2017-05-04T22:50:00+03:00'
categories:
  - OctoberCMS
  - Программирование
---

# OctoberCMS и сортировка моделей

Есть в OctoberCMS такая полезная штука — бихевиор для ручной сортировки моделей. Например, чтобы перетаскиванием отсортировать категории по значимости (и вложенности). Использовать его для своих моделей — очень просто, вот [страничка в документации](https://octobercms.com/docs/backend/reorder). Подключаем бихевиор к классу-контроллеру, заполняем конфиг, а в модели используем трейт `Sortable` (или `NestedTree`). А что такое бихевиор, можно тоже [прочитать в доках](https://octobercms.com/docs/services/behaviors).

Но проблема в том, что из коробки можно заставить этот бихевиор работать только на одном уровне. Вот есть все экземпляры модели одного класса — можно их отсортировать. А мне нужно сделать интерфейс для сортировки фотографий, принадлежащих альбомам. То есть отсортировать только фотокарточки внутри отдельно взятого альбома, без всех остальных.

Бихевиор `ReorderController` для этого использовать не получится, придется строить сортировку вручную. Однако если покопаться, как устроен `ReorderController`, его шаблоны и трейт `Sortable`, можно сделать свою сортировку по аналогии и даже не дублировать какую-то часть кода. Об этом и поговорим.

Начнем с моделей, которые нужно упорядочить. С ними всё просто: достаточно использовать трейт `Sortable`:

```
class Photo extends Model
{
  use \October\Rain\Database\Traits\Sortable;
  // ...
}
```

По умолчанию для сортировки будет использоваться поле `sort_order`, нужно его добавить к модели. Я это делал в уже используемом плагине, поэтому создал новую миграцию:

```
// ...
class AddSortOrder extends Migration
{
  public function up()
  {
    Schema::table('graker_photoalbums_photos', function($table)
    {
      $table->integer('sort_order')->unsigned()->nullable();
    });
  }
// ...
```

В коде трейта `Sortable` уже прописана инициализация поля `sort_order` для вновь созданных экземпляров моделей: поле будет заполняться значением id новой фотографии. Существующих моделей это, однако, не касается, их нужно разово проинициализировать вручную:

```
foreach (Photo::all() as $photo) {
  $photo->sort_order = $photo->id;
  $photo->save();
}
```

Код можно вставить в ту же самую миграцию.

Теперь сделаем сам интерфейс сортировки перетаскиванием объектов вверх-вниз. Для этого нам понадобится новая страничка в админке, которая делается через контроллер. В принципе, можно добавить новый метод в существующий контроллер альбомов, но я решил что для сортировки лучше сделать отдельный контроллер. С точки зрения реализации, разницы никакой, влияет только на путь к странице.

```
class Reorder extends Controller
{

  public $model = NULL;

  public function album($album_id = NULL) {
    $album = Album::find($album_id);
    if (!$album) {
      return '';
    }
    $this->model = $album;
    $this->addJs('/modules/backend/behaviors/reordercontroller/assets/js/october.reorder.js', 'core');
    $this->pageTitle = 'Reordering album ' . $album->title;
    return $this->makePartial('reorder', ['reorderRecords' => $this->model->photos,]);
  }

// ...
```

Контроллер `Reorder`, метод `album`, путь в админке соответственно будет `graker/photoalbums/reorder/album/<id>`. В методе мы загружаем объект альбома, фотографии которого будем сортировать. Затем добавляем скрипт для сортировки — тот же самый, что используется в бихевиоре `ReorderController`. Для порядка выставляем заголовок странички и затем генерируем html по шаблону `_reorder.html` (файл должен быть в `controllers/reorder/`). В шаблон передаем переменную `reorderRecords`, в ней — коллекция всех фотографий альбома (их мы и хотим отсортировать).

Посмотрим на значимую часть шаблона (отбросим генерацию хлебных крошек, ошибок и т.д.):

```
<!-- Reorder List -->
<?= Form::open() ?>
<div
        id="reorderTreeList"
        class="control-treelist"
        data-control="treelist"
        data-nested="0"
        data-handle="> li > .record > a.move"
        data-stripe-load-indicator>
            <?php if ($reorderRecords): ?>
            <ol id="reorderRecords">
                <?= $this->makePartial('records', ['records' => $reorderRecords]) ?>
            </ol>
            <?php else: ?>
            <p><?= Lang::get('backend::lang.reorder.no_records') ?></p>
        <?php endif ?>
</div>
<?= Form::close() ?>

<script>
    $.oc.reorderBehavior.initSorting('simple')
</script>
```

HTML взят из шаблона, используемого в `ReorderController` и лишь слегка упрощен: убрана часть, отвечающая за работу с вложенностью при сортировке, так как в данном случае у фотографий нет иерархии.

Здесь генерируется список объектов, которые мы будем сортировать, в конце вызывается инициализация JS-части. Сами фотографии в списке рендерятся по отдельному шаблону `_records.html`:

```
<?php foreach ($records as $record): ?>

<li data-record-id="<?= $record->getKey() ?>" data-record-sort-order="<?= $record->{$record->getSortOrderColumn()} ?>" >
    <div class="record">
        <a href="javascript:;" class="move"><img src="<?= $record->image->getThumb(100, 100, ['mode' => 'auto']); ?>"/></a>
        <span><?= $record->title ?></span>
        <input name="record_ids[]" type="hidden" value="<?= $record->getKey() ?>" />
    </div>
</li>

<?php endforeach ?>
```

Этот шаблон тоже сделан на базе шаблона из `ReorderController`, из него тоже убрана часть про вложенность. Кроме того, поскольку фотографии необязательно иметь заголовок, добавлена генерация картинки 100x100 для каждого объекта, чтобы хоть как-то их друг от друга отличать. Плюс картинка вставляется внутрь ссылки с классом `move`, так что перетаскивать объекты можно не только за иконку, но и за саму картинку, что удобно.

Наконец, для сохранения результатов сортировки нам нужен ajax-callback в контроллере, он должен называться `onReorder()`:

```
public function onReorder() {
  if (!$ids = post('record_ids')) return;
  if (!$orders = post('sort_orders')) return;
  $model = new Photo();
  $model->setSortableOrder($ids, $orders);
}
```

Метод работает так же как и коллбэк из `ReorderController` — просто сохраняет новые значения для сортировки моделей с помощью метода из трейта `Sortable`. Метод будет вызван аяксом после каждого перетаскивания, так что сохранять форму отдельно даже не надо.

Это, в общем-то, всё. Единственное что нужно не забыть — это сортировать при загрузке фотографии по полю `sort_order`. Это можно сделать прямо в настройках отношения в модели альбома:

```
public $hasMany = [
  'photos' => [
    'Graker\PhotoAlbums\Models\Photo',
    'order' => 'sort_order asc',
  ]
];
```

Выглядит новая страничка сортировки примерно вот так:

![Сортировка моделей в OctoberCMS](https://graker.ru/sites/default/files/styles/max_1300x1300/public/media/october_sorting.png.webp?itok=p-2Zurh_)

Увидеть код в работе можно в плагине [PhotoAlbums](https://github.com/graker/oc-photoalbums-plugin).
