Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
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.
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.
<?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'));
}
}
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.
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:
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:
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.
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!
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)
);
});
}
}
Start Simple: If your service class has no dependencies and just handles simple logic, direct instantiation is fine.
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.
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.
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.
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