- Created a new JSON file for the City entity's AST representation, detailing its methods and properties. - Added a migration to alter the cities table by adding a nullable title column.
5.6 KiB
Category Import / Export API
Prefix:
/api/v1/admin/categories/{bundle}
Bulk JSON export and import for the admin «دستهبندیها» page (CategoriesPage),
backed by App\Category\Controller\CategoryImportController.
{bundle} is one of: provinces, cities, specialties, doctor_services,
insurances, tags.
Permission: ROLE_ADMIN (whole controller).
Each bundle maps to one table:
| bundle | table | has slug |
has weight |
references |
|---|---|---|---|---|
provinces |
provinces |
❌ | ✅ | — |
cities |
cities |
❌ | ✅ | province_id → provinces, representation_id → representations |
specialties |
specialties |
✅ | ✅ | parent_id → specialties (same file) |
doctor_services |
doctor_services |
✅ | ✅ | specialty_id → specialties |
insurances |
insurances |
❌ | ❌ | type ∈ {basic,supplementary} |
tags |
tags |
✅ | ❌ | — |
GET /api/v1/admin/categories/{bundle}/export
Complete export of a bundle — every row, no pagination cap. Returns the raw table columns (the same keys an import expects), so export → import is lossless.
⚠️ Do not use the paginated admin list endpoints (e.g.
/api/v1/admin/specialties?limit=9999) as an export source for import: those cap at 100 rows, and feeding a truncated file into the wipe+replace import below deletes every row beyond the first 100.
Response 200
{
"success": true,
"data": [
{ "id": 1, "uuid": "…", "name": "قلب", "slug": "ghalb", "status": 1, "weight": 0, "parent_id": null }
]
}
For cities the export rows carry every column plus province_name, and
social_media is returned as a JSON object — the exact shape
nobat724_front/data/city.json uses, so the export file can be dropped into the
frontend (and re-imported) without any transformation.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not admin |
ERR_BUNDLE_UNKNOWN |
404 | Unknown bundle |
POST /api/v1/admin/categories/{bundle}/import
Wipe + full replace of the bundle's table from the uploaded JSON, inside a
single transaction (FK checks disabled during the swap). Raw id values from the
file are preserved so relations stay intact.
Validation is strict and two-phase: the whole file is validated first; if
any row is invalid, nothing is written and every error is returned. Unknown
extra keys (e.g. province_name produced by export) are ignored.
Request Body (application/json)
A non-empty array of records, or { "items": [ … ] }. Max 5000 rows.
[
{ "id": 1, "name": "قلب", "slug": "ghalb", "status": 1, "weight": 0, "parent_id": null },
{ "id": 2, "name": "آریتمی", "slug": "arr", "status": 1, "weight": 1, "parent_id": 1 }
]
Per-row validation rules
| Field | Rule |
|---|---|
id |
required, positive integer, unique within the file |
name |
required, non-empty string ≤ 255 |
status |
0 or 1 (default 1) |
weight |
integer ≥ 0 — bundles with a weight column only |
slug |
required, ≤ 255, unique within file — specialties/doctor_services/tags |
type |
basic or supplementary — insurances only |
uuid |
optional; generated if missing |
parent_id |
specialties: null, or an id present in the same file |
specialty_id |
doctor_services: null, or an existing specialties.id |
province_id / representation_id |
cities: null, or an existing id in the referenced table |
title, site_name, slogan, domain, keywords, description, footer_description, contact_phone, email, logo_url |
cities: optional SEO/branding strings (title = meta title of the city website) |
social_media |
cities: optional; JSON object (e.g. {"instagram": "…"}) or its string form — stored as JSON |
Response 200
{ "success": true, "data": { "imported": 128 } }
Response 422 (validation failed — nothing written)
{
"success": false,
"data": null,
"errors": [
{ "row": 2, "field": "parent_id", "message": "ردیف 2: والد با شناسه 999 در همین فایل وجود ندارد" }
]
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not admin |
ERR_BUNDLE_UNKNOWN |
404 | Unknown bundle |
ERR_IMPORT_FORMAT |
422 | Body is not a non-empty array of records |
ERR_IMPORT_TOO_LARGE |
422 | More than 5000 rows |
| (field errors) | 422 | One entry per failed row/field; no rows written |
ERR_IMPORT_FAILED |
500 | DB error during replace (transaction rolled back) |
Seeding a from-scratch database
Canonical category data ships in the repo at data/seed/*.json
(provinces.json, cities.json, specialties.json, doctor_services.json).
After creating an empty schema, populate it with:
ddev exec php bin/console app:seed-categories
The command (App\Category\Command\SeedCategoriesCommand) reads those files and
runs the same strict validation + wipe-and-replace as the HTTP import, in
dependency order (provinces → cities, specialties → doctor_services). The shared
logic lives in App\Category\Service\CategoryImporter.
Full rebuild:
ddev exec php bin/console doctrine:database:drop --force
ddev exec php bin/console doctrine:database:create
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console app:seed-categories
ddev exec php bin/console app:create-admin 09100000001 'Test@1234'