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

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

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

Часто у интернет-магазинов товары сгруппированы по категориям и подкатегориям, при этом количество подкатегорий и их вложенность может быть неограниченной. В этой статье я расскажу вам, как можно с помощью 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"

      }

    }

  ]

}