Skip to content

Commit 38cd63d

Browse files
feat: add seeders with CLI commands, path resolver, and tests
1 parent 38a8fd5 commit 38cd63d

13 files changed

Lines changed: 596 additions & 4 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
All significant changes to this project will be documented in this file.
44

5+
## [1.5.0] - 2025-12-21
6+
7+
### Added
8+
9+
- **Seeders:** New seeding system with `Seeder`, `SeederRunner`, and `SeedPathResolver`.
10+
- **CLI seed commands:** `seed` and `make:seed` with configurable default path (`./database/seeds`).
11+
- **Seeder tests:** Coverage for runner behavior, path resolver, and CLI commands.
12+
- **Docs:** README updated with seeder usage and configuration examples.
13+
14+
### Changed
15+
16+
- CLI seed command output now uses stdout consistently for testability.
17+
518
## [1.4.3] - 2025-12-10
619

720
### Fixed

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ The package includes a migration system (designed to be used via the CLI).
291291
- `migrate:rollback`
292292
- `migrate:status`
293293
- `make:migration`
294+
- `seed`
295+
- `make:seed`
294296

295297
### Example migration
296298

@@ -314,6 +316,26 @@ return new class extends Migration {
314316
};
315317
```
316318

319+
## Seeders
320+
321+
The package includes a lightweight seeding system (via the CLI).
322+
323+
### Example seeder
324+
325+
```php
326+
use Codemonster\Database\Seeders\Seeder;
327+
328+
return new class extends Seeder {
329+
public function run(): void
330+
{
331+
db()->table('users')->insert([
332+
'name' => 'Admin',
333+
'email' => 'admin@example.com',
334+
]);
335+
}
336+
};
337+
```
338+
317339
## ORM (ActiveRecord / Eloquent‑style)
318340

319341
**Since 1.3.0**, the package includes a complete ORM layer:
@@ -469,6 +491,33 @@ You can override paths via the migration kernel/path resolver:
469491
$kernel->getPathResolver()->addPath('/path/to/migrations');
470492
```
471493

494+
### Running seeders
495+
496+
```bash
497+
vendor/bin/database seed
498+
```
499+
500+
### Create a seeder
501+
502+
```bash
503+
vendor/bin/database make:seed UsersSeeder
504+
```
505+
506+
Seed names must be CamelCase using only Latin letters (e.g., `UsersSeeder`). Names that include other symbols or casing styles are rejected.
507+
508+
Default seeds directory:
509+
510+
```text
511+
./database/seeds
512+
```
513+
514+
You can override paths via the seed kernel/path resolver:
515+
516+
```php
517+
$kernel->getSeedPathResolver()->addPath('/path/to/seeds');
518+
```
519+
520+
472521
## Tests
473522

474523
```bash
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Codemonster\Database\CLI\Commands;
4+
5+
use Codemonster\Database\CLI\CommandInterface;
6+
use Codemonster\Database\Seeders\SeedPathResolver;
7+
8+
class MakeSeedCommand implements CommandInterface
9+
{
10+
protected SeedPathResolver $paths;
11+
12+
public function __construct(SeedPathResolver $paths)
13+
{
14+
$this->paths = $paths;
15+
}
16+
17+
public function signature(): string
18+
{
19+
return 'make:seed';
20+
}
21+
22+
public function description(): string
23+
{
24+
return 'Create a new seed file';
25+
}
26+
27+
public function handle(array $arguments): int
28+
{
29+
$name = $arguments[0] ?? null;
30+
31+
if (!$name) {
32+
echo "Seed name is required.\n";
33+
echo "Usage: make:seed UsersSeeder\n";
34+
35+
return 1;
36+
}
37+
38+
if (!$this->isValidName($name)) {
39+
echo "Seed name must be CamelCase, Latin letters only. Example: UsersSeeder\n";
40+
41+
return 1;
42+
}
43+
44+
$path = $this->detectPath();
45+
46+
if (!$path) {
47+
echo "No seeds path configured.\n";
48+
49+
return 1;
50+
}
51+
52+
if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) {
53+
echo "Cannot create seeds directory: {$path}\n";
54+
55+
return 1;
56+
}
57+
58+
$filename = $this->buildFileName($name);
59+
$fullPath = $path . DIRECTORY_SEPARATOR . $filename;
60+
61+
if (file_exists($fullPath)) {
62+
echo "Seed file already exists: {$fullPath}\n";
63+
64+
return 1;
65+
}
66+
67+
file_put_contents($fullPath, $this->stub());
68+
69+
echo "Created seed: {$fullPath}\n";
70+
71+
return 0;
72+
}
73+
74+
protected function detectPath(): ?string
75+
{
76+
$paths = $this->paths->getPaths();
77+
78+
if (!empty($paths)) {
79+
return $paths[0];
80+
}
81+
82+
return null;
83+
}
84+
85+
protected function isValidName(string $name): bool
86+
{
87+
return (bool) preg_match('/^[A-Z][a-z]*(?:[A-Z][a-z]*)*$/', $name);
88+
}
89+
90+
protected function buildFileName(string $name): string
91+
{
92+
$now = new \DateTimeImmutable('now');
93+
$timestamp = $now->format('Y_m_d_His');
94+
$slug = preg_replace('/(?<!^)([A-Z])/', '_$1', $name);
95+
$slug = preg_replace('/[^A-Za-z0-9]+/', '_', $slug);
96+
$slug = trim($slug, '_');
97+
$slug = strtolower($slug);
98+
99+
return $timestamp . '_' . $slug . '.php';
100+
}
101+
102+
protected function stub(): string
103+
{
104+
return <<<PHP
105+
<?php
106+
107+
use Codemonster\\Database\\Seeders\\Seeder;
108+
109+
return new class extends Seeder {
110+
public function run(): void
111+
{
112+
// Example:
113+
// db()->table('users')->insert([
114+
// 'name' => 'Admin',
115+
// 'email' => 'admin@example.com',
116+
// ]);
117+
}
118+
};
119+
120+
PHP;
121+
}
122+
}

src/CLI/Commands/SeedCommand.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Codemonster\Database\CLI\Commands;
4+
5+
use Codemonster\Database\CLI\CommandInterface;
6+
use Codemonster\Database\Seeders\SeederRunner;
7+
8+
class SeedCommand implements CommandInterface
9+
{
10+
protected SeederRunner $seeder;
11+
12+
public function __construct(SeederRunner $seeder)
13+
{
14+
$this->seeder = $seeder;
15+
}
16+
17+
public function signature(): string
18+
{
19+
return 'seed';
20+
}
21+
22+
public function description(): string
23+
{
24+
return 'Run all seeders';
25+
}
26+
27+
public function handle(array $arguments): int
28+
{
29+
$executed = $this->seeder->seed();
30+
31+
if (empty($executed)) {
32+
echo "Nothing to seed.\n";
33+
34+
return 0;
35+
}
36+
37+
foreach ($executed as $name) {
38+
echo "Seeded: {$name}\n";
39+
}
40+
41+
return 0;
42+
}
43+
}

src/CLI/DatabaseCLIKernel.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
use Codemonster\Database\Migrations\MigrationPathResolver;
77
use Codemonster\Database\Migrations\MigrationRepository;
88
use Codemonster\Database\Migrations\Migrator;
9+
use Codemonster\Database\Seeders\SeedPathResolver;
10+
use Codemonster\Database\Seeders\SeederRunner;
911
use Codemonster\Database\CLI\Commands\MigrateCommand;
1012
use Codemonster\Database\CLI\Commands\RollbackCommand;
1113
use Codemonster\Database\CLI\Commands\StatusCommand;
1214
use Codemonster\Database\CLI\Commands\MakeMigrationCommand;
15+
use Codemonster\Database\CLI\Commands\SeedCommand;
16+
use Codemonster\Database\CLI\Commands\MakeSeedCommand;
1317

1418
class DatabaseCLIKernel
1519
{
@@ -19,17 +23,31 @@ class DatabaseCLIKernel
1923

2024
protected Migrator $migrator;
2125

22-
public function __construct(ConnectionInterface $connection, ?MigrationPathResolver $paths = null)
23-
{
26+
protected SeedPathResolver $seedPaths;
27+
28+
protected SeederRunner $seeder;
29+
30+
public function __construct(
31+
ConnectionInterface $connection,
32+
?MigrationPathResolver $paths = null,
33+
?SeedPathResolver $seedPaths = null
34+
) {
2435
$this->paths = $paths ?? new MigrationPathResolver();
2536

2637
if (empty($this->paths->getPaths())) {
2738
$this->paths->addPath(getcwd() . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations');
2839
}
2940

41+
$this->seedPaths = $seedPaths ?? new SeedPathResolver();
42+
43+
if (empty($this->seedPaths->getPaths())) {
44+
$this->seedPaths->addPath(getcwd() . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'seeds');
45+
}
46+
3047
$repository = new MigrationRepository($connection);
3148

3249
$this->migrator = new Migrator($repository, $connection, $this->paths);
50+
$this->seeder = new SeederRunner($connection, $this->seedPaths);
3351
$this->commands = new CommandRegistry();
3452

3553
$this->registerDefaultCommands();
@@ -41,6 +59,8 @@ protected function registerDefaultCommands(): void
4159
$this->commands->register(new RollbackCommand($this->migrator));
4260
$this->commands->register(new StatusCommand($this->migrator));
4361
$this->commands->register(new MakeMigrationCommand($this->paths));
62+
$this->commands->register(new SeedCommand($this->seeder));
63+
$this->commands->register(new MakeSeedCommand($this->seedPaths));
4464
}
4565

4666
public function handle(array $argv): int
@@ -58,8 +78,18 @@ public function getPathResolver(): MigrationPathResolver
5878
return $this->paths;
5979
}
6080

81+
public function getSeedPathResolver(): SeedPathResolver
82+
{
83+
return $this->seedPaths;
84+
}
85+
6186
public function getMigrator(): Migrator
6287
{
6388
return $this->migrator;
6489
}
90+
91+
public function getSeeder(): SeederRunner
92+
{
93+
return $this->seeder;
94+
}
6595
}

src/Seeders/SeedPathResolver.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Codemonster\Database\Seeders;
4+
5+
class SeedPathResolver
6+
{
7+
/**
8+
* @var string[]
9+
*/
10+
protected array $paths = [];
11+
12+
/**
13+
* Add seeders path (only if directory exists).
14+
*/
15+
public function addPath(string $path): void
16+
{
17+
// Allow registering paths even before the directory exists; it can be created later by CLI
18+
$this->paths[] = rtrim($path, DIRECTORY_SEPARATOR);
19+
20+
// Keep list unique to avoid duplicate lookups
21+
$this->paths = array_values(array_unique($this->paths));
22+
}
23+
24+
/**
25+
* @return string[]
26+
*/
27+
public function getPaths(): array
28+
{
29+
return $this->paths;
30+
}
31+
}

src/Seeders/Seeder.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Codemonster\Database\Seeders;
4+
5+
abstract class Seeder
6+
{
7+
/**
8+
* Run the seeder.
9+
*/
10+
abstract public function run(): void;
11+
}

0 commit comments

Comments
 (0)