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.
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:
composer require flytachi/winter-diStep 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.
<?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.
<?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.
$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.
<?php
class UserController
{
public function index(UserService $service): array
{
return $service->names();
}
}$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:
<?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
newfor 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
- Mental model — the two ideas everything else follows from
- Service providers — bind an interface to an implementation
- Injecting into properties —
#[Autowired]andcall() - Scanning & autodiscovery — auto-register annotated classes
- API reference — every container method