Ошибки панели администрирования или опыт разработки Laravel Orchid +5



В прошлом на Хабре публиковалась статья "Orchid CMS — ещё одна CMS на Laravel", а теперь спустя два года и больше 100 релизов попробуем разобрать ошибки и проблемы которые стояли на пути разработки.


1. Позиционирование


В самом заголовке предыдущей статьи и во многих других указывалась, аббревиатура CMS, что во влажных мечтах должно было привлечь больше интереса к разработке. Одновременно с этим, пакет не предлагал ни каких готовых решений в тиражировании веб-сайтов, а только управление в администрировании.


В итоге было только хуже… Само это сочетание отталкивало опытных пользователей устанавливать, а новички ожидали увидеть привычные для себя возможности, вместо, некоторой свободы.


2. Формы


Когда речь заходит об панелях администрирования, впервую очередь имеет значение формы и как с ними работать. Для примера будем рассматривать форму построенную на blade-шаблонах, которая отображает и позволяет редактировать данные некоторого объекта:


<form action="...">
   <input type="text" name="title">
   <input type="text" name="price">
   <input type="submit" value="Изменить">
</form>

Менеджеры, редакторы и пользователи в целом хотят видеть как можно больше об объекте редактирования и в этом нет ни каких проблем если всю информацию можно взять из одной таблицы, но когда информация очень растянута?


Самым популярным решением, что я видел, было просто разделение формы на несколько классов и файлов с представлением. Они отображались в виде вкладок на форме, информация в которых была бы сгруппирована по связям:


<form action="..." id="main">
   <input type="text" name="title">
   <input type="text" name="price">
   <input type="submit" value="Изменить">
</form>

<form action="..." id="editors">
   <!-- Связи с редакторами -->
   <input type="submit" value="Применить">
</form>

<form action="..."  id="history">
   <!-- История изменений -->
   <input type="submit" value="Восстановить">
</form>

Именно это и было воспроизведено… Конечно с небольшими изменениями и особенностями, но смысл оставался прежним. В создании отдельного класса группирующего все обработки в виде других форм:



Накладывая на предыдущие примеры, в коде каждая форма выглядела приблизительно следующим образом:


class Example extends Form
{
    public $name = 'General';

    public function rules(): array
    {
        return [
            'title' => 'required|max:160'
        ];
    }

    public function display(): View
    {
        return view('main');
    }

    public function persist(Model $model)
    {
        //..
        return $model->save();
    }
}

И формирование в контроллере всей группы:


class ExampleController extends Controller
{
    public function index(Model $model)
    {
        $form = new FormGroup([
            Example::class
            // editors..
            // history..
        ]);

        return $form->render($model);
    }
}

Кроме разделения самой формы и небольшой организации кода это не принесло ни каких других результатов. В итоге:


  • Такой способ ни как не помогал решать дублирование нескольких элементов, например, поле "Заголовок" у объекта мог встречаться множество раз, но полной копии формы ни разу.
  • При отличных от "стандартных" действий нужно было создавать отдельный метод контроллера, что напрочь портило весь смысл деления формы.
  • Ни каких плюсов в автоматизации построения представления, все так же необходимо указывать вручную.

3. CRUD


Самый спорный вариант, который очень часто фигурирует в новостных лентах и блогах разработчиков. Такой подход используется почти всеми альтернативными пакетами, сделали и его. По примеру одной известной системы где все храниться в одной таблице была создана модель с динамическим JSON полем:



В отдельном классе описывались необходимые для создания и редактирования поля, а так же базовые действия:


class Example extends Behavior
{
    public $name = 'Main';

    public function rules(): array
    {
        return [
            'title' => 'required|max:160'
        ];
    }

    public function fields(): array
    {
        return [
            Input::make('title')
                ->type('text')
                ->max(160)
                ->required(),
        ];
    }
}

Благодаря такому подходу, написание простейшего кода было быстрым из-за полного отсутствия работы с представлением, но принесло не мало проблем:


  • Нестандартные операции не доступны в рамках объекта.
  • Динамическое представление, вроде модальных окон с загрузкой информации недоступно.
  • При малейшем отклонении, например, построения графиков мы снова возвращаемся к написанию blade шаблонов со стилями и сценариями.
  • Невозможность использовать данные из отличных от базы данных источников.
  • Работа только в рамках одной таблицы, это можно было исправить, но в первой и последней реализации так было.

Исправление


Вариант с CRUD был основан на полном автоматизации представления, что в итоге привело к новой реализации. Как некоторый эталон на этот раз был взят продукт Visual Studio LightSwitch, который позволял пользоваться человеку без глубоких знаний разработки.


Естественно такого эффекта нам не нужно и это даже не является целью, при этом почти все технические возможности присутствовали в Laravel или пользовательских реализаций.
Решено было сосредоточится только на одно единственном аспекте — Экраны.



Экран — это все, что пользователь видит на странице и какие действия может совершать.
Все это описывается в одном классе, он не знает откуда берутся данные, это может быть: база данных, API или любые другие внешние источники. Построение внешнего вида основано на слоях и всё, что необходимо было сделать это лишь определить какие данные будут показаны в том или ином шаблоне.


Слой же может представлять из себя некоторый макет, который может быть таблицей, строкой, графиком и т.п. При этом каждый макет может включать в себя другой макет, то есть вложенность. Например экран делится двумя колонками, в левой поля для заполнения, справа справочная таблица и график и т.д.


Для управления над данными отображаемые на экране предусмотрены команды, которые берут на себя обработку.


class ExampleScreen extends Screen
{
    public $name = 'Example Screen';

    public function query(Model $model): array
    {
        return [
            'model' => $model
        ];
    }

    public function commandBar(): array
    {
        return [
            Link::make('Веб-сайт')
                ->link('http://orchid.software/ru')
                ->icon('icon-globe-alt'),
        ];
    }

    public function layout(): array
    {
        return [
            Layout::rows([
                Input::make('model.title')
                    ->type('text')
                    ->max(160)
                    ->required(),
            ])
        ];
    }
}

Остановка произошла на таком варианте, так как он держит здоровый баланс между полным написанием и автоматической генерации, при этом оставаясь понятным.




К сожалению, не доступен сервер mySQL