0

Container::make() vs app() helper vs constructor injection

Intermediate5 min read·lv-02-006
interview

Concept

Container::make(), the app() helper, and constructor injection are three ways to resolve dependencies from Laravel's IoC container, but they serve distinct purposes and carry different trade-offs. Understanding when to reach for each is a mark of senior Laravel engineering.

Constructor injection is the preferred approach for any class whose dependencies are fixed for the lifetime of that instance. When a controller, service, or command declares typed parameters in its constructor, the container's Illuminate\Container\Container::build() method inspects those types via ReflectionClass and resolves each one automatically. The resolved object is stored in the container's $resolved array and returned. This approach keeps your code framework-agnostic, is trivially testable (swap real implementations for fakes in your tests), and is the SOLID-compliant choice. Laravel resolves controller constructors through Illuminate\Routing\ControllerDispatcher::resolveClassMethodDependencies().

app(string $abstract) is a global function that calls Illuminate\Foundation\Application::make(), which ultimately delegates to Container::make(). It is useful in contexts where you genuinely cannot receive a dependency via injection — Blade macros, model static scopes, or framework bootstrap code. Using app() inside a service class is a service-locator anti-pattern: it hides dependencies, making the class harder to test and its contracts implicit. Reserve it for the outermost layer of bootstrap code or for glue code in service providers.

Container::make(string $abstract, array $parameters = []) is the explicit container API. It is what app() wraps and what service providers use internally. The second $parameters argument lets you pass primitive values that cannot be auto-resolved: app()->make(ReportGenerator::class, ['format' => 'pdf']). These are merged with the container's bindings during the build() call inside resolveNonClass(). The full resolution chain is: make() → check $this->bindings[$abstract] → if singleton and already resolved, return from $this->instances[$abstract] → otherwise call $this->build($concrete).

ApproachTestableCouples to frameworkBest for
Constructor injectionYes (just pass a mock)NoServices, controllers, jobs, commands
app() / app()->make()Harder (requires container setup)YesService providers, macros, legacy glue
Container::make() directlySame as app()YesInternal framework code, packages

A practical rule: if a class appears in your constructor signature, it is injectable. If you are writing code that is itself bootstrapping the container (inside a service provider's register()), use $this->app->make() or the binding callbacks — not constructor injection, because boot order is not guaranteed at that point.

Code Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Contracts\ReportFormatterContract;
use App\Repositories\OrderRepository;
use Illuminate\Container\Container;

// --- 1. Constructor injection (preferred) ---
class ReportService
{
    public function __construct(
        private readonly OrderRepository $orders,
        private readonly ReportFormatterContract $formatter,
    ) {}

    public function generate(int $year): string
    {
        return $this->formatter->format($this->orders->forYear($year));
    }
}

// --- 2. app() helper (service-locator, use sparingly) ---
// Acceptable inside a Blade macro registered during boot()
\Illuminate\Support\Facades\Blade::directive('currency', function (string $expression) {
    // Can't inject here — this is a closure registered outside DI resolution
    return "<?php echo app(\App\Services\CurrencyFormatter::class)->format($expression); ?>";
});

// --- 3. Container::make() with runtime primitives ---
// The container cannot auto-resolve 'format' (a string primitive), so pass it manually.
$generator = app()->make(\App\Services\ReportGenerator::class, [
    'format' => 'pdf',
    'locale' => 'en_GB',
]);

// --- 4. Inspecting the container internals ---
$container = app(); // Illuminate\Foundation\Application extends Container

// See all explicit bindings (closures + concrete class strings)
// $container->getBindings() returns the protected $bindings array
$bindings = $container->getBindings();

// Check if something is already resolved as a singleton
$isResolved = $container->resolved(\App\Services\ReportService::class);

// --- 5. Testing: swap real class with a fake using constructor injection ---
// No container needed — just instantiate with a mock:
$fakeOrders = new class extends OrderRepository {
    public function forYear(int $year): array { return []; }
};
$fakeFormatter = \Mockery::mock(ReportFormatterContract::class);
$service = new ReportService($fakeOrders, $fakeFormatter);

Interview Q&A

Q: What is the difference between app(), app()->make(), and constructor injection, and when should you use each?

app() with a class string is syntactic sugar over Container::make() — it resolves a binding from the container's $bindings or $instances arrays. Constructor injection is the container resolving dependencies on your behalf during build(), which uses ReflectionClass to read type hints. The key distinction is coupling: constructor injection does not reference the container at all, making the class framework-agnostic and trivially testable. app() and make() are service-locator calls that hide dependencies and require the full container to be bootstrapped before a test can run. Use constructor injection everywhere possible; use app() only in places where injection is structurally impossible, such as inside Blade directives, model boot methods, or framework-level macros.


Q: How does Container::make() decide whether to return a cached singleton or build a new instance?

Inside Illuminate\Container\Container::make(), the method first calls getAlias() to resolve any alias registered via alias(). It then checks $this->instances[$abstract] — if a value exists there (placed by instance() or by a first-time singleton() resolution), it is returned immediately. If not found in instances, make() resolves the concrete class via getConcrete(), calls build($concrete), fires resolving callbacks, and — if the binding was registered with singleton() or scoped() — stores the result in $this->instances[$abstract] before returning. Subsequent calls find the cached instance immediately without hitting build() again.


Q: How do you test a class that uses app() internally, and why is it harder than testing injected dependencies?

When a class calls app(SomeDependency::class) internally, the test must set up a real (or partially real) container, bind a fake implementation, and boot the application. With constructor injection you simply pass a mock directly: new MyService(mock(SomeDependency::class)). The container is never involved. For legacy code using app(), you can use Laravel's $this->app->instance(SomeDependency::class, $fake) inside a TestCase to swap the binding before the test runs. This works but adds setup overhead and couples your tests to Laravel's boot cycle. The long-term fix is to move the dependency to the constructor.