Merge branch 'feature/tanach-seeder'

This commit is contained in:
Yisroel Baum 2026-08-15 23:03:05 +03:00
commit 8218de16f5
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
2 changed files with 433 additions and 0 deletions

View file

@ -0,0 +1,249 @@
<?php
namespace Database\Seeders;
use App\Element\CreateElementDto;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Set\CreateSetDto;
use App\Set\CreateSetLevelDto;
use App\Set\Set;
use App\Set\SetLevel;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
use App\Shared\ValueObject\EmailAddress;
use App\User\UserRepository;
use Illuminate\Database\Seeder;
use RuntimeException;
class TanachSeeder extends Seeder
{
private const string SET_NAME = 'Tanach';
private const array LEVEL_KINDS = [
'chelek',
'sefer',
'perek',
];
private const array SEFARIM_BY_CHELEK = [
'Torah' => [
'Bereishis' => 50,
'Shemos' => 40,
'Vayikra' => 27,
'Bamidbar' => 36,
'Devarim' => 34,
],
"Nevi'im" => [
'Yehoshua' => 24,
'Shoftim' => 21,
'Shmuel Alef' => 31,
'Shmuel Beis' => 24,
'Melachim Alef' => 22,
'Melachim Beis' => 25,
'Yeshayahu' => 66,
'Yirmiyahu' => 52,
'Yechezkel' => 48,
'Hoshea' => 14,
'Yoel' => 4,
'Amos' => 9,
'Ovadiah' => 1,
'Yonah' => 4,
'Michah' => 7,
'Nachum' => 3,
'Chavakuk' => 3,
'Tzefaniah' => 3,
'Chaggai' => 2,
'Zechariah' => 14,
'Malachi' => 3,
],
'Kesuvim' => [
'Tehillim' => 150,
'Mishlei' => 31,
'Iyov' => 42,
'Shir HaShirim' => 8,
'Rus' => 4,
'Eichah' => 5,
'Koheles' => 12,
'Esther' => 10,
'Daniel' => 12,
'Ezra' => 10,
'Nechemiah' => 13,
'Divrei HaYamim Alef' => 29,
'Divrei HaYamim Beis' => 36,
],
];
public function run(): void
{
$this->call(UserSeeder::class);
$user = app(UserRepository::class)->findByEmail(
new EmailAddress(UserSeeder::EMAIL),
);
if ($user === null) {
throw new RuntimeException('seeded user not found');
}
$setRepository = app(SetRepository::class);
$tanach = $this->findTanach($setRepository);
if ($tanach === null) {
$tanach = $setRepository->create(new CreateSetDto(
name: self::SET_NAME,
creator: $user,
));
}
$levels = $this->seedLevels(
repository: app(SetLevelRepository::class),
tanach: $tanach,
);
$this->seedElements(
repository: app(ElementRepository::class),
tanach: $tanach,
levels: $levels,
);
}
private function findTanach(SetRepository $repository): ?Set
{
foreach ($repository->all() as $set) {
if ($set->getName() === self::SET_NAME) {
return $set;
}
}
return null;
}
/**
* @return array{
* chelek: SetLevel,
* sefer: SetLevel,
* perek: SetLevel
* }
*/
private function seedLevels(
SetLevelRepository $repository,
Set $tanach,
): array {
$levels = $repository->findBySet($tanach);
foreach ($levels as $depth => $level) {
$expectedKind = self::LEVEL_KINDS[$depth] ?? null;
if ($level->getKind() !== $expectedKind) {
throw new RuntimeException(
'Tanach levels do not match the expected hierarchy',
);
}
}
$missingKinds = array_slice(self::LEVEL_KINDS, count($levels));
foreach ($missingKinds as $kind) {
$levels[] = $repository->create(new CreateSetLevelDto(
set: $tanach,
kind: $kind,
));
}
return [
'chelek' => $this->findLevel($levels, 'chelek'),
'sefer' => $this->findLevel($levels, 'sefer'),
'perek' => $this->findLevel($levels, 'perek'),
];
}
/**
* @param list<SetLevel> $levels
*/
private function findLevel(array $levels, string $kind): SetLevel
{
foreach ($levels as $level) {
if ($level->getKind() === $kind) {
return $level;
}
}
throw new RuntimeException("Tanach {$kind} level not found");
}
/**
* @param array{
* chelek: SetLevel,
* sefer: SetLevel,
* perek: SetLevel
* } $levels
*/
private function seedElements(
ElementRepository $repository,
Set $tanach,
array $levels,
): void {
$chelakim = $repository->findTopLevelBySet($tanach);
foreach (self::SEFARIM_BY_CHELEK as $chelekName => $sefarim) {
$chelek = $this->findOrCreateElement(
repository: $repository,
siblings: $chelakim,
name: $chelekName,
level: $levels['chelek'],
parentElement: null,
);
$seededSefarim = $repository->findByParentElement($chelek);
foreach ($sefarim as $seferName => $perekCount) {
$sefer = $this->findOrCreateElement(
repository: $repository,
siblings: $seededSefarim,
name: $seferName,
level: $levels['sefer'],
parentElement: $chelek,
);
$perakim = $repository->findByParentElement($sefer);
for (
$perekNumber = 1;
$perekNumber <= $perekCount;
$perekNumber++
) {
$this->findOrCreateElement(
repository: $repository,
siblings: $perakim,
name: "Perek {$perekNumber}",
level: $levels['perek'],
parentElement: $sefer,
);
}
}
}
}
/**
* @param list<Element> $siblings
*/
private function findOrCreateElement(
ElementRepository $repository,
array &$siblings,
string $name,
SetLevel $level,
?Element $parentElement,
): Element {
foreach ($siblings as $sibling) {
if (
$sibling->getName() === $name
&& $sibling->getLevel()->getId() === $level->getId()
) {
return $sibling;
}
}
$element = $repository->create(new CreateElementDto(
name: $name,
level: $level,
parentElement: $parentElement,
));
$siblings[] = $element;
return $element;
}
}

View file

@ -0,0 +1,184 @@
<?php
namespace Tests\Feature\Database;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Set\Set;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
use App\Shared\ValueObject\EmailAddress;
use App\User\UserRepository;
use Database\Seeders\TanachSeeder;
use Database\Seeders\UserSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TanachSeederTest extends TestCase
{
use RefreshDatabase;
private const array SEFARIM_BY_CHELEK = [
'Torah' => [
'Bereishis' => 50,
'Shemos' => 40,
'Vayikra' => 27,
'Bamidbar' => 36,
'Devarim' => 34,
],
"Nevi'im" => [
'Yehoshua' => 24,
'Shoftim' => 21,
'Shmuel Alef' => 31,
'Shmuel Beis' => 24,
'Melachim Alef' => 22,
'Melachim Beis' => 25,
'Yeshayahu' => 66,
'Yirmiyahu' => 52,
'Yechezkel' => 48,
'Hoshea' => 14,
'Yoel' => 4,
'Amos' => 9,
'Ovadiah' => 1,
'Yonah' => 4,
'Michah' => 7,
'Nachum' => 3,
'Chavakuk' => 3,
'Tzefaniah' => 3,
'Chaggai' => 2,
'Zechariah' => 14,
'Malachi' => 3,
],
'Kesuvim' => [
'Tehillim' => 150,
'Mishlei' => 31,
'Iyov' => 42,
'Shir HaShirim' => 8,
'Rus' => 4,
'Eichah' => 5,
'Koheles' => 12,
'Esther' => 10,
'Daniel' => 12,
'Ezra' => 10,
'Nechemiah' => 13,
'Divrei HaYamim Alef' => 29,
'Divrei HaYamim Beis' => 36,
],
];
public function test_it_seeds_the_complete_tanach_idempotently(): void
{
$this->seed(TanachSeeder::class);
$this->seed(TanachSeeder::class);
$this->assertDatabaseCount('users', 1);
$this->assertDatabaseCount('sets', 1);
$this->assertDatabaseCount('set_levels', 3);
$this->assertDatabaseCount('elements', 971);
$user = app(UserRepository::class)->findByEmail(
new EmailAddress(UserSeeder::EMAIL),
);
$this->assertNotNull($user);
$tanach = $this->findTanach();
$this->assertSame(
UserSeeder::EMAIL,
$tanach->getCreator()->getEmail()->value(),
);
$levels = app(SetLevelRepository::class)->findBySet($tanach);
$this->assertSame(
['chelek', 'sefer', 'perek'],
array_map(function ($level): string {
return $level->getKind();
}, $levels),
);
$elementRepository = app(ElementRepository::class);
$chelakim = $elementRepository->findTopLevelBySet($tanach);
$this->assertElements(
elements: $chelakim,
expectedNames: array_keys(self::SEFARIM_BY_CHELEK),
expectedKind: 'chelek',
);
$seferCount = 0;
$perekCount = 0;
foreach ($chelakim as $chelek) {
$expectedSefarim = self::SEFARIM_BY_CHELEK[
$chelek->getName()
] ?? null;
$this->assertNotNull($expectedSefarim);
$sefarim = $elementRepository->findByParentElement($chelek);
$this->assertElements(
elements: $sefarim,
expectedNames: array_keys($expectedSefarim),
expectedKind: 'sefer',
);
$seferCount += count($sefarim);
foreach ($sefarim as $sefer) {
$expectedPerekCount = $expectedSefarim[
$sefer->getName()
] ?? null;
$this->assertNotNull($expectedPerekCount);
$expectedPerekNames = [];
for (
$perekNumber = 1;
$perekNumber <= $expectedPerekCount;
$perekNumber++
) {
$expectedPerekNames[] = "Perek {$perekNumber}";
}
$perakim = $elementRepository->findByParentElement($sefer);
$this->assertElements(
elements: $perakim,
expectedNames: $expectedPerekNames,
expectedKind: 'perek',
);
$perekCount += count($perakim);
}
}
$this->assertSame(39, $seferCount);
$this->assertSame(929, $perekCount);
}
private function findTanach(): Set
{
foreach (app(SetRepository::class)->all() as $set) {
if ($set->getName() === 'Tanach') {
return $set;
}
}
$this->fail('Tanach set was not seeded');
}
/**
* @param list<Element> $elements
* @param list<string> $expectedNames
*/
private function assertElements(
array $elements,
array $expectedNames,
string $expectedKind,
): void {
$this->assertSame(
$expectedNames,
array_map(function (Element $element): string {
return $element->getName();
}, $elements),
);
foreach ($elements as $index => $element) {
$this->assertSame($expectedKind, $element->getKind());
$this->assertSame($index + 1, $element->getPosition());
}
}
}