Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

Service Classes and Dependency Injection in Laravel

If you've worked with Laravel, you've probably heard about "service classes" - but what are they really, and what's the best way to use them?

Service classes are not the same as service providers in Laravel. Service classes are simply regular PHP classes that hold your business logic. They're not built into the framework or documented in the official docs - they're just a pattern that many developers use to keep their code organized.

Implementation

First, let's create our service class. By convention, many developers put these in app/Services:

<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

class ImageHandler
{
    public function handle(UploadedFile $image): string
    {
        $path = 'images/' . $image->hashName();
        Storage::put($path, file_get_contents($image));
        return $path;
    }
}

Now let's look at three common ways to use this service class.

The Basic Approaches

1. Direct Instantiation

<?php

namespace App\Http\Controllers;

use App\Services\ImageHandler;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    public function store(Request $request)
    {
        $handler = new ImageHandler();
        $path = $handler->handle($request->file('image'));
    }
}

2. Static Methods

First, we'd need to modify our service class to use static methods:

<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

class ImageHandler
{
    public static function handle(UploadedFile $image): string
    {
        $path = 'images/' . $image->hashName();
        Storage::put($path, file_get_contents($image));
        return $path;
    }
}

Then we can use it in our controller:

<?php

namespace App\Http\Controllers;

use App\Services\ImageHandler;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    public function store(Request $request)
    {
        $path = ImageHandler::handle($request->file('image'));
    }
}

The major drawback of static methods becomes apparent when your service needs dependencies. With regular methods, you can add dependencies through the constructor:

class ImageHandler
{
    public function __construct(
        private Storage $storage,
        private Logger $logger
    ) {}

    public function handle(UploadedFile $image): string
    {
        $path = 'images/' . $image->hashName();
        $this->storage->put($path, file_get_contents($image));
        $this->logger->info('Image uploaded to: ' . $path);
        return $path;
    }
}

But with static methods, you can't do this. You'd have to resort to using facades or global functions:

class ImageHandler
{
    public static function handle(UploadedFile $image): string
    {
        $path = 'images/' . $image->hashName();
        // Have to use facades instead of injected dependencies
        Storage::put($path, file_get_contents($image));
        Log::info('Image uploaded to: ' . $path);
        return $path;
    }
}

This is why static methods are generally discouraged for business logic unless you're absolutely sure the functionality will always be completely standalone and never need external dependencies.

3. Constructor Injection

Constructor injection means that instead of creating service class instances ourselves, we declare them as constructor parameters and let Laravel handle the instantiation. Here's how it works:

<?php

namespace App\Http\Controllers;

use App\Services\ImageHandler;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    public function __construct(
        private ImageHandler $imageHandler  // Laravel sees this parameter
    ) {}                                   // and creates the ImageHandler for us

    public function store(Request $request)
    {
        $path = $this->imageHandler->handle($request->file('image'));
    }
}

What's happening behind the scenes:

  1. When Laravel creates your ArticleController, it looks at the constructor parameters
  2. It sees that you need an ImageHandler
  3. Laravel automatically creates an instance of ImageHandler for you
  4. That instance is stored in the private $imageHandler property
  5. You can then use $this->imageHandler anywhere in your controller

The real magic happens when your ImageHandler itself has dependencies:

<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Log\Logger;
use App\Services\NotificationService;

class ImageHandler
{
    public function __construct(
        private Storage $storage,
        private Logger $logger,
        private NotificationService $notifier
    ) {}

    public function handle(UploadedFile $image): string
    {
        $path = 'images/' . $image->hashName();
        $this->storage->put($path, file_get_contents($image));
        $this->logger->info('Image uploaded to: ' . $path);
        $this->notifier->send('New image uploaded');
        return $path;
    }
}

Laravel will:

  1. See that ArticleController needs ImageHandler
  2. Before creating ImageHandler, see that it needs Storage, Logger, and NotificationService
  3. Create those dependencies first
  4. Create the ImageHandler with those dependencies
  5. Finally, create the ArticleController with the fully configured ImageHandler

All of this happens automatically - you don't need to write any configuration code. This is why constructor injection is so powerful: you just declare what you need, and Laravel handles all the complexity of creating and connecting everything.

Why Constructor Injection Wins

At first glance, constructor injection might seem like overengineering. It requires more setup, and you might think "Why not just create the class when I need it?"

But here's where it gets interesting - constructor injection becomes powerful when your service classes need their own dependencies:

<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Log\Logger;
use App\Services\NotificationService;

class ImageHandler
{
    public function __construct(
        private Storage $storage,
        private Logger $logger,
        private NotificationService $notifier
    ) {}

    public function handle(UploadedFile $image): string
    {
        $path = 'images/' . $image->hashName();
        $this->storage->put($path, file_get_contents($image));
        $this->logger->info('Image uploaded to: ' . $path);
        $this->notifier->send('New image uploaded');
        return $path;
    }
}

With direct instantiation, you'd need to manually create all these dependencies:

$handler = new ImageHandler(
    Storage::disk('local'),
    app(Logger::class),
    new NotificationService()
);

With constructor injection, Laravel handles all of this for you automatically!

The Power of Service Providers

Here's where it gets even better. Using constructor injection sets you up for easy configuration changes through service providers. Want to change how your ImageHandler works? You don't need to touch any of your controllers.

<?php

namespace App\Providers;

use App\Services\ImageHandler;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Storage;
use Illuminate\Log\Logger;
use App\Services\NotificationService;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(ImageHandler::class, function ($app) {
            // Change implementation details here
            return new ImageHandler(
                Storage::disk('s3'),
                $app->make(Logger::class),
                $app->make(NotificationService::class)
            );
        });
    }
}

Best Practices

  1. Start Simple: If your service class has no dependencies and just handles simple logic, direct instantiation is fine.

  2. Use Constructor Injection By Default: It's easier to start with constructor injection than to refactor to it later. It gives you flexibility for future changes.

  3. Consider Future Dependencies: Even if your service is simple now, will it need database access, logging, or other services in the future? Constructor injection makes adding these easy.

  4. Use Service Providers Sparingly: Don't create service providers unless you need custom instantiation logic or interface bindings. Laravel's automatic dependency injection handles most cases.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Subscribe to my newsletter

Get new posts when I publish them.

I respect your privacy. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *