feat: add CategoryImporter service and SeedCategoriesCommand for seeding category data

- Implemented CategoryImporter service to handle bulk export/import logic for categories.
- Created SeedCategoriesCommand to seed category tables from JSON files in data/seed/.
- Added validation and normalization for category data during import.
- Ensured proper error handling and user feedback during the seeding process.
This commit is contained in:
hamed
2026-06-30 21:58:44 +03:30
parent 22937dfa56
commit 9eb5a03258
22 changed files with 5236 additions and 1032 deletions
@@ -0,0 +1,69 @@
<?php
namespace App\Category\Command;
use App\Category\Service\CategoryImporter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Seeds the category tables from the JSON files in data/seed/.
*
* The data ships inside this repo (data/seed/) so a from-scratch database can be
* populated with the canonical, valid categories without any external source.
* Bundles are seeded in dependency order (provinces → cities, specialties →
* doctor_services) and each is a strict wipe+replace via {@see CategoryImporter}.
*
* ddev exec php bin/console app:seed-categories
*/
#[AsCommand(name: 'app:seed-categories', description: 'Seed category tables (provinces, cities, specialties, doctor services) from data/seed/*.json')]
class SeedCategoriesCommand extends Command
{
/** seed order matters: referenced tables first */
private const ORDER = ['provinces', 'cities', 'specialties', 'doctor_services'];
public function __construct(
private readonly CategoryImporter $importer,
private readonly string $projectDir,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$seedDir = $this->projectDir . '/data/seed';
foreach (self::ORDER as $bundle) {
$file = $seedDir . '/' . $bundle . '.json';
if (!is_file($file)) {
$io->error("فایل seed یافت نشد: {$file}");
return Command::FAILURE;
}
$payload = json_decode((string) file_get_contents($file), true);
if (!is_array($payload) || $payload === [] || array_keys($payload) !== range(0, count($payload) - 1)) {
$io->error("{$bundle}: فایل باید یک آرایه‌ی غیرخالی باشد");
return Command::FAILURE;
}
$normalized = [];
$errors = $this->importer->validate($bundle, $payload, $normalized);
if ($errors !== []) {
$io->error("{$bundle}: اعتبارسنجی ناموفق — " . count($errors) . ' خطا');
foreach (array_slice($errors, 0, 10) as $e) {
$io->writeln("{$e['message']}");
}
return Command::FAILURE;
}
$this->importer->replace($bundle, $normalized);
$io->success(sprintf('%s: %d رکورد seed شد', $bundle, count($normalized)));
}
return Command::SUCCESS;
}
}