Skip to content

Commit

Permalink
Singleton/Prototype instances support.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmatosbc committed Jan 2, 2025
1 parent 8a206b2 commit 085a0fb
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 3 deletions.
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 53 additions & 2 deletions src/ServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -16,6 +19,10 @@
*/
class ServiceFactory
{
use ServiceTypeTrait;
use SingletonServiceTrait;
use PrototypeServiceTrait;

/** @var array<string, string> Service name to class name mapping */
private array $services = [];

Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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] ?? []));
}
Expand Down Expand Up @@ -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);
}
}
}
34 changes: 34 additions & 0 deletions src/Traits/PrototypeServiceTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Ananke\Traits;

use Ananke\Traits\ServiceTypeTrait;

trait PrototypeServiceTrait
{
use ServiceTypeTrait;

/**
* Register a service as prototype
*
* @param string $serviceName Name of the service
* @return void
*/
public function registerAsPrototype(string $serviceName): void
{
if (array_key_exists($serviceName, $this->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);
}
}
43 changes: 43 additions & 0 deletions src/Traits/ServiceTypeTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Ananke\Traits;

trait ServiceTypeTrait
{
/** @var array<string, string> 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';
}
}
59 changes: 59 additions & 0 deletions src/Traits/SingletonServiceTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Ananke\Traits;

trait SingletonServiceTrait
{
/** @var array<string, object> 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 = [];
}
}
30 changes: 30 additions & 0 deletions tests/Fixtures/ComplexService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Ananke\Tests\Fixtures;

class ComplexService
{
private string $id;
private array $data;

public function __construct()
{
$this->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;
}
}
18 changes: 18 additions & 0 deletions tests/Fixtures/SimpleService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Ananke\Tests\Fixtures;

class SimpleService
{
private string $id;

public function __construct()
{
$this->id = uniqid('simple_', true);
}

public function getId(): string
{
return $this->id;
}
}
Loading

0 comments on commit 085a0fb

Please sign in to comment.