Retrieve or create a service. Returns mixed.
$service = $container->get(UserService::class);Resolve a class to a typed, non-null instance. Prefer this over get()
when you want the return type preserved:
$service = $container->make(UserService::class); // returns UserService, not mixedIf the class cannot be resolved, make() throws DependencyNotFoundException.
Pass constructor arguments by parameter name as a second argument. Supplied values override autowiring and defaults for the matching parameters (top level only), and the instance is always built fresh:
$report = $container->make(Report::class, ['month' => 3, 'format' => 'pdf']);
// Overrides an autowired dependency with an explicit instance
$service = $container->make(UserService::class, ['logger' => $myLogger]);This resolves parameters that could not be autowired otherwise (e.g. a scalar without a default). Overrides are per-call and never cached.
The id is resolved as make() without parameters resolves it: an alias is
followed, then a class-string binding, so the overrides go to the bound
concrete class:
$container->bind(CacheInterface::class, RedisCache::class);
$cache = $container->make(CacheInterface::class, ['host' => 'cache.local']); // a RedisCacheA closure or instance binding has no constructor to override, so it is not followed.
A key that names no constructor parameter throws
DependencyInvalidArgumentException, listing the parameters the class has and
suggesting the closest one:
$container->make(RedisCache::class, ['hots' => 'cache.local']);
// Unknown parameter(s) 'hots' passed to make() for 'RedisCache'.
// Its constructor takes: $host, $port
// Did you mean one of these?
// - hostAn absent variadic parameter receives no arguments, including scalar, untyped, class, interface, union and intersection types. Autowiring and ordinary global registrations do not add an element. This applies to constructors, injected methods and callables passed to resolve().
An explicit runtime override, named contextual binding, typed contextual binding or parameter #[Inject] selection supplies one value:
$container->get(EventBus::class); // No handlers unless explicitly configured.
$container->when(EventBus::class)->needs(Handler::class)->give($handler);
$container->make(EventBus::class, ['handlers' => $handler]);Values are passed as single arguments. An array supplied for an untyped variadic stays one array value; it is not spread into several arguments. Classes with variadic constructor parameters use runtime resolution, including after installing generated factories, so later contextual bindings still apply.
Like get(), but throws DependencyNotFoundException instead of returning
null when the id cannot be resolved:
$service = $container->getOrFail(UserService::class);Automatically inject dependencies into any callable based on its parameter type hints:
$result = $container->resolve(function (LoggerInterface $logger, CacheInterface $cache) {
$logger->info('Cache cleared');
return $cache->clear();
});Runtime parameters work here too — override callable arguments by name:
$container->resolve($handler, ['request' => $request]);Every form of callable works: a closure, [$object, 'method'],
[Foo::class, 'staticMethod'], 'Foo::staticMethod', a function name, or an
invokable object.
The parameter list is reflected once and reused. Anything with a name to key on
— a function, a Class::method, an invokable's __invoke — is described once
per signature, so two instances of a class share one description. A closure has
no such name, so it is keyed on the closure itself and released along with it;
building a fresh closure per call therefore re-reflects each time. Hoist it out
of the loop if that matters:
$handler = static fn (LoggerInterface $logger) => $logger->info('tick');
foreach ($jobs as $job) {
$container->resolve($handler); // described once, not once per job
}Two similar-looking questions with different answers:
has($id)— willget($id)resolve? This is the PSR-11 contract. Because the container autowires, it istruefor any instantiable class, whether or not you registered it.bound($id)— was this id explicitly registered, as a binding or a stored instance?
$container = new Container();
$container->has(Greeter::class); // true — autowirable
$container->bound(Greeter::class); // false — never registered
$container->has(LoggerInterface::class); // false — no binding, cannot autowire an interface
$container->has(AbstractThing::class); // false — not instantiable
$container->has('nonsense'); // false — not a classUse has() before get(). Use bound() when you care about registration
itself — that is what bindIf() and singletonIf() check.
The container implements ArrayAccess, so array syntax maps to the core methods:
$logger = $container[LoggerInterface::class]; // get()
isset($container[LoggerInterface::class]); // has()
$container['db'] = new PDO(/* ... */); // set()
unset($container['temp']); // remove()String offsets are service ids, including '0' and the empty string.
Integer offsets use their string value consistently for assignment, lookup,
isset() and unset(): $container[1] addresses the same id as
$container->get('1'). Other offset types throw TypeError. Append syntax
($container[] = $service) is rejected because a service needs an explicit id.
Whatever the container hands back for an id, it hands to a constructor parameter
typed as that id. There is one answer per id, not one for get() and another for
autowiring:
$container->set(TaxRates::class, new TaxRates(0.21));
$container->get(TaxRates::class); // the registered instance
$container->get(Invoicing::class)->rates; // the same registered instanceThat holds for every way of registering: a binding, singleton(), lazy(),
set(), factory(), extend(), an alias(), a
definition. Lifetime is still the registration's to decide — a
factory() is re-invoked per injection, a singleton() is not — and reading a
stored service freezes it whether the read was a get() or an injection.
Class-typed constructor parameters also honor concrete-class replacements such as
bind(Mailer::class, FakeMailer::class). A
lazy(Mailer::class, FakeMailer::class) registration defers the selected subclass.
A class-typed parameter uses its declared default only when no registration applies. Contextual bindings, stored values (including null), aliases, bindings, explicit lazy registrations, and services already owned by the container or a parent take precedence. An unregistered optional class keeps its default instead of being autowired. Scalar, untyped, and union defaults keep their existing behavior.
These cases deliberately do not follow the global registration:
- A contextual binding wins over the registration for the consumer it names.
That is narrower than "what this id holds", which is the point of
when(). - A
protect()ed closure is handed over rather than called, so a parameter typed as a class rejects it. Protected closures are for non-class ids.
By default, autowired services are transient: each get() builds a fresh
instance graph, so two resolutions do not share child instances.
To share a single instance, register it with
singleton() or the
#[Singleton] attribute.