diff --git a/.gitignore b/.gitignore index cb5a0ca5f..3fac206a0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ tests/Unit/Log log/ node_modules dist -profile/ \ No newline at end of file +profile/ +db-main.sqlite +db-tenant-1.sqlite +db-tenant-2.sqlite \ No newline at end of file diff --git a/src/Tempest/Auth/src/CurrentUserInitializer.php b/src/Tempest/Auth/src/CurrentUserInitializer.php index 203e0278f..5097813ef 100644 --- a/src/Tempest/Auth/src/CurrentUserInitializer.php +++ b/src/Tempest/Auth/src/CurrentUserInitializer.php @@ -6,16 +6,17 @@ use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; +use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; final readonly class CurrentUserInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class): bool + public function canInitialize(ClassReflector $class, ?string $tag): bool { return $class->implements(CanAuthenticate::class); } - public function initialize(ClassReflector $class, Container $container): object + public function initialize(ClassReflector $class, ?string $tag, Container $container): object { $user = $container->get(Authenticator::class)->currentUser(); diff --git a/src/Tempest/Container/src/DynamicInitializer.php b/src/Tempest/Container/src/DynamicInitializer.php index a14657fbb..85cd4c70e 100644 --- a/src/Tempest/Container/src/DynamicInitializer.php +++ b/src/Tempest/Container/src/DynamicInitializer.php @@ -8,7 +8,7 @@ interface DynamicInitializer { - public function canInitialize(ClassReflector $class): bool; + public function canInitialize(ClassReflector $class, ?string $tag): bool; - public function initialize(ClassReflector $class, Container $container): object; + public function initialize(ClassReflector $class, ?string $tag, Container $container): object; } diff --git a/src/Tempest/Container/src/GenericContainer.php b/src/Tempest/Container/src/GenericContainer.php index ea7edae37..67b7c9e46 100644 --- a/src/Tempest/Container/src/GenericContainer.php +++ b/src/Tempest/Container/src/GenericContainer.php @@ -99,6 +99,10 @@ public function has(string $className, ?string $tag = null): bool public function singleton(string $className, mixed $definition, ?string $tag = null): self { + if ($definition instanceof HasTag) { + $tag = $definition->tag; + } + $className = $this->resolveTaggedName($className, $tag); $this->singletons[$className] = $definition; @@ -270,7 +274,7 @@ private function resolve(string $className, ?string $tag = null, mixed ...$param $object = match (true) { $initializer instanceof Initializer => $initializer->initialize($this->clone()), - $initializer instanceof DynamicInitializer => $initializer->initialize($class, $this->clone()), + $initializer instanceof DynamicInitializer => $initializer->initialize($class, $tag, $this->clone()), }; $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class); @@ -318,7 +322,7 @@ private function initializerForClass(ClassReflector $target, ?string $tag = null /** @var DynamicInitializer $initializer */ $initializer = $this->resolve($initializerClass); - if (! $initializer->canInitialize($target)) { + if (! $initializer->canInitialize(class: $target, tag: $tag)) { continue; } diff --git a/src/Tempest/Container/src/HasTag.php b/src/Tempest/Container/src/HasTag.php new file mode 100644 index 000000000..0aa252f6a --- /dev/null +++ b/src/Tempest/Container/src/HasTag.php @@ -0,0 +1,12 @@ +assertSame('value1', $this->assertSlowerThan(fn () => $instance->dependency->value, $delay)); } + + public function test_has_tags_support(): void + { + $container = new GenericContainer(); + + $container->singleton(HasTagObject::class, new HasTagObject('A', 'tagA')); + $container->singleton(HasTagObject::class, new HasTagObject('B', 'tagB')); + + $this->assertSame('A', $container->get(HasTagObject::class, 'tagA')->name); + $this->assertSame('B', $container->get(HasTagObject::class, 'tagB')->name); + } } diff --git a/src/Tempest/Container/tests/Fixtures/ContainerObjectEInitializer.php b/src/Tempest/Container/tests/Fixtures/ContainerObjectEInitializer.php index c3fc09c1a..d21cc632b 100644 --- a/src/Tempest/Container/tests/Fixtures/ContainerObjectEInitializer.php +++ b/src/Tempest/Container/tests/Fixtures/ContainerObjectEInitializer.php @@ -6,16 +6,17 @@ use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; +use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; final class ContainerObjectEInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class): bool + public function canInitialize(ClassReflector $class, ?string $tag): bool { return $class->getName() === ContainerObjectE::class; } - public function initialize(ClassReflector $class, Container $container): object + public function initialize(ClassReflector $class, ?string $tag, Container $container): object { return new ContainerObjectE(); } diff --git a/src/Tempest/Container/tests/Fixtures/HasTagObject.php b/src/Tempest/Container/tests/Fixtures/HasTagObject.php new file mode 100644 index 000000000..367cb1d7b --- /dev/null +++ b/src/Tempest/Container/tests/Fixtures/HasTagObject.php @@ -0,0 +1,14 @@ +getType()->matches(Database::class); + } + #[Singleton] - public function initialize(Container $container): Database + public function initialize(ClassReflector $class, ?string $tag, Container $container): Database { + $container->singleton(Connection::class, function () use ($tag, $container) { + $config = $container->get(DatabaseConfig::class, $tag); + + $connection = new PDOConnection($config); + $connection->connect(); + + return $connection; + }); + + $connection = $container->get(Connection::class); + return new GenericDatabase( - $container->get(Connection::class), - $container->get(TransactionManager::class), + $connection, + new GenericTransactionManager($connection), ); } } diff --git a/src/Tempest/Database/src/GenericDatabase.php b/src/Tempest/Database/src/GenericDatabase.php index de4ed49e4..4690318b1 100644 --- a/src/Tempest/Database/src/GenericDatabase.php +++ b/src/Tempest/Database/src/GenericDatabase.php @@ -16,8 +16,8 @@ final readonly class GenericDatabase implements Database { public function __construct( - private Connection $connection, - private TransactionManager $transactionManager, + private(set) Connection $connection, + private(set) TransactionManager $transactionManager, ) {} public function execute(Query $query): void diff --git a/src/Tempest/Router/src/RouteBindingInitializer.php b/src/Tempest/Router/src/RouteBindingInitializer.php index 4c0f6414b..dab0e109b 100644 --- a/src/Tempest/Router/src/RouteBindingInitializer.php +++ b/src/Tempest/Router/src/RouteBindingInitializer.php @@ -6,17 +6,18 @@ use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; +use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; use Tempest\Router\Exceptions\NotFoundException; final class RouteBindingInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class): bool + public function canInitialize(ClassReflector $class, ?string $tag): bool { return $class->getType()->matches(Bindable::class); } - public function initialize(ClassReflector $class, Container $container): object + public function initialize(ClassReflector $class, ?string $tag, Container $container): object { $matchedRoute = $container->get(MatchedRoute::class); diff --git a/src/Tempest/Router/src/RouteEnumBindingInitializer.php b/src/Tempest/Router/src/RouteEnumBindingInitializer.php index 6eb63a92c..b7f6b14bf 100644 --- a/src/Tempest/Router/src/RouteEnumBindingInitializer.php +++ b/src/Tempest/Router/src/RouteEnumBindingInitializer.php @@ -7,17 +7,18 @@ use BackedEnum; use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; +use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; use Tempest\Router\Exceptions\NotFoundException; final class RouteEnumBindingInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class): bool + public function canInitialize(ClassReflector $class, ?string $tag): bool { return $class->getType()->matches(BackedEnum::class); } - public function initialize(ClassReflector $class, Container $container): object + public function initialize(ClassReflector $class, ?string $tag, Container $container): object { $matchedRoute = $container->get(MatchedRoute::class); diff --git a/src/Tempest/View/src/Renderers/BladeInitializer.php b/src/Tempest/View/src/Renderers/BladeInitializer.php index 0135dbd1c..4118ec1aa 100644 --- a/src/Tempest/View/src/Renderers/BladeInitializer.php +++ b/src/Tempest/View/src/Renderers/BladeInitializer.php @@ -8,13 +8,14 @@ use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; use Tempest\Container\Singleton; +use Tempest\Container\Tag; use Tempest\Reflection\ClassReflector; use function Tempest\internal_storage_path; final readonly class BladeInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class): bool + public function canInitialize(ClassReflector $class, ?string $tag): bool { if (! class_exists(Blade::class)) { return false; @@ -24,7 +25,7 @@ public function canInitialize(ClassReflector $class): bool } #[Singleton] - public function initialize(ClassReflector $class, Container $container): object + public function initialize(ClassReflector $class, ?string $tag, Container $container): object { $bladeConfig = $container->get(BladeConfig::class); diff --git a/src/Tempest/View/src/Renderers/TwigInitializer.php b/src/Tempest/View/src/Renderers/TwigInitializer.php index 51b7e4a85..7053ec8e6 100644 --- a/src/Tempest/View/src/Renderers/TwigInitializer.php +++ b/src/Tempest/View/src/Renderers/TwigInitializer.php @@ -13,7 +13,7 @@ final readonly class TwigInitializer implements DynamicInitializer { - public function canInitialize(ClassReflector $class): bool + public function canInitialize(ClassReflector $class, ?string $tag): bool { if (! class_exists(Environment::class)) { return false; @@ -23,7 +23,7 @@ public function canInitialize(ClassReflector $class): bool } #[Singleton] - public function initialize(ClassReflector $class, Container $container): object + public function initialize(ClassReflector $class, ?string $tag, Container $container): object { $twigConfig = $container->get(TwigConfig::class); $twigLoader = new FilesystemLoader($twigConfig->viewPaths); diff --git a/tests/Integration/Container/TaggedDynamicInitializerTest.php b/tests/Integration/Container/TaggedDynamicInitializerTest.php new file mode 100644 index 000000000..815d47f1e --- /dev/null +++ b/tests/Integration/Container/TaggedDynamicInitializerTest.php @@ -0,0 +1,43 @@ +addInitializer(DatabaseInitializer::class); + + $container->config(new SQLiteConfig( + path: 'db-main.sqlite', + )); + + $container->config(new SQLiteConfig( + path: 'db-tenant-1.sqlite', + tag: 'tenant-1', + )); + + $container->config(new SQLiteConfig( + path: 'db-tenant-2.sqlite', + tag: 'tenant-2', + )); + + $tenant1 = $container->get(Database::class, tag: 'tenant-1'); + $tenant2 = $container->get(Database::class, tag: 'tenant-2'); + $main = $container->get(Database::class); + + /** @phpstan-ignore-next-line */ + $this->assertSame('db-tenant-1.sqlite', $tenant1->connection->config->path); + /** @phpstan-ignore-next-line */ + $this->assertSame('db-tenant-2.sqlite', $tenant2->connection->config->path); + /** @phpstan-ignore-next-line */ + $this->assertSame('db-main.sqlite', $main->connection->config->path); + } +} diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index 93171d67f..a94db6321 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -16,6 +16,7 @@ use Tempest\Core\ShellExecutor; use Tempest\Core\ShellExecutors\NullShellExecutor; use Tempest\Database\Connection\CachedConnectionInitializer; +use Tempest\Database\Connection\Connection; use Tempest\Database\Migrations\MigrationManager; use Tempest\Discovery\DiscoveryLocation; use Tempest\Framework\Testing\IntegrationTest; @@ -68,6 +69,11 @@ protected function setUp(): void $this->rollbackDatabase(); } + protected function tearDown(): void + { + $this->container->get(Connection::class)->close(); + } + protected function actAsConsoleApplication(string $command = ''): Application { $application = new ConsoleApplication(