Service Container
TinyMVC's service container is a powerful tool for managing class dependencies and performing dependency injection.
Basic Usage
Binding Dependencies
// Bind interface to implementation
container()->bind(LoggerInterface::class, FileLogger::class);
// Bind with closure
container()->bind('cache', function() {
return new RedisCache(config('redis'));
});
// Singleton binding (same instance each time)
container()->singleton(Database::class, function() {
return new Database(config('database'));
});
Resolving Dependencies
// Get instance from container
$logger = container()->get(LoggerInterface::class);
// Using helper function
$cache = get('cache');
// Automatic resolution
$userService = get(UserService::class);
Helper Functions
// Check if binding exists
if (has(LoggerInterface::class)) {
// ...
}
// Bind interface to implementation
bind(LoggerInterface::class, FileLogger::class);
// Singleton binding
singleton(Database::class, function() {
return new Database(config('database'));
});
// Get instance
$db = get(Database::class);
// or, use app helper function
$db = app(Database::class);
Dependency Injection
The container automatically resolves class dependencies:
class UserController
{
public function __construct(
private UserRepository $users,
private LoggerInterface $logger
) {}
}
// Controller will be automatically resolved with dependencies
$controller = get(UserController::class);
Method Injection
// Call class method with dependency injection
$result = container()->call('UserController@store', [
'request' => $specificRequest // Specific parameter
]);
// Call closure with dependency injection
container()->call(function(UserRepository $users, Request $request) {
// ...
});
Aliases
// Create alias
container()->alias('db', Database::class);
// Resolve using alias
$db = get('db');
Practical Examples
Database Service Setup
// In service provider
singleton(Connection::class, function() {
return new PDOConnection(config('database'));
});
// In controller
class UserController
{
public function __construct(private Connection $db) {}
public function index()
{
$users = $this->db->query('SELECT * FROM users');
// ...
}
}
Custom Logger Binding
// Bind interface to environment-specific implementation
if (config('app.env') === 'production') {
bind(LoggerInterface::class, CloudLogger::class);
} else {
bind(LoggerInterface::class, FileLogger::class);
}
// Resolved automatically
class OrderService
{
public function __construct(private LoggerInterface $logger) {}
}
Best Practices:
- Use interfaces for important dependencies
- Register bindings in service providers
- Use singleton for services that should maintain state
- Prefer constructor injection over direct container access
Troubleshooting
# Common issues:
# - "Class not found" → Check binding exists and class is autoloadable
# - "Cannot instantiate interface" → Forgot to bind implementation
# - "Circular dependency" → Two classes depend on each other
# Debug bindings
dd(container()->debug());
# Check if binding exists
if (has(LoggerInterface::class)) {
// ...
}
Service Providers
Service providers are the central place of application bootstrapping. They allow you to register bindings, event listeners, middleware, and more in a structured way.
Creating Providers
Generate a new provider using the Spark CLI:
php spark make:provider AuthServiceProvider
This creates a new provider in app/Providers/AuthServiceProvider.php
:
<?php
namespace App\Providers;
use Spark\Container;
class AuthServiceProvider
{
public function register(Container $container)
{
// Register bindings here
}
public function boot(Container $container)
{
// Perform bootstrapping here
}
}
Registering Providers
Add providers to bootstrap/providers.php
:
return [
\App\Providers\AppServiceProvider::class,
\App\Providers\AuthServiceProvider::class,
\App\Providers\EventServiceProvider::class,
];
Provider Methods
register()
Used to bind services into the container:
public function register(Container $container)
{
$container->singleton(Auth::class, function(Container $container) {
return new Auth(
$container->get(Session::class),
User::class,
[
'cache_enabled' => false,
'guest_route' => 'admin.auth.login',
'logged_in_route' => 'admin.dashboard',
]
);
});
}
boot()
Called after all providers are registered:
public function boot(Container $container)
{
// Register event listeners
event([
'user.created' => [SendWelcomeEmail::class],
]);
// Define authorization abilities
gate([
'edit.profile' => function(User $user) {
return $user->isAdmin() || $user->isOwner();
}
]);
}
Common Use Cases
Database Bindings
public function register(Container $container)
{
$container->singleton(Connection::class, function() {
return new DatabaseConnection(config('database'));
});
}
Third-Party Integrations
public function register(Container $container)
{
$container->singleton(Mailer::class, function() {
return new SMTPMailer(config('mail'));
});
}
Custom Services
public function register(Container $container)
{
$container->singleton('geoip', function() {
return new GeoIPService(config('geoip'));
});
}
Best Practices
- Keep providers focused on a specific domain
- Use
register()
only for bindings - Perform initialization in
boot()
- Group related bindings in dedicated providers
- Load providers in order of dependency
Full Example
<?php
namespace App\Providers;
use Spark\Container;
use App\Services\Analytics;
use App\Listeners\TrackUserLogin;
class AnalyticsServiceProvider
{
public function register(Container $container)
{
$container->singleton(Analytics::class, function() {
return new Analytics(config('analytics.key'));
});
}
public function boot(Container $container)
{
event()->addListener('user.login', function($user) use ($container) {
$container->get(Analytics::class)->track('login', $user->id);
});
// Or using listener class
event()->addListener('user.login', TrackUserLogin::class);
}
}