Laravel Eloquent: строим дерево категорий в формате json

Laravel Eloquent: строим дерево категорий в формате json

Часто у интернет-магазинов товары сгруппированы по категориям и подкатегориям, при этом количество подкатегорий и их вложенность может быть неограниченной. В этой статье я расскажу вам, как можно с помощью Eloquent передать в API вложенность категорий для быстрого отображения дерева.

Для начала создадим миграцию, для примера будем использовать простую модель из двух полей - имя и описание:

class CreateListingCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('listing_categories', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->longText('description')->nullable();
            $table->integer('parent_id')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }
}

Для фиксации вложенности мы используем поле parent_id, где родительская категория будет иметь значение NULL, а дочерняя категория будет ссылаться на id родительской. На картинке к данному посту вы можете увидеть пример введённых данных с двумя родительскими категориями и трёхступенчатой вложенностью.

Далее нам понадобится создать модель app/Models/ListingCategory.php

final class ListingCategory extends \Parents\Models\Model
{

    public const RESOURCE_NAME = 'listing_categories';

    public const DOMAIN_NAME = 'Listings';

    public $table = 'listing_categories';

    protected $dates = [
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    protected $fillable = [
        'name',
        'description',
        'parent_id',
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    protected function serializeDate(DateTimeInterface $date): string
    {
        return $date->format('Y-m-d H:i:s');
    }

    /**
     * Get the index name for the model.
     */
    public function childs(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(__CLASS__, 'parent_id', 'id') ;
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     * @psalm-suppress InvalidReturnStatement
     * @psalm-suppress InvalidReturnType
     */
    public function childrenCategories(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(__CLASS__, 'parent_id', 'id')->with('childs');
    }
}

 

Здесь мы видем два метода - childs, через который мы можем получить дочернюю категорию и childrenCategories - где мы рекурсивно получаем все дочерние связанные записи.

Form Request для создания категории у нас будет выглядеть так:

 

final class StoreListingCategoryRequest extends Request
{
    public function authorize(): bool
    {
        return $this->check('listing_category_create');
    }

    public function rules(): array
    {
        $rules = [
            'data.attributes.name' => [
                'string',
                'min:3',
                'max:190',
                'nullable',
            ],
            'data.attributes.description' => [
                'string',
                'nullable',
            ],
            'data.attributes.parent_id' => [
                'integer',
                'min:-2147483648',
                'max:2147483647',
                'nullable',
                'exists:Domains\Listings\Models\ListingCategory,id'
            ]
        ];
        return $this->mergeWithDefaultRules($rules);
    }
}

 

Стоит отметить, что FormRequest в данном примере настроен на работу по стандарту Json:Api.

Теперь создадим точку входа для получения дерева категорий. Для этого в файле api.php вставляем следующее:

 

Route::get('listing-categories/tree', [
    App\Http\Controllers\Api\V1\ListingCategoriesApiController::class,
    'tree'
])->name('listing-categories.tree');

 

В контроллере в методе tree нам потребуется получить список категорий и передать их в трансформер. Для отображения API вы используем библиотеку fractal:

 

    public function tree(IndexListingCategoriesRequest $request, GetCategoryTreeAction $action): ?JsonResponse
    {
        return fractal(
            $action(),
            new TreeCategoryTransformer(),
            new \League\Fractal\Serializer\JsonApiSerializer($this->getUrl())
        )->withResourceName(ListingCategory::RESOURCE_NAME)
            ->respondJsonApi();
    }

 

В качестве примера класса $action вы можете передать перечень категорий через репозиторий следующим образом:

 

ListingCategory::whereNull('parent_id')

            ->with('childrenCategories')

            ->get();

 

Трансформер в данном случае у нас принимает следующий вид:

 

final class TreeCategoryTransformer extends \Parents\Transformers\Transformer
{
    public function transform(ListingCategory $category): array
    {
        if ($category->childrenCategories) {
            $item = [
                'id' => $category->id,
                'name' => $category->name,
                'description' => $category->description,
                'parent_id' => $category->parent_id,
            ];
            /** @var ListingCategory $value */
            foreach ($category->childrenCategories as $value) {
                $item['children'][] = $this->transform($value);
            }

            return $item;
        }

        return [
            'id' => $category->id,
            'name' => $category->name,
            'description' => $category->description,
            'parent_id' => $category->parent_id,
        ];
    }
}

В конечном итоге после обращения к точке входа по пути api/v1/listing-categories/tree мы получим следующую структуру в формате json:

 

{
  "data": [
    {
      "type": "listing_categories",
      "id": "1",
      "attributes": {
        "name": "Equipment",
        "description": "Super equipment",
        "parent_id": null
      },
      "links": {
        "self": "http:\/\/delta.test:8000\/listing_categories\/1"
      }
    },
    {
      "type": "listing_categories",
      "id": "2",
      "attributes": {
        "name": "Electronics",
        "description": "Super electro products",
        "parent_id": null,
        "children": [
          {
            "id": 3,
            "name": "Computers",
            "description": "Super computers and pcs",
            "parent_id": 2,
            "children": [
              {
                "id": 4,
                "name": "Computer Accessories",
                "description": "Keyboard, mouses etc",
                "parent_id": 3
              },
              {
                "id": 5,
                "name": "Computer Equipment",
                "description": "equipment for computers",
                "parent_id": 3
              }
            ]
          },
          {
            "id": 6,
            "name": "Mobile",
            "description": "Mobile phones",
            "parent_id": 2
          }
        ]
      },
      "links": {
        "self": "http:\/\/delta.test:8000\/listing_categories\/2"
      }
    }
  ]
}

 

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

Я программист. В душе я предприниматель. Я начал зарабатывать деньги с 11 лет, в суровые 90-е годы, сдавая стеклотару в местный магазин и обменивая её на сладости. Я зарабатывал столько, что хватало на разные вкусняшки.

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js, Wordpress. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235