fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
50
src/Framework/Database/Seed/Migrations/CreateSeedsTable.php
Normal file
50
src/Framework/Database/Seed/Migrations/CreateSeedsTable.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\Migration\SafelyReversible;
|
||||
use App\Framework\Database\Schema\Blueprint;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
final readonly class CreateSeedsTable implements Migration, SafelyReversible
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
|
||||
$schema->create('seeds', function (Blueprint $table) {
|
||||
$table->string('id', 255)->primary();
|
||||
$table->string('name', 255)->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamp('executed_at');
|
||||
|
||||
$table->index('name');
|
||||
});
|
||||
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
|
||||
$schema->dropIfExists('seeds');
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return MigrationVersion::fromTimestamp('2025_01_15_000005');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create seeds table for tracking executed seeders';
|
||||
}
|
||||
}
|
||||
|
||||
77
src/Framework/Database/Seed/SeedCommand.php
Normal file
77
src/Framework/Database/Seed/SeedCommand.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
|
||||
final readonly class SeedCommand
|
||||
{
|
||||
public function __construct(
|
||||
private SeedLoader $seedLoader,
|
||||
private SeedRunner $seedRunner,
|
||||
private SeedRepository $seedRepository
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('db:seed', 'Run database seeders')]
|
||||
public function seed(ConsoleInput $input): ExitCode
|
||||
{
|
||||
$className = $input->getOption('class');
|
||||
$fresh = $input->hasOption('fresh');
|
||||
|
||||
try {
|
||||
// Clear seeds table if --fresh option is provided
|
||||
if ($fresh) {
|
||||
echo "⚠️ Clearing seeds table (--fresh option)...\n";
|
||||
$this->seedRepository->clearAll();
|
||||
echo "✅ Seeds table cleared.\n\n";
|
||||
}
|
||||
|
||||
if ($className !== null) {
|
||||
// Run specific seeder
|
||||
echo "Running seeder: {$className}\n";
|
||||
$seeder = $this->seedLoader->load($className);
|
||||
|
||||
if ($seeder === null) {
|
||||
echo "❌ Seeder '{$className}' not found or cannot be instantiated.\n";
|
||||
return ExitCode::SOFTWARE_ERROR;
|
||||
}
|
||||
|
||||
$this->seedRunner->run($seeder);
|
||||
echo "✅ Seeder '{$className}' completed.\n";
|
||||
} else {
|
||||
// Run all seeders
|
||||
echo "Running all seeders...\n\n";
|
||||
$seeders = $this->seedLoader->loadAll();
|
||||
|
||||
if (empty($seeders)) {
|
||||
echo "No seeders found.\n";
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
echo sprintf("Found %d seeder(s):\n", count($seeders));
|
||||
foreach ($seeders as $seeder) {
|
||||
echo " - {$seeder->getName()}: {$seeder->getDescription()}\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
$this->seedRunner->runAll($seeders);
|
||||
echo "\n✅ All seeders completed.\n";
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
echo "❌ Seeding failed: " . $e->getMessage() . "\n";
|
||||
echo "Error details: " . get_class($e) . "\n";
|
||||
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||
|
||||
return ExitCode::SOFTWARE_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Framework/Database/Seed/SeedLoader.php
Normal file
64
src/Framework/Database/Seed/SeedLoader.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed;
|
||||
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
|
||||
final readonly class SeedLoader
|
||||
{
|
||||
public function __construct(
|
||||
private DiscoveryRegistry $discoveryRegistry,
|
||||
private Container $container
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all seeder classes that implement the Seeder interface
|
||||
*
|
||||
* @return Seeder[]
|
||||
*/
|
||||
public function loadAll(): array
|
||||
{
|
||||
$seederInterface = Seeder::class;
|
||||
$classNames = $this->discoveryRegistry->interfaces->get($seederInterface);
|
||||
|
||||
$seeders = [];
|
||||
foreach ($classNames as $className) {
|
||||
try {
|
||||
$seeder = $this->container->get($className->getFullyQualified());
|
||||
if ($seeder instanceof Seeder) {
|
||||
$seeders[] = $seeder;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Skip seeders that cannot be instantiated
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $seeders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific seeder by class name
|
||||
*
|
||||
* @param string $className Fully qualified class name
|
||||
* @return Seeder|null
|
||||
*/
|
||||
public function load(string $className): ?Seeder
|
||||
{
|
||||
try {
|
||||
$seeder = $this->container->get($className);
|
||||
if ($seeder instanceof Seeder) {
|
||||
return $seeder;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Return null if seeder cannot be instantiated
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
63
src/Framework/Database/Seed/SeedRepository.php
Normal file
63
src/Framework/Database/Seed/SeedRepository.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\ValueObjects\SqlQuery;
|
||||
use App\Framework\DateTime\Clock;
|
||||
|
||||
final readonly class SeedRepository
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection,
|
||||
private Clock $clock
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a seeder has already been executed
|
||||
*/
|
||||
public function hasRun(string $name): bool
|
||||
{
|
||||
$sql = 'SELECT COUNT(*) as count FROM seeds WHERE name = ?';
|
||||
$query = SqlQuery::create($sql, [$name]);
|
||||
$result = $this->connection->query($query);
|
||||
$row = $result->fetch();
|
||||
|
||||
return ($row['count'] ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a seeder as executed
|
||||
*/
|
||||
public function markAsRun(string $name, string $description): void
|
||||
{
|
||||
$id = $this->generateId($name);
|
||||
$executedAt = $this->clock->now()->format('Y-m-d H:i:s');
|
||||
|
||||
$sql = 'INSERT INTO seeds (id, name, description, executed_at) VALUES (?, ?, ?, ?)';
|
||||
$query = SqlQuery::create($sql, [$id, $name, $description, $executedAt]);
|
||||
$this->connection->execute($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all seed records (for --fresh option)
|
||||
*/
|
||||
public function clearAll(): void
|
||||
{
|
||||
$sql = 'DELETE FROM seeds';
|
||||
$query = SqlQuery::create($sql);
|
||||
$this->connection->execute($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique ID for a seed record
|
||||
*/
|
||||
private function generateId(string $name): string
|
||||
{
|
||||
return hash('sha256', $name);
|
||||
}
|
||||
}
|
||||
|
||||
74
src/Framework/Database/Seed/SeedRunner.php
Normal file
74
src/Framework/Database/Seed/SeedRunner.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed;
|
||||
|
||||
use App\Framework\Logging\Logger;
|
||||
|
||||
final readonly class SeedRunner
|
||||
{
|
||||
public function __construct(
|
||||
private SeedRepository $seedRepository,
|
||||
private ?Logger $logger = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a seeder if it hasn't been executed yet
|
||||
*
|
||||
* @param Seeder $seeder The seeder to run
|
||||
* @throws \Throwable If the seeder fails
|
||||
*/
|
||||
public function run(Seeder $seeder): void
|
||||
{
|
||||
$name = $seeder->getName();
|
||||
|
||||
// Check if already executed
|
||||
if ($this->seedRepository->hasRun($name)) {
|
||||
$this->log("Skipping seeder '{$name}' - already executed", 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log("Running seeder '{$name}'...", 'info');
|
||||
|
||||
try {
|
||||
$seeder->seed();
|
||||
$this->seedRepository->markAsRun($name, $seeder->getDescription());
|
||||
$this->log("Seeder '{$name}' completed successfully", 'info');
|
||||
} catch (\Throwable $e) {
|
||||
$this->log("Seeder '{$name}' failed: {$e->getMessage()}", 'error');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple seeders
|
||||
*
|
||||
* @param Seeder[] $seeders Array of seeders to run
|
||||
*/
|
||||
public function runAll(array $seeders): void
|
||||
{
|
||||
foreach ($seeders as $seeder) {
|
||||
$this->run($seeder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message if logger is available
|
||||
*/
|
||||
private function log(string $message, string $level = 'info'): void
|
||||
{
|
||||
if ($this->logger !== null) {
|
||||
match ($level) {
|
||||
'error' => $this->logger->error($message),
|
||||
'warning' => $this->logger->warning($message),
|
||||
default => $this->logger->info($message),
|
||||
};
|
||||
} else {
|
||||
// Fallback to echo if no logger available
|
||||
echo "[{$level}] {$message}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/Framework/Database/Seed/SeedServicesInitializer.php
Normal file
43
src/Framework/Database/Seed/SeedServicesInitializer.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Logging\Logger;
|
||||
|
||||
final readonly class SeedServicesInitializer
|
||||
{
|
||||
#[Initializer]
|
||||
public function initializeSeeds(Container $container): void
|
||||
{
|
||||
// SeedRepository
|
||||
$container->singleton(SeedRepository::class, function (Container $c) {
|
||||
return new SeedRepository(
|
||||
$c->get(ConnectionInterface::class),
|
||||
$c->get(Clock::class)
|
||||
);
|
||||
});
|
||||
|
||||
// SeedLoader
|
||||
$container->singleton(SeedLoader::class, function (Container $c) {
|
||||
$discoveryRegistry = $c->get(DiscoveryRegistry::class);
|
||||
return new SeedLoader($discoveryRegistry, $c);
|
||||
});
|
||||
|
||||
// SeedRunner
|
||||
$container->singleton(SeedRunner::class, function (Container $c) {
|
||||
$logger = $c->has(Logger::class) ? $c->get(Logger::class) : null;
|
||||
return new SeedRunner(
|
||||
$c->get(SeedRepository::class),
|
||||
$logger
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
39
src/Framework/Database/Seed/Seeder.php
Normal file
39
src/Framework/Database/Seed/Seeder.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Seed;
|
||||
|
||||
/**
|
||||
* Interface for database seeders
|
||||
*
|
||||
* Seeders are used to populate the database with initial data.
|
||||
* They are idempotent and can be run multiple times safely.
|
||||
*/
|
||||
interface Seeder
|
||||
{
|
||||
/**
|
||||
* Execute the seeder
|
||||
*
|
||||
* This method should be idempotent - running it multiple times
|
||||
* should produce the same result without creating duplicates.
|
||||
*/
|
||||
public function seed(): void;
|
||||
|
||||
/**
|
||||
* Get the unique name of this seeder
|
||||
*
|
||||
* Used for tracking which seeders have been executed.
|
||||
*
|
||||
* @return string Unique seeder name
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Get a human-readable description of what this seeder does
|
||||
*
|
||||
* @return string Description
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user