diff --git a/README.md b/README.md index 88cc872..1c8a2b2 100644 --- a/README.md +++ b/README.md @@ -186,9 +186,88 @@ $factory->registerCondition('cached-access-check', 300 // Cache for 5 minutes ) ); + +## Service Types + +Ananke supports two types of service instantiation: Singleton and Prototype. + +### Singleton Services + +Singleton services are instantiated only once and the same instance is returned for subsequent requests. This is useful for services that maintain state or are resource-intensive to create. + +```php +use Ananke\ServiceFactory; + +$factory = new ServiceFactory(); + +// Register a service as singleton +$factory->register('database', DatabaseConnection::class); +$factory->registerAsSingleton('database'); + +// Both variables will reference the same instance +$db1 = $factory->create('database'); +$db2 = $factory->create('database'); + +assert($db1 === $db2); // true +``` + +You can also clear singleton instances when needed: + +```php +// Clear all singleton instances +$factory->clearSingletons(); + +// Now you'll get a new instance +$db3 = $factory->create('database'); +assert($db3 !== $db1); // true +``` + +### Prototype Services + +Prototype services create a new instance every time they are requested. This is the default behavior and is ideal for services that should not share state. + +```php +use Ananke\ServiceFactory; + +$factory = new ServiceFactory(); + +// Register a service (prototype by default) +$factory->register('transaction', Transaction::class); + +// Or explicitly register as prototype +$factory->registerAsPrototype('transaction'); + +// Each call creates a new instance +$tx1 = $factory->create('transaction'); +$tx2 = $factory->create('transaction'); + +assert($tx1 !== $tx2); // true +``` + +### Changing Service Types + +You can change a service's type after registration: + +```php +use Ananke\ServiceFactory; + +$factory = new ServiceFactory(); +$factory->register('cache', CacheService::class); + +// Start as singleton +$factory->changeServiceType('cache', 'singleton'); +$cache1 = $factory->create('cache'); +$cache2 = $factory->create('cache'); +assert($cache1 === $cache2); // true + +// Switch to prototype +$factory->changeServiceType('cache', 'prototype'); +$cache3 = $factory->create('cache'); +$cache4 = $factory->create('cache'); +assert($cache3 !== $cache4); // true ``` -### Best Practices +## Best Practices 1. **Caching**: Use `CachedCondition` for: - External API calls diff --git a/src/ServiceFactory.php b/src/ServiceFactory.php index d9f4750..c92ede9 100644 --- a/src/ServiceFactory.php +++ b/src/ServiceFactory.php @@ -6,6 +6,9 @@ use Ananke\Exceptions\ClassNotFoundException; use Ananke\Conditions\ConditionInterface; use Ananke\Conditions\CallableCondition; +use Ananke\Traits\SingletonServiceTrait; +use Ananke\Traits\PrototypeServiceTrait; +use Ananke\Traits\ServiceTypeTrait; /** * A flexible service container that supports conditional service instantiation. @@ -16,6 +19,10 @@ */ class ServiceFactory { + use ServiceTypeTrait; + use SingletonServiceTrait; + use PrototypeServiceTrait; + /** @var array Service name to class name mapping */ private array $services = []; @@ -34,16 +41,22 @@ class ServiceFactory * @param string $serviceName Unique identifier for the service * @param string $className Fully qualified class name that exists * @param array $parameters Optional constructor parameters for the service + * @param string $type Service type (singleton or prototype) * @throws ClassNotFoundException When the class does not exist */ - public function register(string $serviceName, string $className, array $parameters = []): void - { + public function register( + string $serviceName, + string $className, + array $parameters = [], + string $type = 'prototype' + ): void { if (!class_exists($className)) { throw new ClassNotFoundException("Class not found: $className"); } $this->services[$serviceName] = $className; $this->parameters[$serviceName] = $parameters; + $this->setServiceType($serviceName, $type); } /** @@ -135,6 +148,24 @@ public function create(string $serviceName): object // Any failed condition will throw an exception } + if ($this->isSingleton($serviceName)) { + if (!isset($this->singletons[$serviceName])) { + $this->singletons[$serviceName] = $this->createInstance($serviceName); + } + return $this->singletons[$serviceName]; + } + + return $this->createInstance($serviceName); + } + + /** + * Create a new instance of a service + * + * @param string $serviceName Name of the service + * @return object + */ + private function createInstance(string $serviceName): object + { $className = $this->services[$serviceName]; return new $className(...($this->parameters[$serviceName] ?? [])); } @@ -163,4 +194,24 @@ public function has(string $serviceName): bool return false; } } + + /** + * Set the service type during registration + * + * @param string $serviceName Name of the service + * @param string $type Service type (singleton or prototype) + * @throws \InvalidArgumentException When invalid type is provided + */ + private function setServiceType(string $serviceName, string $type): void + { + if (!in_array($type, ['singleton', 'prototype'])) { + throw new \InvalidArgumentException("Invalid service type: $type"); + } + + if ($type === 'singleton') { + $this->registerAsSingleton($serviceName); + } else { + $this->registerAsPrototype($serviceName); + } + } } diff --git a/src/Traits/PrototypeServiceTrait.php b/src/Traits/PrototypeServiceTrait.php new file mode 100644 index 0000000..d257490 --- /dev/null +++ b/src/Traits/PrototypeServiceTrait.php @@ -0,0 +1,34 @@ +singletons)) { + unset($this->singletons[$serviceName]); + } + } + + /** + * Check if a service is registered as prototype + * + * @param string $serviceName Name of the service + * @return bool + */ + public function isPrototype(string $serviceName): bool + { + return !array_key_exists($serviceName, $this->singletons); + } +} diff --git a/src/Traits/ServiceTypeTrait.php b/src/Traits/ServiceTypeTrait.php new file mode 100644 index 0000000..4a0b2c8 --- /dev/null +++ b/src/Traits/ServiceTypeTrait.php @@ -0,0 +1,43 @@ + Service name to service type mapping */ + protected array $serviceTypes = []; + + /** + * Change service type between singleton and prototype + * + * @param string $serviceName Name of the service + * @param string $type New service type ('singleton' or 'prototype') + * @throws \InvalidArgumentException When invalid type is provided + * @return void + */ + public function changeServiceType(string $serviceName, string $type): void + { + if (!in_array($type, ['singleton', 'prototype'])) { + throw new \InvalidArgumentException("Invalid service type: $type"); + } + + if ($type === 'prototype') { + $this->registerAsPrototype($serviceName); + } else { + $this->registerAsSingleton($serviceName); + } + + $this->serviceTypes[$serviceName] = $type; + } + + /** + * Get the current type of a service + * + * @param string $serviceName Name of the service + * @return string + */ + public function getServiceType(string $serviceName): string + { + return $this->serviceTypes[$serviceName] ?? 'prototype'; + } +} diff --git a/src/Traits/SingletonServiceTrait.php b/src/Traits/SingletonServiceTrait.php new file mode 100644 index 0000000..316c5d2 --- /dev/null +++ b/src/Traits/SingletonServiceTrait.php @@ -0,0 +1,59 @@ + Singleton instances storage */ + private array $singletons = []; + + /** + * Register a service as singleton + * + * @param string $serviceName Name of the service + * @return void + */ + public function registerAsSingleton(string $serviceName): void + { + if (!isset($this->singletons[$serviceName])) { + $this->singletons[$serviceName] = null; + } + } + + /** + * Check if a service is registered as singleton + * + * @param string $serviceName Name of the service + * @return bool + */ + public function isSingleton(string $serviceName): bool + { + return array_key_exists($serviceName, $this->singletons); + } + + /** + * Get or create a singleton instance + * + * @param string $serviceName Name of the service + * @param callable $factory Factory function to create the instance if needed + * @return object + */ + protected function getSingletonInstance(string $serviceName, callable $factory): object + { + if (!isset($this->singletons[$serviceName])) { + $this->singletons[$serviceName] = $factory(); + } + + return $this->singletons[$serviceName]; + } + + /** + * Clear all singleton instances + * + * @return void + */ + public function clearSingletons(): void + { + $this->singletons = []; + } +} diff --git a/tests/Fixtures/ComplexService.php b/tests/Fixtures/ComplexService.php new file mode 100644 index 0000000..7d82ab8 --- /dev/null +++ b/tests/Fixtures/ComplexService.php @@ -0,0 +1,30 @@ +id = uniqid('complex_', true); + $this->data = []; + } + + public function getId(): string + { + return $this->id; + } + + public function setData(array $data): void + { + $this->data = $data; + } + + public function getData(): array + { + return $this->data; + } +} diff --git a/tests/Fixtures/SimpleService.php b/tests/Fixtures/SimpleService.php new file mode 100644 index 0000000..7b18631 --- /dev/null +++ b/tests/Fixtures/SimpleService.php @@ -0,0 +1,18 @@ +id = uniqid('simple_', true); + } + + public function getId(): string + { + return $this->id; + } +} diff --git a/tests/ServiceTypeTest.php b/tests/ServiceTypeTest.php new file mode 100644 index 0000000..515dc92 --- /dev/null +++ b/tests/ServiceTypeTest.php @@ -0,0 +1,207 @@ +factory = new ServiceFactory(); + printf("\n\n🏭 Setting up new service type test"); + } + + /** + * @test + */ + public function testDefaultServiceTypeIsPrototype(): void + { + printf("\n\n🧪 Test: Default Service Type"); + + $this->factory->register('service', SimpleService::class); + printf("\n ✅ Registered service"); + + $this->assertTrue($this->factory->isPrototype('service')); + $this->assertFalse($this->factory->isSingleton('service')); + printf("\n ℹ️ Service is prototype by default"); + } + + /** + * @test + */ + public function testRegisterAsSingleton(): void + { + printf("\n\n🧪 Test: Singleton Registration"); + + $this->factory->register('service', SimpleService::class); + printf("\n ✅ Registered service"); + + $this->factory->registerAsSingleton('service'); + printf("\n ✅ Converted to singleton"); + + $this->assertTrue($this->factory->isSingleton('service'), "Service should be singleton"); + $this->assertFalse($this->factory->isPrototype('service'), "Service should not be prototype"); + } + + /** + * @test + */ + public function testRegisterAsPrototype(): void + { + printf("\n\n🧪 Test: Prototype Registration"); + + $this->factory->register('service', SimpleService::class); + printf("\n ✅ Registered service"); + + $this->factory->registerAsSingleton('service'); + printf("\n ✅ Converted to singleton"); + + $this->factory->registerAsPrototype('service'); + printf("\n ✅ Converted back to prototype"); + + $this->assertTrue($this->factory->isPrototype('service'), "Service should be prototype"); + $this->assertFalse($this->factory->isSingleton('service'), "Service should not be singleton"); + } + + /** + * @test + */ + public function testSingletonReturnsSameInstance(): void + { + printf("\n\n🧪 Test: Singleton Instance Check"); + + $this->factory->register('service', SimpleService::class); + $this->factory->registerAsSingleton('service'); + printf("\n ✅ Registered singleton service"); + + $instance1 = $this->factory->create('service'); + printf("\n ✅ Created first instance (ID: %s)", $instance1->getId()); + + $instance2 = $this->factory->create('service'); + printf("\n ✅ Created second instance (ID: %s)", $instance2->getId()); + + $this->assertSame($instance1, $instance2, "Singleton should return same instance"); + printf("\n ℹ️ Both instances are identical"); + } + + /** + * @test + */ + public function testPrototypeReturnsNewInstance(): void + { + printf("\n\n🧪 Test: Prototype Instance Check"); + + $this->factory->register('service', SimpleService::class); + printf("\n ✅ Registered prototype service"); + + $instance1 = $this->factory->create('service'); + printf("\n ✅ Created first instance (ID: %s)", $instance1->getId()); + + $instance2 = $this->factory->create('service'); + printf("\n ✅ Created second instance (ID: %s)", $instance2->getId()); + + $this->assertNotSame($instance1, $instance2, "Prototype should return different instances"); + printf("\n ℹ️ Instances are different as expected"); + } + + /** + * @test + */ + public function testChangeServiceType(): void + { + printf("\n\n🧪 Test: Service Type Change"); + + $this->factory->register('service', SimpleService::class); + printf("\n ✅ Registered service"); + + // Change to singleton + $this->factory->changeServiceType('service', 'singleton'); + printf("\n ✅ Changed to singleton"); + $this->assertTrue($this->factory->isSingleton('service'), "Service should be singleton after change"); + + // Get instances + $instance1 = $this->factory->create('service'); + $instance2 = $this->factory->create('service'); + $this->assertSame($instance1, $instance2, "Singleton should return same instance"); + printf("\n ℹ️ Singleton instances are identical (ID: %s)", $instance1->getId()); + + // Change to prototype + $this->factory->changeServiceType('service', 'prototype'); + printf("\n ✅ Changed to prototype"); + $this->assertTrue($this->factory->isPrototype('service'), "Service should be prototype after change"); + + // Get new instances + $instance3 = $this->factory->create('service'); + $instance4 = $this->factory->create('service'); + $this->assertNotSame($instance3, $instance4, "Prototype should return different instances"); + printf("\n ℹ️ Prototype instances are different (IDs: %s, %s)", + $instance3->getId(), + $instance4->getId() + ); + } + + /** + * @test + */ + public function testClearSingletons(): void + { + printf("\n\n🧪 Test: Clear Singletons"); + + // Register services + $this->factory->register('service1', SimpleService::class); + $this->factory->register('service2', ComplexService::class); + printf("\n ✅ Registered two services"); + + // Make them singletons + $this->factory->registerAsSingleton('service1'); + $this->factory->registerAsSingleton('service2'); + printf("\n ✅ Converted both to singletons"); + + // Create initial instances + $instance1 = $this->factory->create('service1'); + $instance2 = $this->factory->create('service2'); + printf("\n ℹ️ Created initial instances (IDs: %s, %s)", + $instance1->getId(), + $instance2->getId() + ); + + // Clear singletons + $this->factory->clearSingletons(); + printf("\n ✅ Cleared all singletons"); + + // Get new instances + $instance3 = $this->factory->create('service1'); + $instance4 = $this->factory->create('service2'); + + // Verify new instances + $this->assertNotSame($instance1, $instance3, "Should get new instance after clearing"); + $this->assertNotSame($instance2, $instance4, "Should get new instance after clearing"); + printf("\n ℹ️ New instances have different IDs: %s, %s", + $instance3->getId(), + $instance4->getId() + ); + } + + /** + * @test + */ + public function testInvalidServiceTypeThrowsException(): void + { + printf("\n\n🧪 Test: Invalid Service Type"); + + $this->factory->register('service', SimpleService::class); + printf("\n ✅ Registered service"); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid service type: invalid'); + + printf("\n ℹ️ Attempting to set invalid type..."); + $this->factory->changeServiceType('service', 'invalid'); + } +}