- Published on
Zero Configuration Resolution - under the hood.
- Authors
- Name
- Karani John
- @MKarani_
Laravel usually boasts of being able to do magic(its not hidden). The goal of laravel is to make the DevEx as good as possible. You can see it in the recent LaraconUs(2025) with what they have done with inertia. Pretty sleek. This will often involve lots of design pattern.
What is zero configuration resolution
This is when we don't need to tell laravel how to resolve a class i.e How we would like the class to be instantiated. This is usually common when a class has no dependencies or its dependecies are other concrete classes.
Concrete class - A class that can be instantiated. Other types of classes: abstract, interface
This "magic" usually happens in what we call the Service Container.
The service container.
The Laravel service container is a powerful tool for managing class dependencies and performing dependency injection - laravel docs
The service container is basically a fleshed out implementation of the PSR-11 Container Interface. This rose as a recommendation to ensure interoperability between different dependency containers. But why do we need it in the first place?
Imagine the problem below:
<?php
class PostRepository
{
public function all()
{
return ["Post 1", "Post 2", "Post 3", "Post 4"];
}
}
class PostService
{
protected PostRepository $repository;
public function __construct(PostRepository $repository)
{
$this->repository = $repository;
}
public function getPosts(): array
{
return array_map(fn($p) => strtoupper($p), $this->repository->all());
}
}
class PostController
{
protected PostService $service;
public function __construct(PostService $service)
{
$this->service = $service;
}
public function index()
{
return $this->service->getPosts();
}
}
// manual instantiation
$repo = new PostRepository();
$service = new PostService($repo);
$data = (new PostController($service))->index();
print_r($data);constructor injection - providing a class with its required dependencies in the constructor.
In the above problem, we need to do manual injection to each class which leads to tight coupling and a headache when writing tests. The PHP community saw this and decided, what if we could manage these dependencies from somewhere else centrally. This way we could test faster and better and also make changes.
What we do is create a central point to manage these dependecies so we can resolve them at runtime instead of having to figure out how to do it manually.
Below is a basic, bad written implementation of a service container implementing just the methods provided by the PSR-11 container interface:
class Container implements ContainerInterface
{
// The store for the entries, you can name it anything
private array $entries = [];
// Provided by the interface and checks if the identifier exits
public function has(string $id): bool
{
return isset($this->entries[$id]);
}
public function get(string $id)
{
if (!$this->has($id)) {
// if the identifier does not exist we throw this exception
// exception thrown implements the `Psr\Container\NotFoundExceptionInterface`
throw new NotFoundException('Could not find class ' . $id);
}
$entry = $this->entries[$id];
// for my case I am using a callable as the resolver
return $entry($this);
}
// You can decide to call this func whatever you want, no problem
}As you can see, our class implements the has and get method. This is what PSR-11 provides for in the interface. You need to decide how to implement the set method. Non Goals of PSR-11.
So we can implement our simple badly written set function.
public function set(string $id, callable $resolver)
{
$this->entries[$id] = $resolver;
}And then in new code we can adjust our classes:
class PostRepository
{
public function all()
{
return ["Post 1", "Post 2", "Post 3", "Post 4"];
}
}
class PostService
{
public function __construct(public PostRepository $repository)
{
}
public function getPosts(): array
{
return array_map(fn($p) => strtoupper($p), $this->repository->all());
}
}
class PostController
{
protected PostService $service;
public function __construct(public PostService $service)
{
}
public function index()
{
return $this->service->getPosts();
}
}We have removed the manual instantion from the specifc classes, but where do they go.
In the index.php, we can instruct the container how these classes will be instatiated. We choose to pass an identifier as the class itself and the key will be the closure.
$container = new Container();
$container->set(PostRepository::class, fn () => new PostRepository());
$container->set(PostService::class, function($c) {
return new PostService($c->get(PostRepository::class));
});
$container->set(PostController::class, function($c) {
return new PostController($c->get(PostService::class));
});
$controller = $container->get(PostController::class);
$controller->index();This is just a bad example but works. We have done what the PSR-11 recommends and have ourselves a simple container implementation. We don't need to do manual instantiation. While this looks the same, imagine having multiple files that need to reuse these classes, then we don't need to keep injecting them manually.
This is what laravel under the hood in its most basic form. In our example above we can see that these classes only depend on other concrete classes or like PostRepository has no dependencies. We have instructed the container still how to resolve these classes. Do we need to though? No.
Laravel boasts of the zero configuration resolution. Its just another word for auto wiring with a bit of spice.
Our aim should be to achieve something like this:
(new Container())->get(PostController::class)->index();This way we can let the container figure our how to resolve the PostController class. This is called autowiring. We make use of the Reflection API. This allows us to introspect classes(for our context) but we can also introspect other things too.
Let's refactor our container to support autowiring.
We need to find our way to resolve the the class given the id so instead of throwing an exception when we don't find a class we add a resolve method that uses reflection API to help resolve the classes in the get method. We are still adhering to the interface.
public function get(string $id)
{
// it has id we call the callable
if ($this->has($id)) {
$entry = $this->entries[$id];
return $entry($this);
}
// if not, we give the container a chance to resolve automatically
return $this->resolve($id);
}Now we can create a method to resolve this automatically which will receive the class identifier.
public function resolve(string $id){}We will use the Reflection API and some defensive programming to limit our auto resolution.
Goals
- Support only class that can be instatiated. (obviously)
- Support class with type hinted dependences(Except for union types)
Our first step will be to use the newReflection class which will report information about our class eg interfaces implemented, namespace, isEnum?, isTrait, etc.
Our concern here would be to check if the class is instantiable. If its not, we can throw a container exception provided for by PSR-11
$reflection = new ReflectionClass($id);
// The reflection API has ability to introspect other stuff like
// interfaces, abstract classes etc. Not our concern atm
if (!$reflection->isInstantiable()){
print_r($reflection);
throw new ContainerException('class is'. $id . 'is not instantiable');
}Next, we are now aware that the class is instantiable, we can check for the existence of a constructor if not, we return the new instance of the class.
$constructor = $reflection->getConstructor();
if(!$constructor){
return $reflection->newInstance();
}At this point we are aware of the existence of a constructor, we can check it has any parameters, if not we return the new instance.
$parameters = $constructor->getParameters();
if(!$parameters){
return $reflection->newInstance();
}If there any parameters, that means we need to resolve and build these dependecies. At the moment, we are supporting only type hinted parameters. If no type hint, how would we know what to do? Maybe its a class, maybe its not. Idk. For our basic implementation we use type hints as the god. Also we don't want to support union types cause its easier just to tell the container how to resolve stuff manually. Even for laravel.
We will do all these checks and then build up an array of dependencies through recursion(repeat for each the above) and then(if it passes), use newInstanceArgs to instantiate the class given the dependencies/arguments.
$dependencies = array_map(function(ReflectionParameter $parameter) use($id){
$name = $parameter->getName();
$type = $parameter->getType();
if(!$type){
throw new ContainerException("failed to resolve type of". $name."for class". $id);
}
if($type instanceof ReflectionUnionType){
throw new ContainerException('No support for union types yet in class'. $id. 'for parameter'.$name);
}
//
if($type instanceof ReflectionNamedType && !$type->isBuiltin()){
// we can try and resolve this here recursion
return $this->get($type->getName());
}
throw new ContainerException("Failed to resolve dependency".$name. "in class".$id). "due to invalid parameter";
},$parameters);
return $reflection->newInstanceArgs($dependencies);This should have helped us autowire our application to resolve the classes automatically. For laravel, the container has a bit more features.
In laravel, the magic happens in the following method:
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
$this->buildStack[] = spl_object_hash($concrete);
try {
return $concrete($this, $this->getLastParameterOverride());
} finally {
array_pop($this->buildStack);
}
}
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface or Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
$this->fireAfterResolvingAttributeCallbacks(
$reflector->getAttributes(), $instance = new $concrete
);
return $instance;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
$this->fireAfterResolvingAttributeCallbacks(
$reflector->getAttributes(), $instance = $reflector->newInstanceArgs($instances)
);
return $instance;
}Laravel's build method is what resolves our classes through zero configuration resolution. Its not magic as you can see. While its more production ready, when you zoom in, it works the same way as our simple autowiring example at its core. This is where the "magic" happens.
All the extra stuff like build stack tracking, event hooks, special exception wrapping, etc are there to make it better and production ready. But if you strip those features, it would work the same way.
Hope you enjoyed your read!.