Package · di

Quickstart

From install to a fully autowired service in five minutes: you’ll define a small object graph, let the container build it from type hints, and call a controller method with its dependencies injected — without writing a single new.

Time ~5 minLevel BeginnerPrereqs PHP 8.4+

What you’ll build

A UserService that depends on a UserRepository, which in turn depends on a DatabaseConnection. You declare those dependencies as constructor parameters and the container assembles the whole chain from one make() call. Then you’ll invoke a controller method with call(), letting the container supply its arguments. The loop is: define → bootstrap → make → call.

Before you start

  • PHP ≥ 8.4 (check with php -v)

Install the package:

bash
composer require flytachi/winter-di

Step 1 — Define your services

Give each class a scope attribute and declare its dependencies as typed constructor parameters. #[Singleton] means one shared instance per process.

src/Service.php
<?php

use Flytachi\Winter\DI\Attribute\Singleton;

#[Singleton]
class DatabaseConnection
{
  public function query(string $sql): array
  {
      // pretend this hits a real database
      return [['id' => 1, 'name' => 'Ada'], ['id' => 2, 'name' => 'Linus']];
  }
}

#[Singleton]
class UserRepository
{
  // DatabaseConnection is autowired by its type
  public function __construct(private DatabaseConnection $db) {}

  public function all(): array
  {
      return $this->db->query('SELECT * FROM users');
  }
}

#[Singleton]
class UserService
{
  public function __construct(private UserRepository $repo) {}

  public function names(): array
  {
      return array_column($this->repo->all(), 'name');
  }
}

No attribute? Still works

A class without a scope attribute defaults to transient (a fresh instance each time) and is still autowired. The attribute just makes the lifetime explicit. See Scopes.

Step 2 — Bootstrap the container

Create the container once, then let the Scanner discover the annotated classes and register them automatically.

bootstrap.php
<?php

require 'vendor/autoload.php';

use Flytachi\Winter\DI\Container;
use Flytachi\Winter\DI\Scanner;
use Flytachi\Winter\DI\Collector\DICollector;

$container = Container::init();

Scanner::run(__DIR__ . '/src')
  ->collect(new DICollector($container))
  ->execute();

Step 3 — Resolve a service with make()

Ask the container for UserService. It reads the constructor, sees it needs a UserRepository, which needs a DatabaseConnection, and builds the entire chain for you.

php
$service = $container->make(UserService::class);

print_r($service->names());
// Array ( [0] => Ada [1] => Linus )

Because every class is #[Singleton], a second make(UserService::class) returns the very same instance — the graph is built once and cached.

Step 4 — Call a method with injection

Controllers and commands rarely construct their own dependencies. call() invokes a method and resolves its parameters from the container — even parameters the class itself doesn’t hold.

src/UserController.php
<?php

class UserController
{
  public function index(UserService $service): array
  {
      return $service->names();
  }
}
php
$names = $container->call([UserController::class, 'index']);

print_r($names);
// Array ( [0] => Ada [1] => Linus )

The container instantiated UserController, saw index() needs a UserService, resolved it (reusing the singleton), and invoked the method. If you see the two names, the whole graph was wired for you. 🎉

The whole thing

The complete, copy-paste version:

app.php
<?php

require 'vendor/autoload.php';

use Flytachi\Winter\DI\Container;
use Flytachi\Winter\DI\Attribute\Singleton;

#[Singleton]
class DatabaseConnection
{
  public function query(string $sql): array
  {
      return [['id' => 1, 'name' => 'Ada'], ['id' => 2, 'name' => 'Linus']];
  }
}

#[Singleton]
class UserRepository
{
  public function __construct(private DatabaseConnection $db) {}
  public function all(): array { return $this->db->query('SELECT * FROM users'); }
}

#[Singleton]
class UserService
{
  public function __construct(private UserRepository $repo) {}
  public function names(): array { return array_column($this->repo->all(), 'name'); }
}

class UserController
{
  public function index(UserService $service): array { return $service->names(); }
}

$container = Container::init();

print_r($container->call([UserController::class, 'index']));
// Array ( [0] => Ada [1] => Linus )

No Scanner needed here

This single-file example skips the Scanner step: make() and call() autowire concrete classes on demand even without registration. The Scanner matters once you split code into many files and want attribute scopes applied automatically — see Scanning & autodiscovery.

What just happened

  • You never called new for a wired service — the container read type hints and built the graph recursively.
  • #[Singleton] set the lifetime — each service is created once and reused.
  • make() resolved a class; call() resolved a method’s parameters and invoked it.
  • Dependencies flowed by type — the parameter’s declared class is the wiring.

Next steps