# Singleton Pattern

## Overview

The Singleton Pattern ensures a class has only one instance and provides a global point of access to it. In Laravel, this is typically achieved through the Service Container's singleton binding.

## Benefits

- **Single Instance**: Guarantees only one instance exists
- **Shared State**: State is shared across all consumers
- **Lazy Loading**: Instance created only when first needed
- **Memory Efficiency**: Avoids creating multiple instances

## Implementation in Laravel

Laravel's Service Container provides singleton binding through `$this->app->singleton()`.

## Basic Singleton Registration

**Location**: `app/Providers/AppServiceProvider.php`

```php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Core\Services\ValidationServices;
use App\Domains\Core\Company\Services\CompanyServices;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Simple singleton registration
        $this->app->singleton('validation.services', function () {
            return new ValidationServices();
        });

        // Singleton with class name as key
        $this->app->singleton(ValidationServices::class, function ($app) {
            return new ValidationServices();
        });

        // Singleton with dependencies
        $this->app->singleton(TransactionRecalculationService::class, function ($app) {
            return new TransactionRecalculationService(
                $app->make(SalesInvoiceServices::class)
            );
        });
    }
}
```

## Current Company Singleton

Used to maintain the current company context throughout a request.

```php
<?php

// Registration
$this->app->singleton('current_company', function ($app) {
    if (!Auth::check()) {
        return app(CompanyServices::class)->find(1);
    }

    return app(CompanyServices::class)->find(Auth::user()->company_id);
});

// Usage anywhere in the application
$company = app('current_company');
// Or using the helper function
$company = company();
```

## Company Settings Singleton with Caching

```php
<?php

$this->app->singleton('company_settings_data', function () {
    $companyId = app('current_company')->id;

    return Cache::rememberForever("cs.{$companyId}", function () {
        return CompanySetting::query()
            ->where('company_id', app('current_company')->id)
            ->first();
    });
});

// Helper function for easy access
function cs(string $key = null, $default = null)
{
    $settings = app('company_settings_data');

    if ($key === null) {
        return $settings;
    }

    return $settings?->{$key} ?? $default;
}
```

## Validation Services Singleton

**Location**: `app/Core/Services/ValidationServices.php`

```php
<?php

namespace App\Core\Services;

class ValidationServices
{
    protected array $rules = [];

    /**
     * Register a validation rule (stored in singleton instance)
     */
    public function registerRule(string $key, callable $rule): void
    {
        $this->rules[$key] = $rule;
    }

    /**
     * Get a validation rule
     */
    public function getRule(string $key): callable
    {
        if (!isset($this->rules[$key])) {
            throw new \Exception(__('No validation rule found for this key.'));
        }

        return $this->rules[$key];
    }

    /**
     * Execute a validation rule
     */
    public function validate(string $key, mixed $value, array $params = []): bool
    {
        $rule = $this->getRule($key);
        return $rule($value, $params);
    }

    /**
     * Check if a rule exists
     */
    public function hasRule(string $key): bool
    {
        return isset($this->rules[$key]);
    }
}
```

**Registration and Usage**:

```php
// Registration in AppServiceProvider
$this->app->singleton('validation.services', function () {
    return new ValidationServices();
});

// Facade for easy access
class Validation extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'validation.services';
    }
}

// Usage
Validation::registerRule('custom_email', function ($value) {
    return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
});

$isValid = Validation::validate('custom_email', 'test@example.com');
```

## Strategy Services as Singletons

```php
<?php

// Register valuation strategies as singletons
$this->app->singleton(FifoValuationService::class, function ($app) {
    return new FifoValuationService();
});

$this->app->singleton(LifoValuationService::class, function ($app) {
    return new LifoValuationService();
});

$this->app->singleton(WeightedAverageValuationService::class, function ($app) {
    return new WeightedAverageValuationService();
});

$this->app->singleton(CurrentPriceValuationService::class, function ($app) {
    return new CurrentPriceValuationService();
});

// Main valuation service using other singletons
$this->app->singleton(InventoryValuationService::class, function ($app) {
    return new InventoryValuationService(
        $app->make(FifoValuationService::class),
        $app->make(LifoValuationService::class),
        $app->make(WeightedAverageValuationService::class),
        $app->make(CurrentPriceValuationService::class)
    );
});
```

## Transaction Service Singleton

```php
<?php

namespace App\Domains\Accounting\Services;

class TransactionRecalculationService
{
    private array $pendingRecalculations = [];

    public function __construct(
        private SalesInvoiceServices $invoiceServices
    ) {}

    /**
     * Queue a transaction for recalculation
     */
    public function queueForRecalculation(int $transactionId): void
    {
        $this->pendingRecalculations[$transactionId] = true;
    }

    /**
     * Process all pending recalculations
     */
    public function processPending(): void
    {
        foreach (array_keys($this->pendingRecalculations) as $id) {
            $this->recalculate($id);
        }

        $this->pendingRecalculations = [];
    }

    /**
     * Recalculate a single transaction
     */
    private function recalculate(int $transactionId): void
    {
        $this->invoiceServices->recalculateBalances($transactionId);
    }
}

// Registration
$this->app->singleton(TransactionRecalculationService::class, function ($app) {
    return new TransactionRecalculationService(
        $app->make(SalesInvoiceServices::class)
    );
});
```

## Helper Functions with Singletons

**Location**: `app/Core/Helpers/helper.php`

```php
<?php

if (!function_exists('company')) {
    /**
     * Get the current company instance
     */
    function company()
    {
        return app('current_company');
    }
}

if (!function_exists('cs')) {
    /**
     * Get company setting value
     */
    function cs(string $key = null, $default = null)
    {
        $settings = app('company_settings_data');

        if ($key === null) {
            return $settings;
        }

        return $settings?->{$key} ?? $default;
    }
}

if (!function_exists('gs')) {
    /**
     * Get global setting value
     */
    function gs(string $key = null, $default = null)
    {
        static $settings = null;

        if ($settings === null) {
            $settings = app('global_settings_data');
        }

        if ($key === null) {
            return $settings;
        }

        return $settings?->{$key} ?? $default;
    }
}
```

## Singletons in the Project

| Name | Class/Key | Purpose |
|------|-----------|---------|
| `current_company` | - | Current company context |
| `company_settings_data` | - | Cached company settings |
| `validation.services` | `ValidationServices` | Custom validation rules |
| - | `FifoValuationService` | FIFO inventory valuation |
| - | `LifoValuationService` | LIFO inventory valuation |
| - | `WeightedAverageValuationService` | Weighted average valuation |
| - | `InventoryValuationService` | Valuation context service |
| - | `TransactionRecalculationService` | Transaction recalculation queue |

## Scoped Singletons

For request-scoped singletons (new instance per request):

```php
<?php

// Scoped singleton (new instance per request in Laravel Octane)
$this->app->scoped('request_context', function () {
    return new RequestContext();
});
```

## Testing Singletons

```php
<?php

namespace Tests\Unit;

use App\Core\Services\ValidationServices;
use Tests\TestCase;

class ValidationServicesTest extends TestCase
{
    public function test_singleton_returns_same_instance(): void
    {
        $instance1 = app(ValidationServices::class);
        $instance2 = app(ValidationServices::class);

        $this->assertSame($instance1, $instance2);
    }

    public function test_registered_rules_persist(): void
    {
        $service = app(ValidationServices::class);

        $service->registerRule('test_rule', fn($value) => $value === 'valid');

        // Same instance should have the rule
        $this->assertTrue(
            app(ValidationServices::class)->hasRule('test_rule')
        );
    }

    public function test_can_replace_singleton_in_tests(): void
    {
        $mock = $this->mock(ValidationServices::class);
        $mock->shouldReceive('validate')->andReturn(true);

        $this->app->instance(ValidationServices::class, $mock);

        $result = app(ValidationServices::class)->validate('any', 'value');

        $this->assertTrue($result);
    }
}
```

## Best Practices

### Do

- Use singletons for stateless services
- Use singletons for expensive-to-create objects
- Use singletons for shared configuration/settings
- Register singletons in service providers
- Consider thread safety for Laravel Octane

### Don't

- Store request-specific data in singletons
- Create singletons with heavy constructor logic
- Use singletons for objects that need different states
- Forget to handle state cleanup between requests
- Create hidden dependencies through singletons

## Related Patterns

- [Service Pattern](SERVICE_PATTERN.md) - Services often registered as singletons
- [Strategy Pattern](STRATEGY_PATTERN.md) - Strategies registered as singletons
