Understanding Laravel Service Classes: A Comprehensive Guide

Delve into the concept of Service Classes, outlining their usage, the appropriate times to employ them, and the practices you should avoid when working with Services.

Laravel Pro Tips
6 min readJan 4, 2024

Understanding Service Classes in Laravel

Service Classes in Laravel are essentially PHP classes focused on managing specific business operations. Unlike other classes, they usually don’t inherit properties from others. You’ll often find them named with a “Service” suffix, like “UserService.”

Many times, when there’s a need for added logic linked to a particular Eloquent model, a Service Class comes into play. For instance, for the “User” model, you might have a “UserService.” There can also be Service Classes tailored to particular functions, like “PaymentService.”

Here are some simple examples to illustrate:

namespace App\Services;
class UserService {

public function store(array $userData): User {
// Logic to create and return a user.
}

}
namespace App\Services;
class CartService {

public function getFromCookie() {
// Logic to retrieve and return a cart.
}

}
namespace App\Services;
class PaymentService {

public function charge($amount) {
// Logic to process a user's charge.
}

}

In these examples, each Service Class has its own distinct purpose — managing user data, handling cart operations, or processing payments.

Understanding Service Classes vs. Models in Laravel

In Laravel, you might be torn between placing logic in a Service or directly in the Model, especially if it’s tied to an Eloquent Model.

Consider the size and complexity of your project. For larger projects, placing all the logic inside a Model can make it a lengthy and complex file. Instead, it’s better to think of a Model as a configuration layer over Eloquent. It should mainly deal with fields, database table structures, relationships, and perhaps some attribute tweaks using accessors and mutators.

Now, think of the terms ‘Model’ and ‘Service’. A ‘Model’ may feel static, like a statue, while ‘Service’ implies actions or operations.

Imagine an Order Model with associated Invoice logic. If you cram all that into the Model…

namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model {
protected $fillable = ['user_id', 'details', 'status'];
public function invoice() {
return $this->hasOne(Invoice::class);
}
public function pushStatus(int $status) {
$this->update(['status' => $status]);
// Perhaps more operations?
}
public function createInvoice() {
if ($this->invoice()->exists()) {
throw new \Exception('Order already has an invoice');
}

return DB::transaction(function() {
$invoice = $this->invoice()->create();
$this->pushStatus(2);
return $invoice;
});
}
}

For the controller:

namespace App\Http\Controllers\Api;
use App\Models\Order;
class InvoiceController extends Controller {
public function store(Order $order) {
try {
$invoice = $order->createInvoice();
} catch (\Exception $exception) {
return response()->json(['error' => $exception->getMessage()], 422);
}
return $invoice->invoice_number;
}
}

This might look okay for now. But as operations multiply (like notifications or shipping), the Model can become cluttered.

A cleaner approach? Extract this logic into a Service:

namespace App\Services;
use App\Models\Order;
class OrderService {
public function pushStatus(Order $order, int $status) {
$order->update(['status' => $status]);
// Potentially other operations?
}
public function createInvoice(Order $order) {
if ($order->invoice()->exists()) {
throw new \Exception('Order already has an invoice');
}

return DB::transaction(function() use ($order) {
$invoice = $order->invoice()->create();
$this->pushStatus($order, 2);
return $invoice;
});
}
}

In the updated controller:

namespace App\Http\Controllers\Api;
use App\Models\Order;
use App\Services\OrderService;
class InvoiceController extends Controller {
public function store(Order $order, OrderService $orderService) {
try {
$invoice = $orderService->createInvoice($order);
} catch (\Exception $exception) {
return response()->json(['error' => $exception->getMessage()], 422);
}
return $invoice->invoice_number;
}
}

By doing this, you’re ensuring that your code is more modular, maintainable, and organized.

Creating a Service Class in Laravel

While Laravel provides artisan commands for many tasks, it doesn’t have one for creating service classes. But don’t worry, creating one is straightforward.

  1. Set Up the Directory: Begin by setting up a directory to house your service classes. Typically, this directory is named Services and resides within the app folder.
  2. Create the Service Class: Inside the Services directory, you can manually create your PHP class for the service.

Using Service Classes in Controllers

Let’s explore two methods to call service classes from your controllers in Laravel.

Imagine we have a UserService class that contains a store method, designed to create and return a user:

namespace App\Services;
class UserService {
public function store(array $userData): User
{
// Code to Create User and return User.
}
}

Let’s also assume our controller has a store() method which gets triggered when the form's submit button is pressed.

Here, we instantiate the service class directly using the new keyword:

class UserController extends Controller
{
public function store(UserStoreRequest $request)
{
$userService = new UserService();
$userService->store($request->validated());
// More methods from the service can be called like this:
// $userService->anotherMethod();
return redirect()->route('users.index');
}
}

Laravel provides a feature called dependency injection, which can automatically generate objects of our service classes. By adding UserService as a parameter to the controller's method, Laravel will automatically resolve and provide an instance:

class UserController extends Controller
{
public function store(UserStoreRequest $request, UserService $userService)
{
$userService->store($request->validated());
return redirect()->route('users.index');
}
}

In this approach, our method is tidier since we don’t need to initialize the class within the function. This enhances readability and maintains a cleaner code structure.

Avoid Using Global Values in Service Classes

A Service Class should act like a “black box”. Here’s how it should ideally work:

  1. The controller provides specific inputs to the service.
  2. The service processes these inputs and performs the necessary actions.
  3. The service returns the result to the controller.

This means that your Service Class should not be aware of or work with global values like Auth, Session, or Request URL. It should also not send responses directly to the browser.

Here’s an illustration:

Consider a VoteService class:

class VoteService
{
public function store($question_id, $value): Vote
{
$question = Question::find($question_id);
abort_if(
$question->user_id == auth()->id(),
500,
'You are not allowed to vote on your own question'
);
//...
}
}

In this code, instead of terminating the process within the service using abort_if(), it’s better practice to throw an exception:

class VoteService
{
public function store($question_id, $value): Vote
{
$question = Question::find($question_id);
if ($question->user_id == auth()->id()) {
throw new \Exception('You are not allowed to vote on your own question');
}
//...
}
}

Then, you can catch this exception in your controller:

class VoteController extends Controller
{
public function voice(StoreVoteController $request, VoteService $service)
{
try {
$voice = $service->store($request->input('question_id'), $request->input('value'));
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
//...
}
}

In this way, the controller manages the workflow while the service class simply signals when there’s an issue by throwing an exception.

Additionally, avoid using global helpers like auth() within your service class. As mentioned, a service class should not be aware of global variables or sessions. Instead, pass necessary data, such as user IDs, as parameters. Here’s how you can improve the VoteService class:

class VoteService
{
public function store($question_id, $value, $user_id): Vote
{
$question = Question::find($question_id);
if ($question->user_id == $user_id) {
throw new \Exception('You are not allowed to vote on your own question');
}
//...
}
}
class VoteController extends Controller
{
public function voice(StoreVoteController $request, VoteService $service)
{
try {
$voice = $service->store($request->input('question_id'), $request->input('value'), auth()->id());
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
//...
}
}

With this approach, the service class stays ignorant of the larger application context and focuses on processing the data passed to it, maintaining a clean separation of concerns.

Make Your Service Classes Reusable

In Laravel, you don’t always have to invoke Service Classes from Controllers. You might use them in Artisan commands, unit tests, or elsewhere.

Imagine you have a CurrencyService to handle currency conversions:

class CurrencyService
{
const RATES = [
'usd' => [
'eur' => 0.98
]
];
public function convert(float $amount, string $fromCurrency, string $toCurrency): float
{
$conversionRate = self::RATES[$fromCurrency][$toCurrency] ?? 0;
return round($amount * $conversionRate, 2);
}
}

The beauty of such a service is its reusability. For instance, you can easily test it without interacting with a database or simulating browser/API requests. You just invoke the method, provide the required inputs, and verify the output.

Here’s how you might test the CurrencyService:

class CurrencyTest extends TestCase
{
public function testConversionFromUSDToEUR(): void
{
$amountInUSD = 100;
$expectedAmountInEUR = 98;

$convertedAmount = (new CurrencyService())->convert($amountInUSD, 'usd', 'eur');

$this->assertEquals($expectedAmountInEUR, $convertedAmount);
}
public function testConversionFromGBPToEUR(): void
{
$amountInGBP = 100;

// This will return 0 because we haven't provided a conversion rate from GBP to EUR in our service
$convertedAmount = (new CurrencyService())->convert($amountInGBP, 'gbp', 'eur');

$this->assertEquals(0, $convertedAmount);
}
}

By crafting your service classes to be independent and focused, you make them reusable and testable, leading to cleaner and more maintainable code.

Unlock the secrets to mastering Laravel and supercharge your development skills! This guide, crafted by seasoned experts, is your golden ticket to bypass common roadblocks and accelerate your career growth. With this treasure trove of Laravel wisdom, you’re not just buying an eBook; you’re investing in a leap forward. Two years of professional advancement await at your fingertips. Make the smart move — transform your Laravel expertise with just one click. Get Your Copy Now!

--

--