Lập trình · 22/09/2026

Laravel 13 Service Container and Dependency Injection

Lesson 15 — Service Container and Dependency Injection: separate the source of time from TaskFlow's overdue rule. Following lesson 14, we begin application organization. The goal is a useful substitution point, not an interface for every class.

Service Container và Dependency Injection trong Laravel 13

Lesson 15 — Service Container and Dependency Injection: separate the source of time from TaskFlow's overdue rule. Following lesson 14, we begin application organization. The goal is a useful substitution point, not an interface for every class.

1. Time is a dependency

A task is overdue when it is not done, has due_at and its deadline is strictly earlier than now. Equality is not overdue in this contract. Reading real time directly makes boundary tests fragile. Injecting Clock lets each test choose a fixed instant.

Dependency injection means receiving required collaborators instead of constructing all of them internally. The container assembles those objects. You can still call new TaskDeadline($clock) in ordinary PHP; DI does not require a framework.

2. Define the contract and service

Create these files, retaining Task's immutable_datetime cast and the example's UTC application timezone:

app/Contracts/Clock.php

<?php

namespace App\Contracts;

use Carbon\CarbonImmutable;

interface Clock
{
    public function now(): CarbonImmutable;
}

app/Support/SystemClock.php

<?php

namespace App\Support;

use App\Contracts\Clock;
use Carbon\CarbonImmutable;

class SystemClock implements Clock
{
    public function now(): CarbonImmutable
    {
        return CarbonImmutable::now('UTC');
    }
}

app/Services/TaskDeadline.php

<?php

namespace App\Services;

use App\Contracts\Clock;
use App\Models\Task;

class TaskDeadline
{
    public function __construct(private readonly Clock $clock) {}

    public function isOverdue(Task $task): bool
    {
        return $task->status !== 'done'
            && $task->due_at !== null
            && $task->due_at->lessThan($this->clock->now());
    }
}

Clock promises a CarbonImmutable result. SystemClock reads UTC on each now() call instead of freezing time in its constructor. TaskDeadline applies the rule without knowing whether time is real or fixed. It neither persists data nor sends notifications or authorizes users.

3. Bind the interface

Add imports and this register() implementation to AppServiceProvider, preserving boot():

use App\Contracts\Clock;
use App\Support\SystemClock;

public function register(): void
{
    $this->app->bind(Clock::class, SystemClock::class);
}

The skeleton already registers this provider in bootstrap/providers.php. The container can construct concrete TaskDeadline, but needs a selected implementation for Clock. Missing binding produces a resolution error; hiding new SystemClock inside the service would remove the substitution point rather than fix the design.

4. Resolve at boundaries and inject into business code

$service = app(\App\Services\TaskDeadline::class);
$overdue = $service->isOverdue($task);

This app() call demonstrates resolution and appears in our tests. A controller can type-hint TaskDeadline in its constructor or a framework-invoked method. Avoid scattering app(Clock::class) through business methods, which hides dependencies.

Concrete classes with resolvable constructors do not all need manual bindings. Primitive settings such as an API URL cannot be inferred merely from string type hints; provide explicit configuration or a binding factory. Never hardcode credentials in a provider.

5. Choose object lifetime deliberately

bind is sufficient here. singleton retains an instance in the container; scoped follows a framework-managed lifecycle, useful for workers handling multiple requests or jobs. A long-lived singleton holding a current user or Request may retain the previous operation's data.

SystemClock is stateless, but this example does not need instance-count optimization. Choose lifetime based on state and lifecycle rather than assuming singleton is always better. Consult the Service Container documentation for long-running environments.

6. Substitute the clock before resolving the service

tests/Feature/TaskDeadlineTest.php

<?php

namespace Tests\Feature;

use App\Contracts\Clock;
use App\Models\Task;
use App\Services\TaskDeadline;
use App\Support\SystemClock;
use Carbon\CarbonImmutable;
use Tests\TestCase;

class TaskDeadlineTest extends TestCase
{
    public function test_default_interface_binding_resolves(): void
    {
        $this->assertInstanceOf(SystemClock::class, app(Clock::class));
        $this->assertInstanceOf(TaskDeadline::class, app(TaskDeadline::class));
    }

    public function test_injected_clock_makes_deadline_boundaries_deterministic(): void
    {
        $this->app->instance(Clock::class, new class implements Clock {
            public function now(): CarbonImmutable
            {
                return CarbonImmutable::parse('2026-10-01 12:00:00', 'UTC');
            }
        });
        $service = app(TaskDeadline::class);
        $this->assertTrue($service->isOverdue(new Task(['status' => 'todo', 'due_at' => '2026-10-01 11:59:59'])));
        $this->assertFalse($service->isOverdue(new Task(['status' => 'todo', 'due_at' => '2026-10-01 12:00:00'])));
        $this->assertFalse($service->isOverdue(new Task(['status' => 'done', 'due_at' => '2026-10-01 11:00:00'])));
        $this->assertFalse($service->isOverdue(new Task(['status' => 'todo', 'due_at' => null])));
    }
}
php artisan test --filter=TaskDeadlineTest
php artisan test

instance() installs a fixed Clock before resolving TaskDeadline. An already constructed service retains its earlier dependency, so setup order matters. These cases use unsaved models and need no database.

The complete suite passed 37 tests with 126 assertions. Checks cover default binding plus overdue, equal-deadline, done and null-deadline cases. No sleep is needed to hit a time boundary; explicit time makes the result reproducible.

7. Exercise and boundaries

Add a future deadline and equivalent instants with different offsets. Construct the service directly with a fake clock to show that the rule does not depend on the container. Temporarily remove the binding, inspect the resolution error and restore it.

A Clock abstraction does not implement user timezone policy, business calendars, holidays or authorization. Avoid building unnecessary layers ahead of requirements. Navigation: Lesson 14 · Roadmap. Next comes Services, Actions and DTOs.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.