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