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

Есть в OctoberCMS такая полезная штука — бихевиор для ручной сортировки моделей. Например, чтобы перетаскиванием отсортировать категории по значимости (и вложенности). Использовать его для своих моделей — очень просто, вот страничка в документации. Подключаем бихевиор к классу-контроллеру, заполняем конфиг, а в модели используем трейт Sortable (или NestedTree). А что такое бихевиор, можно тоже прочитать в доках.

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

Бихевиор 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

Увидеть код в работе можно в плагине PhotoAlbums.

Комментарии