GitHub

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);
    }
}