diff --git a/packages/auth/src/CurrentUserInitializer.php b/packages/auth/src/CurrentUserInitializer.php index 5097813ef..b6f7a4d31 100644 --- a/packages/auth/src/CurrentUserInitializer.php +++ b/packages/auth/src/CurrentUserInitializer.php @@ -8,15 +8,16 @@ use Tempest\Container\DynamicInitializer; use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; +use UnitEnum; final readonly class CurrentUserInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->implements(CanAuthenticate::class); } - public function initialize(ClassReflector $class, ?string $tag, Container $container): object + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): object { $user = $container->get(Authenticator::class)->currentUser(); diff --git a/packages/cache/src/CacheInitializer.php b/packages/cache/src/CacheInitializer.php index f10d8318e..1d22e9b87 100644 --- a/packages/cache/src/CacheInitializer.php +++ b/packages/cache/src/CacheInitializer.php @@ -9,19 +9,20 @@ use Tempest\Container\DynamicInitializer; use Tempest\Container\Singleton; use Tempest\Reflection\ClassReflector; +use UnitEnum; use function Tempest\env; use function Tempest\Support\str; final readonly class CacheInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Cache::class); } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): Cache + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Cache { return new GenericCache( cacheConfig: $container->get(CacheConfig::class, $tag), diff --git a/packages/cache/src/Testing/RestrictedCacheInitializer.php b/packages/cache/src/Testing/RestrictedCacheInitializer.php index c5a7573d8..ae4e9b9d7 100644 --- a/packages/cache/src/Testing/RestrictedCacheInitializer.php +++ b/packages/cache/src/Testing/RestrictedCacheInitializer.php @@ -8,17 +8,18 @@ use Tempest\Container\Singleton; use Tempest\Discovery\SkipDiscovery; use Tempest\Reflection\ClassReflector; +use UnitEnum; #[SkipDiscovery] final class RestrictedCacheInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Cache::class); } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): Cache + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Cache { return new RestrictedCache($tag); } diff --git a/packages/container/src/Container.php b/packages/container/src/Container.php index 0c4d2b878..dc970491b 100644 --- a/packages/container/src/Container.php +++ b/packages/container/src/Container.php @@ -7,6 +7,7 @@ use Tempest\Reflection\ClassReflector; use Tempest\Reflection\FunctionReflector; use Tempest\Reflection\MethodReflector; +use UnitEnum; interface Container { @@ -14,7 +15,7 @@ public function register(string $className, callable $definition): self; public function unregister(string $className, bool $tagged = false): self; - public function singleton(string $className, mixed $definition, ?string $tag = null): self; + public function singleton(string $className, mixed $definition, null|string|UnitEnum $tag = null): self; public function config(object $config): self; @@ -23,9 +24,9 @@ public function config(object $config): self; * @param class-string $className * @return null|TClassName */ - public function get(string $className, ?string $tag = null, mixed ...$params): mixed; + public function get(string $className, null|string|UnitEnum $tag = null, mixed ...$params): mixed; - public function has(string $className, ?string $tag = null): bool; + public function has(string $className, null|string|UnitEnum $tag = null): bool; public function invoke(ClassReflector|MethodReflector|FunctionReflector|callable|string $method, mixed ...$params): mixed; diff --git a/packages/container/src/DynamicInitializer.php b/packages/container/src/DynamicInitializer.php index 85cd4c70e..f650e8318 100644 --- a/packages/container/src/DynamicInitializer.php +++ b/packages/container/src/DynamicInitializer.php @@ -5,10 +5,11 @@ namespace Tempest\Container; use Tempest\Reflection\ClassReflector; +use UnitEnum; interface DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool; + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool; - public function initialize(ClassReflector $class, ?string $tag, Container $container): object; + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): object; } diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index e1e2fac64..cb94cbf87 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -17,6 +17,7 @@ use Tempest\Reflection\ParameterReflector; use Tempest\Reflection\TypeReflector; use Throwable; +use UnitEnum; final class GenericContainer implements Container { @@ -122,12 +123,12 @@ public function unregister(string $className, bool $tagged = false): self return $this; } - public function has(string $className, ?string $tag = null): bool + public function has(string $className, null|string|UnitEnum $tag = null): bool { return isset($this->definitions[$className]) || isset($this->singletons[$this->resolveTaggedName($className, $tag)]); } - public function singleton(string $className, mixed $definition, ?string $tag = null): self + public function singleton(string $className, mixed $definition, null|string|UnitEnum $tag = null): self { if ($definition instanceof HasTag) { $tag = $definition->tag; @@ -156,7 +157,7 @@ public function config(object $config): self * @param class-string $className * @return null|TClassName */ - public function get(string $className, ?string $tag = null, mixed ...$params): ?object + public function get(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object { $this->resolveChain(); @@ -298,7 +299,7 @@ public function removeInitializer(ClassReflector|string $initializerClass): Cont return $this; } - private function resolve(string $className, ?string $tag = null, mixed ...$params): ?object + private function resolve(string $className, null|string|UnitEnum $tag = null, mixed ...$params): ?object { $class = new ClassReflector($className); @@ -361,7 +362,7 @@ private function initializerForBuiltin(TypeReflector $target, string $tag): ?Ini return null; } - private function initializerForClass(ClassReflector $target, ?string $tag = null): null|Initializer|DynamicInitializer + private function initializerForClass(ClassReflector $target, null|string|UnitEnum $tag = null): null|Initializer|DynamicInitializer { // Initializers themselves can't be initialized, // otherwise you'd end up with infinite loops @@ -454,7 +455,7 @@ private function autowireDependencies(MethodReflector|FunctionReflector $method, return $dependencies; } - private function autowireDependency(ParameterReflector $parameter, ?string $tag, mixed $providedValue = null): mixed + private function autowireDependency(ParameterReflector $parameter, null|string|UnitEnum $tag, mixed $providedValue = null): mixed { $parameterType = $parameter->getType(); @@ -494,7 +495,7 @@ private function autowireDependency(ParameterReflector $parameter, ?string $tag, throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter)); } - private function autowireObjectDependency(TypeReflector $type, ?string $tag, mixed $providedValue, bool $lazy): mixed + private function autowireObjectDependency(TypeReflector $type, null|string|UnitEnum $tag, mixed $providedValue, bool $lazy): mixed { // If the provided value is of the right type, // don't waste time autowiring, return it! @@ -591,8 +592,12 @@ public function __clone(): void $this->chain = $this->chain?->clone(); } - private function resolveTaggedName(string $className, ?string $tag): string + private function resolveTaggedName(string $className, null|string|UnitEnum $tag): string { + if ($tag instanceof UnitEnum) { + $tag = $tag->name; + } + return $tag ? "{$className}#{$tag}" : $className; diff --git a/packages/container/tests/ContainerTest.php b/packages/container/tests/ContainerTest.php index e904fcfc2..6268bd61f 100644 --- a/packages/container/tests/ContainerTest.php +++ b/packages/container/tests/ContainerTest.php @@ -33,6 +33,7 @@ use Tempest\Container\Tests\Fixtures\ContainerObjectEInitializer; use Tempest\Container\Tests\Fixtures\DependencyWithBuiltinDependencies; use Tempest\Container\Tests\Fixtures\DependencyWithTaggedDependency; +use Tempest\Container\Tests\Fixtures\EnumTag; use Tempest\Container\Tests\Fixtures\HasTagObject; use Tempest\Container\Tests\Fixtures\ImplementsInterfaceA; use Tempest\Container\Tests\Fixtures\InjectA; @@ -258,6 +259,26 @@ public function test_tagged_singleton(): void $this->assertSame('cli', $container->get(TaggedDependency::class, 'cli')->name); } + public function test_tagged_singleton_with_enum(): void + { + $container = new GenericContainer(); + + $container->singleton( + TaggedDependency::class, + new TaggedDependency('web'), + tag: EnumTag::FOO, + ); + + $container->singleton( + TaggedDependency::class, + new TaggedDependency('cli'), + tag: EnumTag::BAR, + ); + + $this->assertSame('web', $container->get(TaggedDependency::class, EnumTag::FOO)->name); + $this->assertSame('cli', $container->get(TaggedDependency::class, EnumTag::BAR)->name); + } + public function test_tagged_singleton_with_initializer(): void { $container = new GenericContainer(); diff --git a/packages/container/tests/Fixtures/ContainerObjectEInitializer.php b/packages/container/tests/Fixtures/ContainerObjectEInitializer.php index d21cc632b..525c5f109 100644 --- a/packages/container/tests/Fixtures/ContainerObjectEInitializer.php +++ b/packages/container/tests/Fixtures/ContainerObjectEInitializer.php @@ -8,15 +8,16 @@ use Tempest\Container\DynamicInitializer; use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; +use UnitEnum; final class ContainerObjectEInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getName() === ContainerObjectE::class; } - public function initialize(ClassReflector $class, ?string $tag, Container $container): object + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): object { return new ContainerObjectE(); } diff --git a/packages/container/tests/Fixtures/EnumTag.php b/packages/container/tests/Fixtures/EnumTag.php new file mode 100644 index 000000000..b4684f709 --- /dev/null +++ b/packages/container/tests/Fixtures/EnumTag.php @@ -0,0 +1,9 @@ +count, [...$this->bindings, ...$bindings]); + return new Query($this->count, [...$this->bindings, ...$bindings])->onDatabase($this->onDatabase); } private function resolveTable(string|object $model): TableDefinition diff --git a/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php index c8a71a892..32b71b66b 100644 --- a/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php @@ -4,6 +4,7 @@ use Tempest\Database\Builder\ModelDefinition; use Tempest\Database\Builder\TableDefinition; +use Tempest\Database\OnDatabase; use Tempest\Database\Query; use Tempest\Database\QueryStatements\DeleteStatement; use Tempest\Database\QueryStatements\WhereStatement; @@ -16,7 +17,7 @@ */ final class DeleteQueryBuilder implements BuildsQuery { - use HasConditions; + use HasConditions, OnDatabase; private DeleteStatement $delete; @@ -68,6 +69,6 @@ public function toSql(): string public function build(mixed ...$bindings): Query { - return new Query($this->delete, [...$this->bindings, ...$bindings]); + return new Query($this->delete, [...$this->bindings, ...$bindings])->onDatabase($this->onDatabase); } } diff --git a/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php index f8ad7f9d5..fda1af370 100644 --- a/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php @@ -8,16 +8,20 @@ use Tempest\Database\Exceptions\CannotInsertHasManyRelation; use Tempest\Database\Exceptions\CannotInsertHasOneRelation; use Tempest\Database\Id; +use Tempest\Database\OnDatabase; use Tempest\Database\Query; use Tempest\Database\QueryStatements\InsertStatement; use Tempest\Mapper\SerializerFactory; use Tempest\Reflection\ClassReflector; use Tempest\Support\Arr\ImmutableArray; +use Tempest\Support\Conditions\HasConditions; use function Tempest\Database\model; final class InsertQueryBuilder implements BuildsQuery { + use HasConditions, OnDatabase; + private InsertStatement $insert; private array $after = []; @@ -70,10 +74,7 @@ public function build(mixed ...$bindings): Query $this->insert->addEntry($data); } - return new Query( - $this->insert, - $bindings, - ); + return new Query($this->insert, $bindings)->onDatabase($this->onDatabase); } public function then(Closure ...$callbacks): self diff --git a/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php index 46dec2628..db0037f70 100644 --- a/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php @@ -6,10 +6,10 @@ use Closure; use Tempest\Database\Builder\FieldDefinition; -use Tempest\Database\Builder\ModelDefinition; use Tempest\Database\Builder\ModelInspector; use Tempest\Database\Id; use Tempest\Database\Mappers\SelectModelMapper; +use Tempest\Database\OnDatabase; use Tempest\Database\Query; use Tempest\Database\QueryStatements\FieldStatement; use Tempest\Database\QueryStatements\JoinStatement; @@ -19,6 +19,7 @@ use Tempest\Database\QueryStatements\WhereStatement; use Tempest\Support\Arr\ImmutableArray; use Tempest\Support\Conditions\HasConditions; +use UnitEnum; use function Tempest\Database\model; use function Tempest\map; @@ -28,7 +29,7 @@ */ final class SelectQueryBuilder implements BuildsQuery { - use HasConditions; + use HasConditions, OnDatabase; /** @var class-string $modelClass */ private readonly string $modelClass; @@ -225,7 +226,7 @@ public function build(mixed ...$bindings): Query $this->select->join[] = $relation->getJoinStatement(); } - return new Query($this->select, [...$this->bindings, ...$bindings]); + return new Query($this->select, [...$this->bindings, ...$bindings])->onDatabase($this->onDatabase); } private function clone(): self diff --git a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php index 0d5961f3f..0efb5d705 100644 --- a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php @@ -5,6 +5,7 @@ use Tempest\Database\Exceptions\CannotUpdateHasManyRelation; use Tempest\Database\Exceptions\CannotUpdateHasOneRelation; use Tempest\Database\Id; +use Tempest\Database\OnDatabase; use Tempest\Database\Query; use Tempest\Database\QueryStatements\UpdateStatement; use Tempest\Database\QueryStatements\WhereStatement; @@ -18,7 +19,7 @@ final class UpdateQueryBuilder implements BuildsQuery { - use HasConditions; + use HasConditions, OnDatabase; private UpdateStatement $update; @@ -87,7 +88,7 @@ public function build(mixed ...$bindings): Query $bindings[] = $binding; } - return new Query($this->update, $bindings); + return new Query($this->update, $bindings)->onDatabase($this->onDatabase); } private function resolveValues(): ImmutableArray diff --git a/packages/database/src/Database.php b/packages/database/src/Database.php index 241d6ae0d..c5ca3a68d 100644 --- a/packages/database/src/Database.php +++ b/packages/database/src/Database.php @@ -4,15 +4,22 @@ namespace Tempest\Database; +use Tempest\Database\Builder\QueryBuilders\BuildsQuery; +use Tempest\Database\Config\DatabaseDialect; + interface Database { - public function execute(Query $query): void; + public DatabaseDialect $dialect { + get; + } + + public function execute(BuildsQuery|Query $query): void; public function getLastInsertId(): ?Id; - public function fetch(Query $query): array; + public function fetch(BuildsQuery|Query $query): array; - public function fetchFirst(Query $query): ?array; + public function fetchFirst(BuildsQuery|Query $query): ?array; public function withinTransaction(callable $callback): bool; } diff --git a/packages/database/src/DatabaseDialectInitializer.php b/packages/database/src/DatabaseDialectInitializer.php deleted file mode 100644 index 45ccda28b..000000000 --- a/packages/database/src/DatabaseDialectInitializer.php +++ /dev/null @@ -1,16 +0,0 @@ -get(DatabaseConfig::class)->dialect; - } -} diff --git a/packages/database/src/DatabaseInitializer.php b/packages/database/src/DatabaseInitializer.php index cdada320e..2cffe1846 100644 --- a/packages/database/src/DatabaseInitializer.php +++ b/packages/database/src/DatabaseInitializer.php @@ -12,32 +12,36 @@ use Tempest\Database\Connection\PDOConnection; use Tempest\Database\Transactions\GenericTransactionManager; use Tempest\Reflection\ClassReflector; +use UnitEnum; final readonly class DatabaseInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Database::class); } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): Database + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Database { - $container->singleton(Connection::class, function () use ($tag, $container) { - $config = $container->get(DatabaseConfig::class, $tag); + $container->singleton( + className: Connection::class, + definition: function () use ($tag, $container) { + $config = $container->get(DatabaseConfig::class, $tag); - $connection = new PDOConnection($config); - $connection->connect(); + $connection = new PDOConnection($config); + $connection->connect(); - return $connection; - }); + return $connection; + }, + tag: $tag, + ); - $connection = $container->get(Connection::class); + $connection = $container->get(Connection::class, $tag); return new GenericDatabase( $connection, new GenericTransactionManager($connection), - $container->get(DatabaseConfig::class)->dialect, ); } } diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index 57badacc3..d43c4b204 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -9,6 +9,7 @@ use PDO; use PDOException; use PDOStatement; +use Tempest\Database\Builder\QueryBuilders\BuildsQuery; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Connection\Connection; use Tempest\Database\Exceptions\QueryException; @@ -20,30 +21,30 @@ final class GenericDatabase implements Database private ?PDOStatement $lastStatement = null; private ?Query $lastQuery = null; + public DatabaseDialect $dialect { + get => $this->connection->config->dialect; + } + public function __construct( private(set) readonly Connection $connection, private(set) readonly TransactionManager $transactionManager, - private(set) readonly DatabaseDialect $dialect, ) {} - public function execute(Query $query): void + public function execute(BuildsQuery|Query $query): void { + if ($query instanceof BuildsQuery) { + $query = $query->build(); + } + $bindings = $this->resolveBindings($query); try { - // foreach (explode(';', $query->toSql()) as $sql) { - // if (! trim($sql)) { - // continue; - // } - $statement = $this->connection->prepare($query->toSql()); $statement->execute($bindings); $this->lastStatement = $statement; $this->lastQuery = $query; - - // } } catch (PDOException $pdoException) { throw new QueryException($query, $bindings, $pdoException); } @@ -68,8 +69,12 @@ public function getLastInsertId(): ?Id return Id::tryFrom($lastInsertId); } - public function fetch(Query $query): array + public function fetch(BuildsQuery|Query $query): array { + if ($query instanceof BuildsQuery) { + $query = $query->build(); + } + $bindings = $this->resolveBindings($query); $pdoQuery = $this->connection->prepare($query->toSql()); @@ -79,8 +84,12 @@ public function fetch(Query $query): array return $pdoQuery->fetchAll(PDO::FETCH_NAMED); } - public function fetchFirst(Query $query): ?array + public function fetchFirst(BuildsQuery|Query $query): ?array { + if ($query instanceof BuildsQuery) { + $query = $query->build(); + } + return $this->fetch($query)[0] ?? null; } diff --git a/packages/database/src/Migrations/MigrationManager.php b/packages/database/src/Migrations/MigrationManager.php index b5875f496..46d5f7f53 100644 --- a/packages/database/src/Migrations/MigrationManager.php +++ b/packages/database/src/Migrations/MigrationManager.php @@ -5,6 +5,7 @@ namespace Tempest\Database\Migrations; use PDOException; +use Tempest\Container\Container; use Tempest\Database\Builder\ModelDefinition; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Database; @@ -13,6 +14,7 @@ use Tempest\Database\Exceptions\QueryException; use Tempest\Database\HasLeadingStatements; use Tempest\Database\HasTrailingStatements; +use Tempest\Database\OnDatabase; use Tempest\Database\Query; use Tempest\Database\QueryStatement; use Tempest\Database\QueryStatements\CompoundStatement; @@ -21,24 +23,34 @@ use Tempest\Database\QueryStatements\ShowTablesStatement; use Throwable; +use function Tempest\Database\query; use function Tempest\event; -final readonly class MigrationManager +final class MigrationManager { + use OnDatabase; + + private Database $database { + get => $this->container->get(Database::class, $this->onDatabase); + } + + private DatabaseDialect $dialect { + get => $this->database->dialect; + } + public function __construct( - private DatabaseDialect $dialect, - private Database $database, - private RunnableMigrations $migrations, + private readonly RunnableMigrations $migrations, + private readonly Container $container, ) {} public function up(): void { try { - $existingMigrations = Migration::all(); + $existingMigrations = Migration::select()->onDatabase($this->onDatabase)->all(); } catch (PDOException $pdoException) { if ($this->dialect->isTableNotFoundError($pdoException)) { $this->executeUp(new CreateMigrationsTable()); - $existingMigrations = Migration::all(); + $existingMigrations = Migration::select()->onDatabase($this->onDatabase)->all(); } else { throw $pdoException; } @@ -61,7 +73,7 @@ public function up(): void public function down(): void { try { - $existingMigrations = Migration::all(); + $existingMigrations = Migration::select()->onDatabase($this->onDatabase)->all(); } catch (PDOException $pdoException) { if (! $this->dialect->isTableNotFoundError($pdoException)) { throw $pdoException; @@ -97,11 +109,11 @@ public function dropAll(): void $tables = $this->getTableDefinitions(); // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: false)->execute($this->dialect); + new SetForeignKeyChecksStatement(enable: false)->execute($this->dialect, $this->onDatabase); // Drop each table foreach ($tables as $table) { - new DropTableStatement($table->name)->execute($this->dialect); + new DropTableStatement($table->name)->execute($this->dialect, $this->onDatabase); event(new TableDropped($table->name)); } @@ -109,14 +121,16 @@ public function dropAll(): void event(new FreshMigrationFailed($throwable)); } finally { // Enable foreign key checks - new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect); + new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect, $this->onDatabase); } } public function rehashAll(): void { try { - $existingMigrations = Migration::all(); + $existingMigrations = Migration::select() + ->onDatabase($this->onDatabase) + ->all(); } catch (PDOException) { return; } @@ -147,7 +161,7 @@ public function rehashAll(): void public function validate(): void { try { - $existingMigrations = Migration::all(); + $existingMigrations = Migration::select()->onDatabase($this->onDatabase)->all(); } catch (PDOException) { return; } @@ -206,9 +220,11 @@ public function executeUp(MigrationInterface $migration): void $this->database->execute($query); } - Migration::create( - name: $migration->name, - hash: $this->getMigrationHash($migration), + $this->database->execute( + query(Migration::class)->insert( + name: $migration->name, + hash: $this->getMigrationHash($migration), + ), ); } catch (PDOException $pdoException) { event(new MigrationFailed($migration->name, $pdoException)); @@ -233,15 +249,15 @@ public function executeDown(MigrationInterface $migration): void // TODO: don't just disable FK checking when executing down // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: false)->execute($this->dialect); + new SetForeignKeyChecksStatement(enable: false)->execute($this->dialect, $this->onDatabase); $this->database->execute($query); // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect); + new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect, $this->onDatabase); } catch (PDOException $pdoException) { // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect); + new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect, $this->onDatabase); event(new MigrationFailed($migration->name, $pdoException)); diff --git a/packages/database/src/OnDatabase.php b/packages/database/src/OnDatabase.php new file mode 100644 index 000000000..ea2d3d0d5 --- /dev/null +++ b/packages/database/src/OnDatabase.php @@ -0,0 +1,19 @@ +onDatabase = $databaseTag; + + return $clone; + } +} diff --git a/packages/database/src/Query.php b/packages/database/src/Query.php index 1b1df535e..24a45de1e 100644 --- a/packages/database/src/Query.php +++ b/packages/database/src/Query.php @@ -4,13 +4,22 @@ namespace Tempest\Database; -use Tempest\Database\Config\DatabaseConfig; use Tempest\Database\Config\DatabaseDialect; use function Tempest\get; final class Query { + use OnDatabase; + + private Database $database { + get => get(Database::class, $this->onDatabase); + } + + private DatabaseDialect $dialect { + get => $this->database->dialect; + } + public function __construct( public string|QueryStatement $sql, public array $bindings = [], @@ -22,7 +31,7 @@ public function execute(mixed ...$bindings): ?Id { $this->bindings = [...$this->bindings, ...$bindings]; - $database = $this->getDatabase(); + $database = $this->database; $query = $this->withBindings($bindings); @@ -37,19 +46,19 @@ public function execute(mixed ...$bindings): ?Id public function fetch(mixed ...$bindings): array { - return $this->getDatabase()->fetch($this->withBindings($bindings)); + return $this->database->fetch($this->withBindings($bindings)); } public function fetchFirst(mixed ...$bindings): ?array { - return $this->getDatabase()->fetchFirst($this->withBindings($bindings)); + return $this->database->fetchFirst($this->withBindings($bindings)); } public function toSql(): string { $sql = $this->sql; - $dialect = $this->getDatabaseConfig()->dialect; + $dialect = $this->dialect; if ($sql instanceof QueryStatement) { $sql = $sql->compile($dialect); @@ -77,14 +86,4 @@ public function withBindings(array $bindings): self return $clone; } - - private function getDatabase(): Database - { - return get(Database::class); - } - - private function getDatabaseConfig(): DatabaseConfig - { - return get(DatabaseConfig::class); - } } diff --git a/packages/database/src/QueryStatements/CanExecuteStatement.php b/packages/database/src/QueryStatements/CanExecuteStatement.php index a06a4e235..82dd9a875 100644 --- a/packages/database/src/QueryStatements/CanExecuteStatement.php +++ b/packages/database/src/QueryStatements/CanExecuteStatement.php @@ -7,10 +7,11 @@ use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Id; use Tempest\Database\Query; +use UnitEnum; trait CanExecuteStatement { - public function execute(DatabaseDialect $dialect): ?Id + public function execute(DatabaseDialect $dialect, null|string|UnitEnum $onDatabase): ?Id { $sql = $this->compile($dialect); @@ -18,6 +19,8 @@ public function execute(DatabaseDialect $dialect): ?Id return null; } - return new Query($sql)->execute(); + return new Query($sql) + ->onDatabase($onDatabase) + ->execute(); } } diff --git a/packages/database/src/functions.php b/packages/database/src/functions.php index 21b081903..c71b23c15 100644 --- a/packages/database/src/functions.php +++ b/packages/database/src/functions.php @@ -1,7 +1,6 @@ getParameters() as $parameter) { + if ($parameter->getName() === $name) { + return $parameter; + } + } + + return null; + } + public function invokeArgs(?object $object, array $args = []): mixed { return $this->reflectionMethod->invokeArgs($object, $args); diff --git a/packages/reflection/src/TypeReflector.php b/packages/reflection/src/TypeReflector.php index ec436848a..600312bf0 100644 --- a/packages/reflection/src/TypeReflector.php +++ b/packages/reflection/src/TypeReflector.php @@ -151,10 +151,16 @@ public function isClass(): bool public function isEnum(): bool { - if ($this->matches(UnitEnum::class)) { - return true; - } + return $this->isUnitEnum() || $this->isBackedEnum(); + } + public function isUnitEnum(): bool + { + return $this->matches(UnitEnum::class); + } + + public function isBackedEnum(): bool + { return $this->matches(BackedEnum::class); } diff --git a/packages/reflection/tests/Fixtures/TestBackedEnum.php b/packages/reflection/tests/Fixtures/TestBackedEnum.php new file mode 100644 index 000000000..e33bbd8d9 --- /dev/null +++ b/packages/reflection/tests/Fixtures/TestBackedEnum.php @@ -0,0 +1,7 @@ +assertInstanceOf( + ParameterReflector::class, + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('enum'), + ); + + $this->assertNull( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('unknown'), + ); + } +} diff --git a/packages/reflection/tests/TypeReflectorTest.php b/packages/reflection/tests/TypeReflectorTest.php new file mode 100644 index 000000000..06996396b --- /dev/null +++ b/packages/reflection/tests/TypeReflectorTest.php @@ -0,0 +1,94 @@ +assertTrue( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('enum') + ->getType() + ->isEnum(), + ); + + $this->assertTrue( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('backedEnum') + ->getType() + ->isEnum(), + ); + + $this->assertTrue( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('backedEnum') + ->getType() + ->isBackedEnum(), + ); + + $this->assertTrue( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('backedEnum') + ->getType() + ->isEnum(), + ); + + $this->assertTrue( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('backedEnum') + ->getType() + ->isUnitEnum(), + ); + + $this->assertTrue( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('enum') + ->getType() + ->isUnitEnum(), + ); + + $this->assertFalse( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('enum') + ->getType() + ->isBackedEnum(), + ); + + $this->assertFalse( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('other') + ->getType() + ->isBackedEnum(), + ); + + $this->assertFalse( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('other') + ->getType() + ->isEnum(), + ); + + $this->assertFalse( + new ClassReflector(TestClassA::class) + ->getMethod('method') + ->getParameter('other') + ->getType() + ->isUnitEnum(), + ); + } +} diff --git a/packages/router/src/Exceptions/InvalidEnumParameterException.php b/packages/router/src/Exceptions/InvalidEnumParameterException.php new file mode 100644 index 000000000..e9f1c2b67 --- /dev/null +++ b/packages/router/src/Exceptions/InvalidEnumParameterException.php @@ -0,0 +1,15 @@ +toUri([$matchedRoute->route->handler->getDeclaringClass(), $matchedRoute->route->handler->getName()]); foreach ($matchedRoute->params as $key => $value) { + if ($value instanceof BackedEnum) { + $value = $value->value; + } + $currentUri = replace($currentUri, '/({' . preg_quote($key, '/') . '(?::.*?)?})/', $value); } diff --git a/packages/router/src/RouteBindingInitializer.php b/packages/router/src/RouteBindingInitializer.php index dab0e109b..411ca27a1 100644 --- a/packages/router/src/RouteBindingInitializer.php +++ b/packages/router/src/RouteBindingInitializer.php @@ -6,18 +6,18 @@ use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; -use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; use Tempest\Router\Exceptions\NotFoundException; +use UnitEnum; final class RouteBindingInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Bindable::class); } - public function initialize(ClassReflector $class, ?string $tag, Container $container): object + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): object { $matchedRoute = $container->get(MatchedRoute::class); diff --git a/packages/router/src/RouteEnumBindingInitializer.php b/packages/router/src/RouteEnumBindingInitializer.php deleted file mode 100644 index b7f6b14bf..000000000 --- a/packages/router/src/RouteEnumBindingInitializer.php +++ /dev/null @@ -1,43 +0,0 @@ -getType()->matches(BackedEnum::class); - } - - public function initialize(ClassReflector $class, ?string $tag, Container $container): object - { - $matchedRoute = $container->get(MatchedRoute::class); - - $parameter = null; - - foreach ($matchedRoute->route->handler->getParameters() as $searchParameter) { - if ($searchParameter->getType()->equals($class->getType())) { - $parameter = $searchParameter; - - break; - } - } - - $enum = $class->callStatic('tryFrom', $matchedRoute->params[$parameter->getName()]); - - if ($enum === null) { - throw new NotFoundException(); - } - - return $enum; - } -} diff --git a/packages/router/src/Routing/Matching/GenericRouteMatcher.php b/packages/router/src/Routing/Matching/GenericRouteMatcher.php index 97f1b8193..4b73138f2 100644 --- a/packages/router/src/Routing/Matching/GenericRouteMatcher.php +++ b/packages/router/src/Routing/Matching/GenericRouteMatcher.php @@ -5,6 +5,8 @@ namespace Tempest\Router\Routing\Matching; use Psr\Http\Message\ServerRequestInterface as PsrRequest; +use Tempest\Router\Exceptions\InvalidEnumParameterException; +use Tempest\Router\Exceptions\NotFoundException; use Tempest\Router\MatchedRoute; use Tempest\Router\RouteConfig; use Tempest\Router\Routing\Construction\DiscoveredRoute; @@ -59,7 +61,11 @@ private function matchDynamicRoute(PsrRequest $request): ?MatchedRoute $route = $routesForMethod[$matchResult->mark]; // Extract the parameters based on the route and matches - $routeParams = $this->extractParams($route, $matchResult->matches); + try { + $routeParams = $this->extractParams($route, $matchResult->matches); + } catch (InvalidEnumParameterException) { + return null; + } return new MatchedRoute($route, $routeParams); } @@ -73,8 +79,21 @@ private function matchDynamicRoute(PsrRequest $request): ?MatchedRoute private function extractParams(DiscoveredRoute $route, array $routeMatches): array { $valueMap = []; + foreach ($route->parameters as $i => $param) { - $valueMap[$param] = $routeMatches[$i + 1]; + $value = $routeMatches[$i + 1]; + + $parameterReflector = $route->handler->getParameter($param); + + if ($parameterReflector && $parameterReflector->getType()?->isBackedEnum()) { + $value = $parameterReflector->getType()->asClass()->callStatic('tryFrom', $value); + + if ($value === null) { + throw new InvalidEnumParameterException($route->handler, $parameterReflector); + } + } + + $valueMap[$param] = $value; } return $valueMap; diff --git a/packages/storage/src/StorageInitializer.php b/packages/storage/src/StorageInitializer.php index 7b189c768..3e303a5a6 100644 --- a/packages/storage/src/StorageInitializer.php +++ b/packages/storage/src/StorageInitializer.php @@ -7,16 +7,17 @@ use Tempest\Container\Singleton; use Tempest\Reflection\ClassReflector; use Tempest\Storage\Config\StorageConfig; +use UnitEnum; final class StorageInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Storage::class); } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): Storage + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Storage { return new GenericStorage( storageConfig: $container->get(StorageConfig::class, $tag), diff --git a/packages/storage/src/Testing/RestrictedStorageInitializer.php b/packages/storage/src/Testing/RestrictedStorageInitializer.php index b978fbe2b..b52ee2137 100644 --- a/packages/storage/src/Testing/RestrictedStorageInitializer.php +++ b/packages/storage/src/Testing/RestrictedStorageInitializer.php @@ -8,17 +8,18 @@ use Tempest\Discovery\SkipDiscovery; use Tempest\Reflection\ClassReflector; use Tempest\Storage\Storage; +use UnitEnum; #[SkipDiscovery] final class RestrictedStorageInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Storage::class); } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): Storage + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Storage { return new RestrictedStorage($tag); } diff --git a/packages/view/src/Renderers/BladeInitializer.php b/packages/view/src/Renderers/BladeInitializer.php index 4118ec1aa..62dae84a8 100644 --- a/packages/view/src/Renderers/BladeInitializer.php +++ b/packages/view/src/Renderers/BladeInitializer.php @@ -10,12 +10,13 @@ use Tempest\Container\Singleton; use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; +use UnitEnum; use function Tempest\internal_storage_path; final readonly class BladeInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { if (! class_exists(Blade::class)) { return false; @@ -25,7 +26,7 @@ public function canInitialize(ClassReflector $class, ?string $tag): bool } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): object + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): object { $bladeConfig = $container->get(BladeConfig::class); diff --git a/packages/view/src/Renderers/TwigInitializer.php b/packages/view/src/Renderers/TwigInitializer.php index 7053ec8e6..82ad4d067 100644 --- a/packages/view/src/Renderers/TwigInitializer.php +++ b/packages/view/src/Renderers/TwigInitializer.php @@ -10,10 +10,11 @@ use Tempest\Reflection\ClassReflector; use Twig\Environment; use Twig\Loader\FilesystemLoader; +use UnitEnum; final readonly class TwigInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { if (! class_exists(Environment::class)) { return false; @@ -23,7 +24,7 @@ public function canInitialize(ClassReflector $class, ?string $tag): bool } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): object + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): object { $twigConfig = $container->get(TwigConfig::class); $twigLoader = new FilesystemLoader($twigConfig->viewPaths); diff --git a/src/Tempest/Framework/Commands/MigrateDownCommand.php b/src/Tempest/Framework/Commands/MigrateDownCommand.php index 6fd973d5d..1a7865bce 100644 --- a/src/Tempest/Framework/Commands/MigrateDownCommand.php +++ b/src/Tempest/Framework/Commands/MigrateDownCommand.php @@ -5,6 +5,7 @@ namespace Tempest\Framework\Commands; use Tempest\Console\Console; +use Tempest\Console\ConsoleArgument; use Tempest\Console\ConsoleCommand; use Tempest\Console\Middleware\CautionMiddleware; use Tempest\Console\Middleware\ForceMiddleware; @@ -28,9 +29,11 @@ public function __construct( description: 'Rollbacks all executed migrations', middleware: [ForceMiddleware::class, CautionMiddleware::class], )] - public function __invoke(): void - { - $this->migrationManager->down(); + public function __invoke( + #[ConsoleArgument(description: 'Use a specific database.')] + ?string $database = null, + ): void { + $this->migrationManager->onDatabase($database)->down(); $this->console->success(sprintf('Rolled back %s migrations', $this->count)); } diff --git a/src/Tempest/Framework/Commands/MigrateFreshCommand.php b/src/Tempest/Framework/Commands/MigrateFreshCommand.php index 8c3ea5f0b..9d7c32021 100644 --- a/src/Tempest/Framework/Commands/MigrateFreshCommand.php +++ b/src/Tempest/Framework/Commands/MigrateFreshCommand.php @@ -34,6 +34,8 @@ public function __construct( public function __invoke( #[ConsoleArgument(description: 'Validates the integrity of existing migration files by checking if they have been tampered with.')] bool $validate = true, + #[ConsoleArgument(description: 'Use a specific database.')] + ?string $database = null, ): ExitCode { if ($validate) { $validationSuccess = $this->console->call(MigrateValidateCommand::class); @@ -44,13 +46,13 @@ public function __invoke( } $this->console->header('Dropping tables'); - $this->migrationManager->dropAll(); + $this->migrationManager->onDatabase($database)->dropAll(); if ($this->count === 0) { $this->console->info('There is no migration to drop.'); } - return $this->console->call(MigrateUpCommand::class, ['fresh' => false, 'validate' => false]); + return $this->console->call(MigrateUpCommand::class, ['fresh' => false, 'validate' => false, 'database' => $database]); } #[EventHandler] diff --git a/src/Tempest/Framework/Commands/MigrateUpCommand.php b/src/Tempest/Framework/Commands/MigrateUpCommand.php index 129043592..bafe73a27 100644 --- a/src/Tempest/Framework/Commands/MigrateUpCommand.php +++ b/src/Tempest/Framework/Commands/MigrateUpCommand.php @@ -39,6 +39,8 @@ public function __invoke( bool $validate = true, #[ConsoleArgument(description: 'Drops all tables and rerun migrations from scratch.')] bool $fresh = false, + #[ConsoleArgument(description: 'Use a specific database.')] + ?string $database = null, ): ExitCode { if ($validate) { $validationSuccess = $this->console->call(MigrateValidateCommand::class); @@ -49,11 +51,11 @@ public function __invoke( } if ($fresh) { - return $this->console->call(MigrateFreshCommand::class, ['validate' => false, 'fresh' => false]); + return $this->console->call(MigrateFreshCommand::class, ['validate' => false, 'database' => $database]); } $this->console->header('Migrating'); - $this->migrationManager->up(); + $this->migrationManager->onDatabase($database)->up(); if ($this->count === 0) { $this->console->info('There is no new migration to run.'); diff --git a/src/Tempest/Framework/Commands/MigrateValidateCommand.php b/src/Tempest/Framework/Commands/MigrateValidateCommand.php index e69eb3c6e..8d08755cc 100644 --- a/src/Tempest/Framework/Commands/MigrateValidateCommand.php +++ b/src/Tempest/Framework/Commands/MigrateValidateCommand.php @@ -5,6 +5,7 @@ namespace Tempest\Framework\Commands; use Tempest\Console\Console; +use Tempest\Console\ConsoleArgument; use Tempest\Console\ConsoleCommand; use Tempest\Console\ExitCode; use Tempest\Container\Singleton; @@ -28,10 +29,12 @@ public function __construct( name: 'migrate:validate', description: 'Validates the integrity of existing migration files by checking if they have been tampered with.', )] - public function __invoke(): ExitCode - { + public function __invoke( + #[ConsoleArgument(description: 'Use a specific database.')] + ?string $database = null, + ): ExitCode { $this->console->header('Validating migration files'); - $this->migrationManager->validate(); + $this->migrationManager->onDatabase($database)->validate(); if (! $this->validationPassed) { $this->console->writeln(); diff --git a/tests/Integration/Container/Commands/ContainerShowCommandTest.php b/tests/Integration/Container/Commands/ContainerShowCommandTest.php index a7d74d121..420000b17 100644 --- a/tests/Integration/Container/Commands/ContainerShowCommandTest.php +++ b/tests/Integration/Container/Commands/ContainerShowCommandTest.php @@ -6,6 +6,7 @@ use Tempest\Container\Container; use Tempest\Container\GenericContainer; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use UnitEnum; final class ContainerShowCommandTest extends FrameworkIntegrationTestCase { @@ -41,7 +42,7 @@ public function unregister(string $className, bool $tagged = false): self return $this; } - public function singleton(string $className, mixed $definition, ?string $tag = null): self + public function singleton(string $className, mixed $definition, null|string|UnitEnum $tag = null): self { $this->container->singleton($className, $definition, $tag); @@ -55,12 +56,12 @@ public function config(object $config): self return $this; } - public function get(string $className, ?string $tag = null, mixed ...$params): mixed + public function get(string $className, null|string|UnitEnum $tag = null, mixed ...$params): mixed { return $this->container->get($className, $tag, ...$params); } - public function has(string $className, ?string $tag = null): bool + public function has(string $className, null|string|UnitEnum $tag = null): bool { return $this->container->has($className, $tag); } diff --git a/tests/Integration/Database/Builder/InsertQueryBuilderTest.php b/tests/Integration/Database/Builder/InsertQueryBuilderTest.php index f5b5440b0..dd86c0809 100644 --- a/tests/Integration/Database/Builder/InsertQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/InsertQueryBuilderTest.php @@ -3,6 +3,7 @@ namespace Tests\Tempest\Integration\Database\Builder; use Tempest\Database\Config\DatabaseDialect; +use Tempest\Database\Database; use Tempest\Database\Exceptions\CannotInsertHasManyRelation; use Tempest\Database\Exceptions\CannotInsertHasOneRelation; use Tempest\Database\Id; @@ -228,7 +229,7 @@ public function test_insert_with_non_object_model(): void private function buildExpectedInsert(string $query): string { - if ($this->container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { + if ($this->container->get(Database::class)->dialect === DatabaseDialect::POSTGRESQL) { $query .= ' RETURNING *'; } diff --git a/tests/Integration/Database/Builder/SelectQueryBuilderTest.php b/tests/Integration/Database/Builder/SelectQueryBuilderTest.php index aaae8840f..8a67065a5 100644 --- a/tests/Integration/Database/Builder/SelectQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/SelectQueryBuilderTest.php @@ -5,7 +5,9 @@ namespace Tests\Tempest\Integration\Database\Builder; use Tempest\Database\Builder\QueryBuilders\SelectQueryBuilder; +use Tempest\Database\Config\SQLiteConfig; use Tempest\Database\Migrations\CreateMigrationsTable; +use Tempest\Database\Migrations\MigrationManager; use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable; use Tests\Tempest\Fixtures\Migrations\CreateBookTable; use Tests\Tempest\Fixtures\Migrations\CreateChapterTable; diff --git a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php index ca8313ca4..b4d270fc3 100644 --- a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php @@ -4,6 +4,7 @@ use Tempest\Database\Builder\QueryBuilders\UpdateQueryBuilder; use Tempest\Database\Config\DatabaseDialect; +use Tempest\Database\Database; use Tempest\Database\Exceptions\CannotUpdateHasManyRelation; use Tempest\Database\Exceptions\CannotUpdateHasOneRelation; use Tempest\Database\Exceptions\InvalidUpdateStatement; @@ -175,7 +176,7 @@ public function test_insert_new_relation_on_update(): void VALUES (?) SQL; - if ($this->container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { + if ($this->container->get(Database::class)->dialect === DatabaseDialect::POSTGRESQL) { $expected .= ' RETURNING *'; } diff --git a/tests/Integration/Database/Fixtures/.gitignore b/tests/Integration/Database/Fixtures/.gitignore new file mode 100644 index 000000000..767aa8132 --- /dev/null +++ b/tests/Integration/Database/Fixtures/.gitignore @@ -0,0 +1,2 @@ +backup.sqlite +main.sqlite \ No newline at end of file diff --git a/tests/Integration/Database/MultiDatabaseTest.php b/tests/Integration/Database/MultiDatabaseTest.php new file mode 100644 index 000000000..dafff9754 --- /dev/null +++ b/tests/Integration/Database/MultiDatabaseTest.php @@ -0,0 +1,253 @@ +markTestSkipped('Multiple databases are not properly supported on Windows yet'); + } + + $files = [ + __DIR__ . '/Fixtures/main.sqlite', + __DIR__ . '/Fixtures/backup.sqlite', + ]; + + foreach ($files as $file) { + if (is_file($file)) { + unlink($file); + } + + touch($file); + } + + $this->container->removeInitializer(TestingDatabaseInitializer::class); + $this->container->addInitializer(DatabaseInitializer::class); + + $this->container->config(new SQLiteConfig( + path: __DIR__ . '/Fixtures/main.sqlite', + tag: 'main', + )); + + $this->container->config(new SQLiteConfig( + path: __DIR__ . '/Fixtures/backup.sqlite', + tag: 'backup', + )); + } + + public function test_with_multiple_connections(): void + { + $migrationManager = $this->container->get(MigrationManager::class); + + $migrationManager->onDatabase('main')->executeUp(new CreateMigrationsTable()); + $migrationManager->onDatabase('main')->executeUp(new CreatePublishersTable()); + $migrationManager->onDatabase('backup')->executeUp(new CreateMigrationsTable()); + $migrationManager->onDatabase('backup')->executeUp(new CreatePublishersTable()); + + query(Publisher::class) + ->insert( + id: new Id(1), + name: 'Main 1', + description: 'Description Main 1', + ) + ->onDatabase('main') + ->execute(); + + query(Publisher::class) + ->insert( + id: new Id(2), + name: 'Main 2', + description: 'Description Main 2', + ) + ->onDatabase('main') + ->execute(); + + query(Publisher::class) + ->insert( + id: new Id(1), + name: 'Backup 1', + description: 'Description Backup 1', + ) + ->onDatabase('backup') + ->execute(); + + query(Publisher::class) + ->insert( + id: new Id(2), + name: 'Backup 2', + description: 'Description Backup 2', + ) + ->onDatabase('backup') + ->execute(); + + $publishersMain = query(Publisher::class)->select()->onDatabase('main')->all(); + $publishersBackup = query(Publisher::class)->select()->onDatabase('backup')->all(); + + $this->assertCount(2, $publishersMain); + $this->assertSame('Main 1', $publishersMain[0]->name); + $this->assertSame('Main 2', $publishersMain[1]->name); + + $this->assertCount(2, $publishersBackup); + $this->assertSame('Backup 1', $publishersBackup[0]->name); + $this->assertSame('Backup 2', $publishersBackup[1]->name); + + query(Publisher::class)->update(name: 'Updated Main 1')->where('id = ?', 1)->onDatabase('main')->execute(); + query(Publisher::class)->update(name: 'Updated Backup 1')->where('id = ?', 1)->onDatabase('backup')->execute(); + + $this->assertSame('Updated Main 1', query(Publisher::class)->select()->where('id = ?', 1)->onDatabase('main')->first()->name); + $this->assertSame('Updated Backup 1', query(Publisher::class)->select()->where('id = ?', 1)->onDatabase('backup')->first()->name); + + query(Publisher::class)->delete()->where('id = ?', 1)->onDatabase('main')->execute(); + + $this->assertSame(1, query(Publisher::class)->count()->onDatabase('main')->execute()); + $this->assertSame(2, query(Publisher::class)->count()->onDatabase('backup')->execute()); + } + + public function test_with_different_dialects(): void + { + if ($this->container->get(Database::class)->dialect !== DatabaseDialect::MYSQL) { + $this->markTestSkipped('We only test this in the MySQL test action'); + } + + $this->container->config(new SQLiteConfig( + path: __DIR__ . '/Fixtures/main.sqlite', + tag: 'sqlite-main', + )); + + $this->container->config(new MysqlConfig( + tag: 'mysql-main', + )); + + $migrationManager = $this->container->get(MigrationManager::class); + + $migrationManager->onDatabase('sqlite-main')->executeUp(new CreateMigrationsTable()); + $migrationManager->onDatabase('mysql-main')->executeUp(new CreateMigrationsTable()); + + $this->expectNotToPerformAssertions(); + } + + public function test_fails_with_unknown_connection(): void + { + $migrationManager = $this->container->get(MigrationManager::class); + + try { + $migrationManager->onDatabase('unknown')->executeUp(new CreateMigrationsTable()); + } catch (CannotResolveTaggedDependency $cannotResolveTaggedDependency) { + $this->assertStringContainsString('Could not resolve tagged dependency Tempest\Database\Config\DatabaseConfig#unknown', $cannotResolveTaggedDependency->getMessage()); + } + } + + public function test_migrate_up_command(): void + { + $this->console + ->call('migrate:up --database=main') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('main')->execute() > 0); + + $this->assertException( + PDOException::class, + fn () => $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0), + ); + + $this->console + ->call('migrate:up --database=backup') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0); + } + + public function test_migrate_fresh_command(): void + { + $this->console + ->call('migrate:fresh --database=main') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('main')->execute() > 0); + + $this->assertException( + PDOException::class, + fn () => $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0), + ); + + $this->console + ->call('migrate:fresh --database=backup') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0); + } + + public function test_migrate_up_fresh_command(): void + { + $this->console + ->call('migrate:up --fresh --database=main') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('main')->execute() > 0); + + $this->assertException( + PDOException::class, + fn () => $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0), + ); + + $this->console + ->call('migrate:up --fresh --database=backup') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0); + } + + public function test_migrate_down_command(): void + { + $this->console + ->call('migrate:up --database=main') + ->assertSuccess(); + + $this->console + ->call('migrate:up --database=backup') + ->assertSuccess(); + + $this->console + ->call('migrate:down --database=backup') + ->assertSuccess(); + + $this->assertTrue(query(Migration::class)->count()->onDatabase('main')->execute() > 0); + + $this->assertException( + PDOException::class, + fn () => $this->assertTrue(query(Migration::class)->count()->onDatabase('backup')->execute() > 0), + ); + } + + public function test_migrate_validate_command(): void + { + $this->console + ->call('migrate:validate --database=main') + ->assertSuccess(); + } +} diff --git a/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php b/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php index 09cb96656..95856eafd 100644 --- a/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php +++ b/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php @@ -7,6 +7,7 @@ use RuntimeException; use Tempest\Database\Config\DatabaseConfig; use Tempest\Database\Config\DatabaseDialect; +use Tempest\Database\Database; use Tempest\Database\DatabaseMigration; use Tempest\Database\Exceptions\InvalidDefaultValue; use Tempest\Database\Exceptions\InvalidValue; @@ -120,7 +121,7 @@ public function test_enum_statement(): void { $this->migrate(CreateMigrationsTable::class); - if ($this->container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { + if ($this->container->get(Database::class)->dialect === DatabaseDialect::POSTGRESQL) { $enumTypeMigration = new class() implements DatabaseMigration { public string $name = '0'; diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index b67b4138f..32f709c85 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -4,6 +4,7 @@ namespace Tests\Tempest\Integration; +use Closure; use InvalidArgumentException; use Tempest\Console\ConsoleApplication; use Tempest\Console\Input\ConsoleArgumentBag; @@ -32,6 +33,7 @@ use Tempest\View\View; use Tempest\View\ViewConfig; use Tempest\View\ViewRenderer; +use Throwable; use function Tempest\Support\Path\normalize; @@ -188,4 +190,25 @@ protected function assertSameWithoutBackticks(string $expected, string $actual): $clean($actual), ); } + + protected function assertException( + string $expectedExceptionClass, + Closure $handler, + ?Closure $assertException = null, + ): void { + try { + $handler(); + } catch (Throwable $throwable) { + $this->assertInstanceOf($expectedExceptionClass, $throwable); + + if ($assertException !== null) { + $assertException($throwable); + } + + return; + } + + /* @phpstan-ignore-next-line */ + $this->assertTrue(false, "Expected exception {$expectedExceptionClass} was not thrown"); + } } diff --git a/tests/Integration/ORM/Mappers/QueryMapperTest.php b/tests/Integration/ORM/Mappers/QueryMapperTest.php index 0d5c9bd88..864d20813 100644 --- a/tests/Integration/ORM/Mappers/QueryMapperTest.php +++ b/tests/Integration/ORM/Mappers/QueryMapperTest.php @@ -6,6 +6,7 @@ use Tempest\Database\Builder\QueryBuilders\UpdateQueryBuilder; use Tempest\Database\Config\DatabaseDialect; +use Tempest\Database\Database; use Tempest\Database\Id; use Tempest\Database\Query; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; @@ -25,7 +26,7 @@ public function test_insert_query(): void $query = query(Author::class)->insert($author)->build(); - $dialect = $this->container->get(DatabaseDialect::class); + $dialect = $this->container->get(Database::class)->dialect; $expected = match ($dialect) { DatabaseDialect::POSTGRESQL => <<<'SQL' @@ -48,7 +49,7 @@ public function test_update_query(): void $query = query($author)->update(name: 'other')->build(); - $dialect = $this->container->get(DatabaseDialect::class); + $dialect = $this->container->get(Database::class)->dialect; $expected = match ($dialect) { DatabaseDialect::POSTGRESQL => <<<'SQL' diff --git a/tests/Integration/TestingDatabaseInitializer.php b/tests/Integration/TestingDatabaseInitializer.php index 41527801f..2c49e35ce 100644 --- a/tests/Integration/TestingDatabaseInitializer.php +++ b/tests/Integration/TestingDatabaseInitializer.php @@ -8,44 +8,53 @@ use Tempest\Container\DynamicInitializer; use Tempest\Container\Singleton; use Tempest\Database\Config\DatabaseConfig; -use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Connection\Connection; use Tempest\Database\Connection\PDOConnection; use Tempest\Database\Database; use Tempest\Database\GenericDatabase; use Tempest\Database\Transactions\GenericTransactionManager; use Tempest\Reflection\ClassReflector; +use UnitEnum; final class TestingDatabaseInitializer implements DynamicInitializer { - private static ?PDOConnection $connection = null; + /** @var Connection[] */ + private static array $connections = []; - public function canInitialize(ClassReflector $class, ?string $tag): bool + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Database::class); } #[Singleton] - public function initialize(ClassReflector $class, ?string $tag, Container $container): Database + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Database { - if (self::$connection === null) { + $tag = match (true) { + $tag instanceof UnitEnum => $tag->name, + is_string($tag) => $tag, + default => '', + }; + + /** @var PDOConnection|null $connection */ + $connection = self::$connections[$tag] ?? null; + + if ($connection === null) { $config = $container->get(DatabaseConfig::class, $tag); $connection = new PDOConnection($config); $connection->connect(); - self::$connection = $connection; + self::$connections[$tag] = $connection; } - if (self::$connection->ping() === false) { - self::$connection->reconnect(); + if ($connection->ping() === false) { + $connection->reconnect(); } - $container->singleton(Connection::class, self::$connection); + $container->singleton(Connection::class, $connection, $tag); return new GenericDatabase( - self::$connection, - new GenericTransactionManager(self::$connection), - $container->get(DatabaseDialect::class), + $connection, + new GenericTransactionManager($connection), ); } }