Lessons → Laravel Advanced → Lesson 7

Service Layer in Laravel

Laravel
⏱ 25 min read🔥 AdvancedNot completed

The Service Layer sits between controllers and repositories. Controllers handle HTTP (input, responses), repositories handle data access, and services handle business logic — things like sending emails, charging payments, or computing derived values.

Responsibility Split

LayerResponsibilityExample
ControllerHTTP input/outputValidate request, return JSON
ServiceBusiness logicRegister user, send welcome email
RepositoryData accessINSERT into users table

1. Folder Structure

Directory Layout
app/
├── Services/
│   └── UserService.php
├── Repositories/
│   ├── Contracts/
│   │   └── UserRepositoryInterface.php
│   └── Eloquent/
│       └── UserRepository.php
└── Http/
    └── Controllers/
        └── UserController.php

2. Create the Service Class

app/Services/UserService.php
<?php

namespace App\Services;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

class UserService
{
    public function __construct(
        private readonly UserRepositoryInterface $users
    ) {}

    public function register(array $data): User
    {
        $data['password'] = Hash::make($data['password']);

        $user = $this->users->create($data);

        Mail::to($user->email)->send(new WelcomeEmail($user));

        return $user;
    }

    public function updateProfile(int $userId, array $data): bool
    {
        if (isset($data['password'])) {
            $data['password'] = Hash::make($data['password']);
        }

        return $this->users->update($userId, $data);
    }

    public function deactivate(int $userId): bool
    {
        // Business rule: soft-delete and revoke tokens
        $user = $this->users->find($userId);

        if (!$user) {
            return false;
        }

        $user->tokens()->delete();

        return $this->users->delete($userId);
    }
}

3. Thin Controller

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\RegisterUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
    public function __construct(
        private readonly UserService $userService
    ) {}

    public function store(RegisterUserRequest $request): JsonResponse
    {
        $user = $this->userService->register($request->validated());

        return response()->json($user, 201);
    }

    public function update(UpdateUserRequest $request, int $id): JsonResponse
    {
        $this->userService->updateProfile($id, $request->validated());

        return response()->json(['message' => 'Profile updated']);
    }

    public function destroy(int $id): JsonResponse
    {
        $this->userService->deactivate($id);

        return response()->json(['message' => 'Account deactivated']);
    }
}

4. Form Requests for Validation

Keep validation in dedicated Form Request classes, not in the service:

app/Http/Requests/RegisterUserRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class RegisterUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'unique:users'],
            'password' => ['required', 'min:8', 'confirmed'],
        ];
    }
}

5. Service With Multiple Dependencies

Services can depend on other services or external clients:

app/Services/OrderService.php
<?php

namespace App\Services;

use App\Repositories\Contracts\OrderRepositoryInterface;
use App\Repositories\Contracts\ProductRepositoryInterface;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function __construct(
        private readonly OrderRepositoryInterface $orders,
        private readonly ProductRepositoryInterface $products,
        private readonly PaymentService $payments,
    ) {}

    public function placeOrder(int $userId, array $items): array
    {
        return DB::transaction(function () use ($userId, $items) {
            $total = 0;

            foreach ($items as $item) {
                $product = $this->products->find($item['product_id']);

                if ($product->stock < $item['quantity']) {
                    throw new \RuntimeException("Insufficient stock for {$product->name}");
                }

                $total += $product->price * $item['quantity'];
                $this->products->decrementStock($product->id, $item['quantity']);
            }

            $order = $this->orders->create([
                'user_id' => $userId,
                'total'   => $total,
                'status'  => 'pending',
            ]);

            $this->payments->charge($userId, $total, $order->id);

            return ['order_id' => $order->id, 'total' => $total];
        });
    }
}

6. Testing the Service in Isolation

tests/Unit/UserServiceTest.php
<?php

namespace Tests\Unit;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use App\Services\UserService;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
use Tests\TestCase;

class UserServiceTest extends TestCase
{
    public function test_register_creates_user_and_sends_welcome_email(): void
    {
        Mail::fake();

        $user = User::factory()->make(['id' => 1]);

        $repo = $this->mock(UserRepositoryInterface::class);
        $repo->shouldReceive('create')->once()->andReturn($user);

        $service = app(UserService::class);
        $result  = $service->register([
            'name'     => 'Alice',
            'email'    => 'alice@example.com',
            'password' => 'secret123',
        ]);

        $this->assertSame($user, $result);
        Mail::assertSent(WelcomeEmail::class, fn ($m) => $m->hasTo('alice@example.com'));
    }

    public function test_deactivate_returns_false_when_user_not_found(): void
    {
        $repo = $this->mock(UserRepositoryInterface::class);
        $repo->shouldReceive('find')->with(999)->andReturn(null);

        $service = app(UserService::class);

        $this->assertFalse($service->deactivate(999));
    }
}

Full Request Lifecycle

Request Flow
HTTP Request
    │
    ▼
Controller            ← validates input (FormRequest), returns HTTP response
    │
    ▼
Service               ← orchestrates business logic, sends emails, fires events
    │
    ▼
Repository            ← reads/writes the database
    │
    ▼
Database (MySQL)
💡
Keep services free of HTTP concerns. A service should never reference request(), response(), or redirect. That makes it reusable from console commands, queue jobs, and API endpoints alike.
🎉
Pattern complete! Repository + Service layers together give you clean, testable, and maintainable Laravel applications that scale with your team.
← Previous Lesson All Lessons →
🧠

Test your knowledge!

Take the Laravel quiz.

Take Quiz →