What is the Singleton Design Pattern?
The singleton design pattern in Laravel 11 ensures that a class has only one instance and provides a global point of access to that instance. This pattern is particularly useful when you need to manage shared resources like database connections, configuration managers, cache handlers, or logging services.
Key Benefits of Singleton Pattern
Implementing the singleton design pattern in Laravel 11 offers several advantages:
- Memory Efficiency: Only one instance is created and reused throughout the application
- Controlled Access: Provides a single point of control for the instance
- Lazy Initialization: The instance is created only when needed
- Global State Management: Maintains consistent state across the application
Prerequisites
Before implementing the singleton design pattern in Laravel 11, ensure you have:
- Laravel 11.x installed
- PHP 8.2 or higher
- Basic understanding of Laravel service container
- Familiarity with dependency injection
Method 1: Using Laravel Service Container (Recommended)
Step 1: Create the Singleton Class
First, create a new class that you want to use as a singleton. Create a file at app/Services/ConfigurationManager.php
:
<?php
namespace App\Services;
class ConfigurationManager
{
protected array $settings = [];
public function set(string $key, mixed $value): void
{
$this->settings[$key] = $value;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->settings[$key] ?? $default;
}
public function all(): array
{
return $this->settings;
}
public function has(string $key): bool
{
return isset($this->settings[$key]);
}
}
Step 2: Register as Singleton in Service Provider
To implement the singleton design pattern in Laravel 11, register your class in a service provider. Open or create app/Providers/AppServiceProvider.php
:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\ConfigurationManager;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// Register as singleton
$this->app->singleton(ConfigurationManager::class, function ($app) {
return new ConfigurationManager();
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
Step 3: Use the Singleton in Your Application
Now you can inject and use the singleton in your controllers. Create a controller at app/Http/Controllers/ConfigController.php
:
<?php
namespace App\Http\Controllers;
use App\Services\ConfigurationManager;
use Illuminate\Http\Request;
class ConfigController extends Controller
{
protected ConfigurationManager $configManager;
public function __construct(ConfigurationManager $configManager)
{
$this->configManager = $configManager;
}
public function store(Request $request)
{
$this->configManager->set('app_name', 'My Laravel App');
$this->configManager->set('version', '1.0.0');
return response()->json([
'message' => 'Configuration stored',
'settings' => $this->configManager->all()
]);
}
public function show()
{
return response()->json([
'settings' => $this->configManager->all()
]);
}
}
Method 2: Traditional Singleton Pattern Implementation
Creating a Classic Singleton Class
For the traditional singleton design pattern in Laravel 11, create a class at app/Services/DatabaseConnection.php
:
<?php
namespace App\Services;
class DatabaseConnection
{
private static ?DatabaseConnection $instance = null;
protected string $connectionString;
protected array $config = [];
// Private constructor prevents direct instantiation
private function __construct()
{
$this->connectionString = 'mysql:host=localhost;dbname=laravel';
$this->config = [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'laravel'),
];
}
// Prevent cloning of the instance
private function __clone() {}
// Prevent unserialization of the instance
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
public static function getInstance(): DatabaseConnection
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection(): string
{
return $this->connectionString;
}
public function getConfig(): array
{
return $this->config;
}
}
Using the Traditional Singleton
Create a controller at app/Http/Controllers/DatabaseController.php
to use this pattern:
<?php
namespace App\Http\Controllers;
use App\Services\DatabaseConnection;
class DatabaseController extends Controller
{
public function checkConnection()
{
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();
// Both variables reference the same instance
$isSame = ($db1 === $db2); // Returns true
return response()->json([
'is_singleton' => $isSame,
'connection' => $db1->getConnection(),
'config' => $db1->getConfig()
]);
}
}
Method 3: Using Singleton with Interfaces
Step 1: Create an Interface
Create an interface at app/Contracts/CacheManagerInterface.php
:
<?php
namespace App\Contracts;
interface CacheManagerInterface
{
public function put(string $key, mixed $value, int $ttl = 3600): bool;
public function get(string $key): mixed;
public function forget(string $key): bool;
public function flush(): bool;
}
Step 2: Implement the Interface
Create the implementation at app/Services/CacheManager.php
:
<?php
namespace App\Services;
use App\Contracts\CacheManagerInterface;
class CacheManager implements CacheManagerInterface
{
protected array $cache = [];
protected array $ttl = [];
public function put(string $key, mixed $value, int $ttl = 3600): bool
{
$this->cache[$key] = $value;
$this->ttl[$key] = time() + $ttl;
return true;
}
public function get(string $key): mixed
{
if (!$this->has($key)) {
return null;
}
if ($this->isExpired($key)) {
$this->forget($key);
return null;
}
return $this->cache[$key];
}
public function forget(string $key): bool
{
unset($this->cache[$key], $this->ttl[$key]);
return true;
}
public function flush(): bool
{
$this->cache = [];
$this->ttl = [];
return true;
}
protected function has(string $key): bool
{
return isset($this->cache[$key]);
}
protected function isExpired(string $key): bool
{
return isset($this->ttl[$key]) && $this->ttl[$key] < time();
}
}
Step 3: Bind Interface to Singleton
Update app/Providers/AppServiceProvider.php
:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\CacheManagerInterface;
use App\Services\CacheManager;
use App\Services\ConfigurationManager;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind interface to singleton implementation
$this->app->singleton(CacheManagerInterface::class, function ($app) {
return new CacheManager();
});
// Previous singleton registration
$this->app->singleton(ConfigurationManager::class, function ($app) {
return new ConfigurationManager();
});
}
public function boot(): void
{
//
}
}
Step 4: Use Interface Injection
Create a controller at app/Http/Controllers/CacheController.php
:
<?php
namespace App\Http\Controllers;
use App\Contracts\CacheManagerInterface;
use Illuminate\Http\Request;
class CacheController extends Controller
{
public function __construct(
protected CacheManagerInterface $cache
) {}
public function store(Request $request)
{
$key = $request->input('key');
$value = $request->input('value');
$ttl = $request->input('ttl', 3600);
$this->cache->put($key, $value, $ttl);
return response()->json([
'message' => 'Data cached successfully',
'key' => $key
]);
}
public function show(string $key)
{
$value = $this->cache->get($key);
return response()->json([
'key' => $key,
'value' => $value
]);
}
public function destroy(string $key)
{
$this->cache->forget($key);
return response()->json([
'message' => 'Cache cleared'
]);
}
}
Creating Routes for Testing
Add routes to routes/web.php
or routes/api.php
:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ConfigController;
use App\Http\Controllers\DatabaseController;
use App\Http\Controllers\CacheController;
// Configuration Manager Routes
Route::post('/config', [ConfigController::class, 'store']);
Route::get('/config', [ConfigController::class, 'show']);
// Database Connection Route
Route::get('/database/check', [DatabaseController::class, 'checkConnection']);
// Cache Manager Routes
Route::post('/cache', [CacheController::class, 'store']);
Route::get('/cache/{key}', [CacheController::class, 'show']);
Route::delete('/cache/{key}', [CacheController::class, 'destroy']);
Real-World Example: Logger Service
Creating a Logger Singleton
Let's create a practical logging service at app/Services/Logger.php
:
<?php
namespace App\Services;
class Logger
{
protected array $logs = [];
protected string $logFile;
public function __construct()
{
$this->logFile = storage_path('logs/app-singleton.log');
}
public function log(string $level, string $message, array $context = []): void
{
$timestamp = now()->toDateTimeString();
$logEntry = [
'timestamp' => $timestamp,
'level' => strtoupper($level),
'message' => $message,
'context' => $context
];
$this->logs[] = $logEntry;
// Write to file
$formatted = sprintf(
"[%s] %s: %s %s\n",
$timestamp,
strtoupper($level),
$message,
!empty($context) ? json_encode($context) : ''
);
file_put_contents($this->logFile, $formatted, FILE_APPEND);
}
public function info(string $message, array $context = []): void
{
$this->log('info', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('error', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('warning', $message, $context);
}
public function getLogs(): array
{
return $this->logs;
}
public function clearLogs(): void
{
$this->logs = [];
}
}
Register Logger as Singleton
Update app/Providers/AppServiceProvider.php
:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\Logger;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Logger::class, function ($app) {
return new Logger();
});
}
}
Using the Logger
Create a controller at app/Http/Controllers/LogController.php
:
<?php
namespace App\Http\Controllers;
use App\Services\Logger;
use Illuminate\Http\Request;
class LogController extends Controller
{
public function __construct(
protected Logger $logger
) {}
public function logActivity(Request $request)
{
$this->logger->info('User activity logged', [
'user_id' => auth()->id() ?? 'guest',
'action' => $request->input('action'),
'ip' => $request->ip()
]);
return response()->json([
'message' => 'Activity logged',
'all_logs' => $this->logger->getLogs()
]);
}
public function getLogs()
{
return response()->json([
'logs' => $this->logger->getLogs()
]);
}
}
Testing Your Singleton Implementation
Create a Feature Test
Create a test file at tests/Feature/SingletonTest.php
:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Services\ConfigurationManager;
use App\Contracts\CacheManagerInterface;
class SingletonTest extends TestCase
{
public function test_singleton_returns_same_instance(): void
{
$instance1 = app(ConfigurationManager::class);
$instance2 = app(ConfigurationManager::class);
$this->assertSame($instance1, $instance2);
}
public function test_singleton_maintains_state(): void
{
$config = app(ConfigurationManager::class);
$config->set('test_key', 'test_value');
$newInstance = app(ConfigurationManager::class);
$this->assertEquals('test_value', $newInstance->get('test_key'));
}
public function test_interface_bound_to_singleton(): void
{
$cache1 = app(CacheManagerInterface::class);
$cache2 = app(CacheManagerInterface::class);
$this->assertSame($cache1, $cache2);
}
public function test_singleton_cache_functionality(): void
{
$cache = app(CacheManagerInterface::class);
$cache->put('user_1', ['name' => 'John Doe'], 3600);
$retrieved = $cache->get('user_1');
$this->assertEquals(['name' => 'John Doe'], $retrieved);
}
}
Run the Tests
Execute the following command in your terminal:
php artisan test --filter=SingletonTest
Project Structure Overview
Here's the complete project structure for implementing the singleton design pattern in Laravel 11:
laravel-project/
├── app/
│ ├── Contracts/
│ │ └── CacheManagerInterface.php
│ ├── Http/
│ │ └── Controllers/
│ │ ├── CacheController.php
│ │ ├── ConfigController.php
│ │ ├── DatabaseController.php
│ │ └── LogController.php
│ ├── Providers/
│ │ └── AppServiceProvider.php
│ └── Services/
│ ├── CacheManager.php
│ ├── ConfigurationManager.php
│ ├── DatabaseConnection.php
│ └── Logger.php
├── routes/
│ ├── api.php
│ └── web.php
├── tests/
│ └── Feature/
│ └── SingletonTest.php
└── storage/
└── logs/
└── app-singleton.log
Best Practices for Singleton Pattern in Laravel 11
1. Use Service Container Over Classic Pattern
The singleton design pattern in Laravel 11 is best implemented using the service container rather than the traditional static getInstance() method. This approach provides better testability and follows Laravel's dependency injection principles.
2. Bind to Interfaces
Always bind your singleton implementations to interfaces. This allows for easier testing and flexibility to swap implementations:
$this->app->singleton(CacheManagerInterface::class, CacheManager::class);
3. Use Constructor Injection
Inject dependencies through constructors instead of using app() helper or facades:
public function __construct(ConfigurationManager $config)
{
$this->config = $config;
}
4. Avoid Singleton for Stateful Objects
Be cautious when using singletons for objects that maintain user-specific state, as this can lead to data leakage between requests in long-running processes.
Common Pitfalls and Solutions
Pitfall 1: Memory Leaks
Solution: Implement proper cleanup methods and clear data when no longer needed:
public function clear(): void
{
$this->data = [];
$this->cache = [];
}
Pitfall 2: Testing Difficulties
Solution: Use Laravel's container binding to mock singletons in tests:
public function test_example(): void
{
$mock = Mockery::mock(ConfigurationManager::class);
$this->app->instance(ConfigurationManager::class, $mock);
// Your test code
}
Pitfall 3: Thread Safety in Queue Jobs
Solution: Be aware that singletons persist across multiple queue jobs in the same worker process. Clear state between jobs if necessary.
Advanced: Singleton with Lazy Loading
Create a lazy-loading singleton at app/Services/HeavyService.php
:
<?php
namespace App\Services;
class HeavyService
{
protected ?array $heavyData = null;
public function getData(): array
{
if ($this->heavyData === null) {
$this->heavyData = $this->loadHeavyData();
}
return $this->heavyData;
}
protected function loadHeavyData(): array
{
// Simulate expensive operation
sleep(2);
return [
'data' => 'Loaded heavy data',
'timestamp' => now()->toDateTimeString()
];
}
}
Performance Considerations
When implementing the singleton design pattern in Laravel 11, consider these performance factors:
- Memory Usage: Singletons remain in memory for the entire request lifecycle
- Initialization Cost: Use lazy loading for expensive initialization
- Cache Awareness: Be mindful of data size in singleton instances
- Octane Compatibility: Ensure singletons are stateless when using Laravel Octane
Singleton vs Other Patterns
Singleton vs Facade
While Laravel facades provide static-like interfaces, the singleton design pattern in Laravel 11 offers more explicit dependency injection:
// Using Facade
use Illuminate\Support\Facades\Cache;
Cache::put('key', 'value');
// Using Singleton with Dependency Injection
public function __construct(CacheManagerInterface $cache)
{
$this->cache = $cache;
$this->cache->put('key', 'value');
}
Singleton vs Scoped Binding
Laravel also offers scoped bindings, which create one instance per request:
// Singleton - One instance for entire application
$this->app->singleton(ConfigManager::class);
// Scoped - One instance per request
$this->app->scoped(RequestContext::class);
Real-World Use Cases
Use Case 1: API Rate Limiter
Create a rate limiter service at app/Services/RateLimiter.php
:
<?php
namespace App\Services;
class RateLimiter
{
protected array $attempts = [];
protected int $maxAttempts = 60;
protected int $decayMinutes = 1;
public function tooManyAttempts(string $key): bool
{
$this->clearOldAttempts($key);
return count($this->attempts[$key] ?? []) >= $this->maxAttempts;
}
public function hit(string $key): int
{
$this->attempts[$key][] = time();
return count($this->attempts[$key]);
}
public function remainingAttempts(string $key): int
{
$this->clearOldAttempts($key);
return max(0, $this->maxAttempts - count($this->attempts[$key] ?? []));
}
protected function clearOldAttempts(string $key): void
{
if (!isset($this->attempts[$key])) {
return;
}
$cutoff = time() - ($this->decayMinutes * 60);
$this->attempts[$key] = array_filter(
$this->attempts[$key],
fn($timestamp) => $timestamp > $cutoff
);
}
public function clear(string $key): void
{
unset($this->attempts[$key]);
}
}
Use Case 2: Feature Flag Manager
Create a feature flag manager at app/Services/FeatureFlagManager.php
:
<?php
namespace App\Services;
class FeatureFlagManager
{
protected array $flags = [];
public function __construct()
{
$this->loadFlags();
}
protected function loadFlags(): void
{
$this->flags = [
'new_dashboard' => true,
'beta_features' => false,
'experimental_ui' => env('ENABLE_EXPERIMENTAL_UI', false),
];
}
public function isEnabled(string $flag): bool
{
return $this->flags[$flag] ?? false;
}
public function enable(string $flag): void
{
$this->flags[$flag] = true;
}
public function disable(string $flag): void
{
$this->flags[$flag] = false;
}
public function all(): array
{
return $this->flags;
}
}
Register Both Services
Update app/Providers/AppServiceProvider.php
:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\RateLimiter;
use App\Services\FeatureFlagManager;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(RateLimiter::class, function ($app) {
return new RateLimiter();
});
$this->app->singleton(FeatureFlagManager::class, function ($app) {
return new FeatureFlagManager();
});
}
}
Using Singleton with Middleware
Create middleware at app/Http/Middleware/CheckRateLimit.php
:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Services\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class CheckRateLimit
{
public function __construct(
protected RateLimiter $limiter
) {}
public function handle(Request $request, Closure $next): Response
{
$key = $this->resolveRequestKey($request);
if ($this->limiter->tooManyAttempts($key)) {
return response()->json([
'message' => 'Too many requests',
'retry_after' => 60
], 429);
}
$this->limiter->hit($key);
$response = $next($request);
return $response->withHeaders([
'X-RateLimit-Limit' => 60,
'X-RateLimit-Remaining' => $this->limiter->remainingAttempts($key),
]);
}
protected function resolveRequestKey(Request $request): string
{
return 'rate_limit:' . $request->ip();
}
}
Register Middleware
Add middleware to bootstrap/app.php
(Laravel 11 structure):
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\CheckRateLimit;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'rate.limit' => CheckRateLimit::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Debugging Singleton Instances
Create a Debug Helper
Create a helper at app/Helpers/SingletonDebugger.php
:
<?php
namespace App\Helpers;
class SingletonDebugger
{
public static function verifyIsSingleton(string $class): array
{
$instance1 = app($class);
$instance2 = app($class);
return [
'class' => $class,
'is_singleton' => $instance1 === $instance2,
'instance1_id' => spl_object_id($instance1),
'instance2_id' => spl_object_id($instance2),
'memory_usage' => memory_get_usage(true),
];
}
public static function listSingletons(): array
{
$bindings = app()->getBindings();
$singletons = [];
foreach ($bindings as $abstract => $concrete) {
if (app()->isShared($abstract)) {
$singletons[] = $abstract;
}
}
return $singletons;
}
}
Create Debug Controller
Create a controller at app/Http/Controllers/DebugController.php
:
<?php
namespace App\Http\Controllers;
use App\Helpers\SingletonDebugger;
use App\Services\ConfigurationManager;
use App\Services\Logger;
class DebugController extends Controller
{
public function verifySingletons()
{
return response()->json([
'config_manager' => SingletonDebugger::verifyIsSingleton(
ConfigurationManager::class
),
'logger' => SingletonDebugger::verifyIsSingleton(
Logger::class
),
'all_singletons' => SingletonDebugger::listSingletons(),
]);
}
}
Singleton Pattern with Events
Create Event-Driven Singleton
Create an event manager at app/Services/EventManager.php
:
<?php
namespace App\Services;
class EventManager
{
protected array $listeners = [];
protected array $events = [];
public function listen(string $event, callable $callback): void
{
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = $callback;
}
public function dispatch(string $event, array $data = []): void
{
$this->events[] = [
'event' => $event,
'data' => $data,
'timestamp' => now()->toDateTimeString()
];
if (!isset($this->listeners[$event])) {
return;
}
foreach ($this->listeners[$event] as $callback) {
call_user_func($callback, $data);
}
}
public function getEvents(): array
{
return $this->events;
}
public function getListeners(string $event): array
{
return $this->listeners[$event] ?? [];
}
}
Documentation and Comments
When implementing the singleton design pattern in Laravel 11, always document your singleton classes properly:
<?php
namespace App\Services;
/**
* Configuration Manager Singleton
*
* Manages application-wide configuration settings.
* This class is registered as a singleton in the service container.
*
* @package App\Services
* @singleton
*/
class ConfigurationManager
{
/**
* Configuration settings storage
*
* @var array
*/
protected array $settings = [];
/**
* Set a configuration value
*
* @param string $key Configuration key
* @param mixed $value Configuration value
* @return void
*/
public function set(string $key, mixed $value): void
{
$this->settings[$key] = $value;
}
}
Conclusion
The singleton design pattern in Laravel 11 is a powerful tool for managing shared resources and maintaining consistent state across your application. By leveraging Laravel's service container, you can implement singletons that are testable, maintainable, and follow best practices.
Key Takeaways
- Use Laravel's service container for singleton registration rather than traditional static methods
- Bind singletons to interfaces for better flexibility and testing
- Use constructor injection for dependency management
- Be aware of memory implications and state management in long-running processes
- Implement proper cleanup methods to avoid memory leaks
- Document your singleton classes clearly for team collaboration
Next Steps
Now that you understand the singleton design pattern in Laravel 11, consider exploring:
- Factory Pattern for object creation
- Repository Pattern for data access
- Observer Pattern for event-driven architecture
- Strategy Pattern for algorithm selection
- Dependency Injection best practices in Laravel
Additional Resources
- Laravel Documentation: Official service container documentation
- Design Patterns: Gang of Four design patterns book
- PHP The Right Way: Modern PHP best practices
- Laravel Best Practices: Community-driven Laravel conventions
Frequently Asked Questions
When should I use the singleton pattern in Laravel?
Use the singleton design pattern in Laravel 11 when you need to maintain a single instance of a class throughout the application lifecycle, such as for configuration managers, loggers, cache handlers, or connection pools.
Is the singleton pattern thread-safe in Laravel?
In traditional PHP-FPM environments, each request runs in isolation, so thread safety isn't a concern. However, with Laravel Octane or Swoole, you need to be careful about state management in singletons between requests.
Can I mock singletons in tests?
Yes, Laravel makes it easy to mock singletons in tests using the instance()
method or by binding mock implementations to the container during test setup.
What's the difference between singleton() and bind()?
The singleton()
method creates one instance that's reused for all subsequent requests, while bind()
creates a new instance every time the dependency is resolved.
Pro Tip: When implementing the singleton design pattern in Laravel 11, always consider whether your use case truly requires a singleton. Overuse of singletons can lead to hidden dependencies and make your code harder to test. Use them judiciously for shared resources that genuinely need to maintain state across the application.