DEV Community

A0mineTV
A0mineTV

Posted on

Dependency Injection and Conciseness with PHP 8 in Laravel 12

Laravel 12 leverages PHP 8’s constructor property promotion to make dependency injection (DI) more concise and readable. In this article, we’ll explore:

  1. What constructor property promotion is
  2. How Laravel 12’s service container resolves promoted properties
  3. A before-and-after comparison (Laravel 11 vs Laravel 12)
  4. Examples in controllers, services, and commands
  5. Best practices and pitfalls

1. What Is Constructor Property Promotion ?

In PHP 8, you can declare and initialize class properties directly in the constructor signature. Instead of writing:

<?php

namespace App\Http\Controllers;

use App\Services\UserService;
use Psr\Log\LoggerInterface;

class UserController extends Controller
{
    protected UserService      $service;
    protected LoggerInterface  $logger;

    public function __construct(UserService $service, LoggerInterface $logger)
    {
        $this->service = $service;
        $this->logger  = $logger;
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

You can collapse property declaration, parameter definition, and assignment into one line per dependency:

<?php

namespace App\Http\Controllers;

use App\Services\UserService;
use Psr\Log\LoggerInterface;

class UserController extends Controller
{
    public function __construct(
        protected UserService     $service,
        protected LoggerInterface $logger
    ) {
        // no boilerplate assignments
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Key benefits:

  • Boilerplate reduction: No need for separate property stubs and $this->… assignments.
  • Improved readability: Dependencies are declared, typed, and initialized right in the constructor.
  • Consistency: Encourages listing all required services in one place.

2. How Laravel 12 Resolves Promoted Properties

Laravel’s service container (IoC) automatically resolves constructor dependencies by inspecting type hints. In Laravel 12, nothing changes under the hood: whether you declare properties via traditional stubs or promote them in the constructor, Laravel will still attempt to resolve each parameter from the container.

// In a service provider or elsewhere, Laravel already bound UserService and LoggerInterface:

use App\Services\UserService;
use Psr\Log\LoggerInterface;

$this->app->bind(UserService::class, function ($app) {
    return new UserService(/* … */);
});

$this->app->bind(LoggerInterface::class, function ($app) {
    return $app->make(\Monolog\Logger::class);
});
Enter fullscreen mode Exit fullscreen mode

When you request UserController from the container (for example, via a route), Laravel:

  • Inspects the constructor signature
  • Sees UserService $service and LoggerInterface $logger
  • Resolves those instances automatically
  • Instantiates UserController, injecting $service and $logger into the promoted properties

All of that happens exactly the same whether or not you use property promotion. The difference is purely syntactic, significantly reducing boilerplate.


3. Before and After: Laravel 11 vs Laravel 12

Let’s compare how you would write a controller in Laravel 11 (PHP 7.4 / PHP 8 without promotions) vs Laravel 12 (PHP 8 + promotions).

3.1. Laravel 11 (Pre-PHP 8 Promotions)

<?php

namespace App\Http\Controllers;

use App\Services\ProductService;
use Illuminate\Config\Repository as Config;
use Psr\Log\LoggerInterface;

class ProductController extends Controller
{
    protected ProductService   $service;
    protected LoggerInterface $logger;
    protected Config          $config;

    public function __construct(
        ProductService $service,
        LoggerInterface $logger,
        Config $config
    ) {
        $this->service = $service;
        $this->logger  = $logger;
        $this->config  = $config;
    }

    public function show(int $id)
    {
        $product = $this->service->find($id);
        $this->logger->info("Showing product #{$id}");
        return view('products.show', compact('product'));
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Lines for property stubs and assignments: 7
  • Property types must match constructor parameters
  • Boilerplate: $this->… = $…;

3.2. Laravel 12 (PHP 8 Constructor Property Promotion)

<?php

namespace App\Http\Controllers;

use App\Services\ProductService;
use Illuminate\Config\Repository as Config;
use Psr\Log\LoggerInterface;

class ProductController extends Controller
{
    public function __construct(
        protected ProductService  $service,
        protected LoggerInterface $logger,
        protected Config         $config
    ) {
        // No boilerplate assignments
    }

    public function show(int $id)
    {
        $product = $this->service->find($id);
        $this->logger->info("Showing product #{$id}");
        return view('products.show', compact('product'));
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Lines for promoted properties: 3
  • No manual assignments
  • Cleaner, more concise

If your controller requires additional services down the line, you simply add another promoted argument without creating a separate property stub or assignment.


4. Examples in Laravel 12: Controllers, Services, and Commands

4.1. Service Class

Even custom services benefit from promotion:

<?php

namespace App\Services;

use App\Repositories\OrderRepository;
use Psr\Log\LoggerInterface;

class OrderReportService
{
    public function __construct(
        protected OrderRepository $orderRepo,
        protected LoggerInterface $logger
    ) {
        // The container injects these automatically
    }

    public function generateMonthlyReport(string $month): array
    {
        $orders = $this->orderRepo->getByMonth($month);
        $this->logger->info("Generating report for {$month}", ['count' => count($orders)]);
        // process data…
        return ['total' => count($orders), 'orders' => $orders];
    }
}
Enter fullscreen mode Exit fullscreen mode

No need to write:

protected OrderRepository $orderRepo;
protected LoggerInterface $logger;

public function __construct(OrderRepository $orderRepo, LoggerInterface $logger)
{
    $this->orderRepo = $orderRepo;
    $this->logger    = $logger;
}
Enter fullscreen mode Exit fullscreen mode

4.2. Artisan Command

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\OrderReportService;
use Illuminate\Support\Facades\Mail;

class SendMonthlyReport extends Command
{
    protected $signature   = 'report:send {month}';
    protected $description = 'Generate and email the monthly order report';

    public function __construct(
        protected OrderReportService $reportService,
        protected Mailer             $mailer
    ) {
        parent::__construct();
    }

    public function handle()
    {
        $month = $this->argument('month');
        $reportData = $this->reportService->generateMonthlyReport($month);

        $this->mailer->to('[email protected]')
            ->send(new \App\Mail\OrderReportMail($reportData));

        $this->info("Report for {$month} sent successfully.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Again, no property stubs or assignment lines—everything is declared directly in the constructor.

 4.3. Policy Class

Laravel 12’s built-in code generation (e.g., php artisan make:policy —model=Post) can also generate promoted properties:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function __construct(
        protected User $user,
        protected Post $post
    ) {
        // No boilerplate assignment
    }

    public function update(): bool
    {
        return $this->user->id === $this->post->user_id;
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Best Practices and Pitfalls

 5.1. Visibility

You must specify a visibility keyword (public, protected, or private). Omitting visibility is a syntax error:

public function __construct(
    UserService $service // ❌ Missing visibility modifier
) {}
Enter fullscreen mode Exit fullscreen mode

Usually, services and dependencies are declared as protected, but you can choose private in final classes or public if you want to access them externally (rarely recommended).

5.2. Typed Properties

Always use type hints. An untyped promotion produces a mixed property, which can weaken static analysis:

// ❌ Avoid
public function __construct(
    protected $service   // type is mixed
) {}

// ✅ Better
public function __construct(
    protected UserService $service
) {}
Enter fullscreen mode Exit fullscreen mode

 5.3. Too Many Dependencies

If a class constructor starts to look like:

public function __construct(
    protected AService $a,
    protected BService $b,
    protected CService $c,
    protected DService $d,
    protected EService $e
) {}
Enter fullscreen mode Exit fullscreen mode

… it might be violating the Single Responsibility Principle. Consider refactoring:

  • Group related logic into smaller classes.
  • Introduce a façade or manager service that itself injects the underlying services.

5.4. Readonly Properties (PHP 8.1+)

If a dependency should never change after construction, mark it as public readonly (PHP 8.1+). This enforces immutability:

class NotificationService
{
    public function __construct(
        public readonly Mailer $mailer,
        public readonly Logger $logger
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Now, any attempt to reassign $this->mailer or $this->logger after construction will throw an error.


6. Summary

Using PHP 8’s constructor property promotion in Laravel 12 helps you:

  • Eliminate boilerplate: No need for separate property stubs and manual assignments.
  • Increase readability: Dependencies are declared, typed, and initialized in one place.
  • Maintain consistency: All dependencies live in the constructor signature.

Whether you’re writing controllers, services, commands, or policies, adopting promoted properties keeps your Laravel 12 code concise and focused on business logic rather than repetitive setup. Just remember to:

  • Always declare types and visibility.
  • Avoid injecting too many dependencies—refactor if your constructor becomes unwieldy.
  • Consider readonly for truly immutable dependencies (PHP 8.1+).

Top comments (1)

Collapse
 
nothingimportant34 profile image
No Name

Good article even though it has nothing to do with Laravel