fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View 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';
}
}

View 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;
}
}
}

View 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;
}
}

View 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);
}
}

View 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";
}
}
}

View 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
);
});
}
}

View 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;
}