Laravel: When to Use Dependency Injection, Services and Static Methods
Founder of QuickAdminPanel
Sometimes we need to put application logic somewhere outside of Controllers or Models, it’s usually are so-called Services. But there are a few ways to use them – as static “helpers”, as objects, or with Dependency Injection. Let’s see when each one is appropriate.
The biggest problem I’ve seen in this topic – there are a lot of articles about HOW to use Dependency Injection and Services, but almost no explanation of WHY you should use it and WHEN it’s actually beneficial. So let’s dive into examples, with some theory along the way.
In this article, we will cover one reporting example with using different techniques to move code from Controller to Service:
- First Way: From Controller to Static Service “Helper”
- Second Way: Create Service Object with Non-Static Method
- Third Way: Service Object with a Parameter
- Fourth Way: Dependency Injection – The Simple Case
- Fifth Way: Dependency Injection with Interface – Advanced Usage
Initial Example: Report in Controller
Let’s say we’re building a monthly report, something like this:
And if we put it all in Controller, it would look something like this:
// ... use statements class ClientReportController extends Controller { public function index(Request $request) { $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->orderBy('transaction_date', 'desc'); if ($request->has('project')) { $q->where('project_id', $request->project); } $transactions = $q->get(); $entries = []; foreach ($transactions as $row) { // ... Another 50 lines of code to fill in $entries by month } return view('report', compact('entries')); } }
Now, you see that DB query, and also hidden 50 lines of code – it’s probably too much for the Controller, so we need to store them somewhere, right?
First Way: From Controller to Static Service “Helper”
The most popular way to separate logic from the Controller is to create a separate class, usually called a Service. In other words, it could be called a “helper” or just simply a “function”.
Notice: Service classes are not part of Laravel itself, there’s no make:service Artisan command. It’s just a simple PHP class for calculations, and “service” is just a typical name for it.
So, we create a file app/Services/ReportService.php:
namespace App\Services; use App\Transaction; use Carbon\Carbon; class ReportService { public static function getTransactionReport(int $projectId = NULL) { $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->orderBy('transaction_date', 'desc'); if (!is_null($projectId)) { $q->where('project_id', $projectId); } $transactions = $q->get(); $entries = []; foreach ($transactions as $row) { // ... Same 50 lines of code copied from Controller to here } return $entries; } }
And now, we can just call that function from a Controller, like this:
// ... other use statements use App\Services\ReportService; class ClientReportController extends Controller { public function index(Request $request) { $entries = ReportService::getTransactionReport($request->input('project')); return view('report', compact('entries')); } }
That’s it, Controller now is much cleaner, right?
As you can see, we used static method and called it with :: syntax, so didn’t actually create an object for that Service class.
When to use this?
Usually, when you would easily replace that with a simple function, without class. It’s like a global helper, but sitting inside of ReportService class just for the sake of staying with object-oriented code, and to keep order – with namespaces and folders.
Also, keep in mind that static methods and classes are stateless. It means that the method is called only for that one time, and doesn’t save any data within that class itself.
But if you do want to keep some data inside that service…
Second Way: Create Service Object with Non-Static Method
Another way to initiate that class is to make that method non-static, and create an object:
app/Services/ReportService.php:
class ReportService { // Just "public", but no "static" public function getTransactionReport(int $projectId = NULL) { // ... Absolutely the same code as in static version return $entries; } }
ClientReportController:
// ... other use statements use App\Services\ReportService; class ClientReportController extends Controller { public function index(Request $request) { $entries = (new ReportService())->getTransactionReport($request->input('project')); return view('report', compact('entries'); } }
Or, if you don’t like long one-liners:
$reportService = new ReportService(); $entries = $reportService->getTransactionReport($request->input('project'));
Doesn’t make much difference from the static method, right?
That’s because, for this simple case, it actually makes no difference.
But it is useful if you have a few methods inside the service and you want to “chain” them, calling immediately one after another, so every method would return the same service instance. You can watch my 8-minute video about it, or, as a simple example, look here:
class ReportService { private $year; public function setYear($year) { $this->year = $year; return $this; } public function getTransactionReport(int $projectId = NULL) { $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->whereYear('transaction_date', $this->year) ->orderBy('transaction_date', 'desc'); // ... Other code
And then in Controller, you do this:
public function index(Request $request) { $entries = (new ReportService()) ->setYear(2020) ->getTransactionReport($request->input('project')); // ... Other code
When to use this?
To be honest, in rare cases, mostly for chaining methods like in the example above.
If your Service doesn’t accept any parameters while creating its object new ReportService(), then just use static methods. You don’t need to create an object, at all.
Third Way: Service Object with a Parameter
But what if you want to create that service with a parameter? Like, you want to have a Yearly report and initiate that class, passing the actual $year, that would be applied to all the methods inside of that service.
app/Services/YearlyReportService.php:
class YearlyReportService { private $year; public function __construct(int $year) { $this->year = $year; } public function getTransactionReport(int $projectId = NULL) { // Notice the ->whereYear('transaction_date', $this->year) $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->whereYear('transaction_date', $this->year) ->orderBy('transaction_date', 'desc'); $entries = []; foreach ($transactions as $row) { // ... Same 50 line of code } return $entries; } // Another report that uses the same $this->year public function getIncomeReport(int $projectId = NULL) { // Notice the ->whereYear('transaction_date', $this->year) $q = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->whereYear('transaction_date', $this->year) ->where('transaction_type', 'income') ->orderBy('transaction_date', 'desc'); $entries = []; // ... Some more logic return $entries; } }
Looks a bit more complicated, right?
But now, as a result of this, here’s what we can do in the Controller.
// ... other use statements use App\Services\YearlyReportService; class ClientReportController extends Controller { public function index(Request $request) { $year = $request->input('year', date('Y')); // default to current year $reportService = new YearlyReportService($year); $fullReport = $reportService->getTransactionReport($request->input('project')); $incomeReport = $reportService->getIncomeReport($request->input('project')); } }
In this example, both methods of the Service will use the same Year parameter that we passed while creating the object.
Now, it makes sense to actually create that object, instead of using static methods. Because now our service does have the actual state and depends on a year.
When to use this?
When your Service has a parameter and you want to create its object passing some parameter value that will be re-used when calling all Service’s methods for that Service object.
Fourth Way: Dependency Injection – The Simple Case
If you have a few methods in the Controller and want to re-use the same Service in all of them, you may also inject that in the Constructor of the Controller, as a type-hinted parameter, like this:
class ClientReportController extends Controller { private $reportService; public function __construct(ReportService $service) { $this->reportService = $service; } public function index(Request $request) { $entries = $this->reportService->getTransactionReport($request->input('project')); // ... } public function income(Request $request) { $entries = $this->reportService->getIncomeReport($request->input('project')); // ... } }
What is actually happening here?
- We’re creating a private property of the Controller called $reportService;
- We’re passing a parameter of ReportService type to the __construct() method;
- Inside the Constructor, we’re assigning that parameter to that private property;
- Then, in all our Controller, we can use $this->reportService and all its methods.
This is powered by a Laravel itself, so you don’t need to worry about actually creating that class object, you just need to pass the correct parameter type to the constructor.
When to use this?
When you have multiple methods in your Controller that want to use the same Service, and when Service doesn’t require any parameters (like $year in the example above). This way is just to save you time so you don’t have to do new ReportService() in each Controller method.
But wait, there’s more to that type-hinted injection – you can do that in any method, not only Controller. It’s called a method injection.
Like this:
class ClientReportController extends Controller { public function index(Request $request, ReportService $reportService) { $entries = $reportService->getTransactionReport($request->input('project')); // ... }
As you can see, no Constructor or private property needed, you just inject a type-hinted variable, and use it inside the method. Laravel creates that object “by magic”.
But, to be honest, for our exact example it’s not that useful, these injections come with writing even more code than just creating the service, right? So what is the actual advantage of using dependency injection?
Fifth Way: Dependency Injection with Interface – Advanced Usage
In the previous example, we were passing a parameter to the controller and Laravel “magically” resolved that parameter to create a Service object behind the scenes.
What if we could control that variable value? What if, for example, we could pass some Service for the testing phase, and another Service for real live usage?
For that, we will create an Interface and two classes of that Service that would implement that Interface. It’s like a contract – Interface would define the properties and methods that should exist in all the classes that implement that interface. Let’s build an example.
Remember those two Services in the example above ReportService and YearlyReportService? Let’s make them implement the same interface.
We create a new file app/Interfaces/ReportServiceInterface.php:
namespace App\Interfaces; interface ReportServiceInterface { public function getTransactionReport(int $projectId = NULL); }
And that’s it, we don’t need to do anything here – an interface is just a set of rules, without any “body” of the methods. So, here, we define that every class that implements that interface, must have that getTransactionReport() method.
Now, app/Services/ReportService.php:
use App\Interfaces\ReportServiceInterface; class ReportService implements ReportServiceInterface { public function getTransactionReport(int $projectId = NULL) { // ... same old code
Also, app/Services/YearlyReportService.php:
use App\Interfaces\ReportServiceInterface; class YearlyReportService implements ReportServiceInterface { private $year; public function __construct(int $year = NULL) { $this->year = $year; } public function getTransactionReport(int $projectId = NULL) { // Again, same old code with $year as a parameter
Now, the main part – which class do we type-hint into a Controller? ReportService or YearlyReportService?
In fact, we don’t type-hint a class anymore – we type-hint an interface.
use App\Interfaces\ReportServiceInterface; class ClientReportController extends Controller { private $reportService; public function __construct(ReportServiceInterface $reportService) { $this->reportService = $reportService; } public function index(Request $request) { $entries = $this->reportService->getTransactionReport($request->input('project')); // ... Same old code
The main part here is __construct(ReportServiceInterface $reportService). Now, we can attach and swap any class that implements that interface.
But, by default, we lose Laravel “magic injection”, because the framework doesn’t know which class to use. So if you leave it like this, you will get an error:
Illuminate\Contracts\Container\BindingResolutionException
Target [App\Interfaces\ReportServiceInterface] is not instantiable while building [App\Http\Controllers\Admin\ClientReportController].
And that’s very true, we didn’t say which class to instantiate.
We need to do it in app/Providers/AppServiceProvider.php in a register() method.
To make this example perfectly clear, let’s add an if-statement with a logic that if we have a local environment, we take ReportService, otherwise we need YearlyReportService.
use App\Interfaces\ReportServiceInterface; use App\Services\ReportService; use App\Services\YearlyReportService; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { if (app()->environment('local')) { $this->app->bind(ReportServiceInterface::class, function () { return new ReportService(); }); } else { $this->app->bind(ReportServiceInterface::class, function () { return new YearlyReportService(); }); } } }
See what’s happening here?
We are choosing which service to use, depending on where we are currently working – on our local computer, or on a live server.
When to use this?
The example above is probably the most common use for the Dependency Injection with Interfaces – when you need to swap your Service depending on some condition, and you can easily do this in a Service Provider.
Some more examples can be when you’re swapping your email provider or your payment provider. But then, of course, it’s important (and not easy) to make sure that both services implement the same interface.
Loooong article, right? Yes, because this topic is pretty complicated, and I wanted to explain it with pretty real-life examples, so you would understand not only how to use Dependency Injection and Services, but also WHY to use them and WHEN to use each case.
If you have any more questions or comments, use the form below!