βοΈ Introduction
In Laravel, it’s convenient to put business logic into controllers or models. But what should you do when the logic starts to grow? The right solution is to move it into dedicated service classes. This approach follows the Single Responsibility Principle (SRP) and helps make your code scalable and easy to test.
π§± Structure of a Service Class
Letβs take a look at a basic example of a service class responsible for creating an order:
app/
βββ Services/
β βββ OrderService.php
namespace App\Services; use App\Models\Order; use App\Models\User; use Illuminate\Support\Facades\DB; class OrderService { public function create(array $data, User $user): Order { return DB::transaction(function () use ($data, $user) { $order = new Order(); $order->user_id = $user->id; $order->total = $data['total']; $order->status = 'new'; $order->save(); // Here you could dispatch events, send emails, etc. return $order; }); } }
π Using It in a Controller
namespace App\Http\Controllers; use App\Http\Requests\OrderRequest; use App\Services\OrderService; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; class OrderController extends Controller { public function __construct( protected OrderService $orderService ) {} public function store(OrderRequest $request): JsonResponse { $order = $this->orderService->create($request->validated(), Auth::user()); return response()->json([ 'message' => 'Order created successfully.', 'order' => $order, ]); } }
β Why This Approach Rocks
- Cleaner architecture: Controllers become slim and act as entry points, not logic holders.
- Easy testing: Services can be tested in isolation with mocked dependencies.
- Flexibility: Services are easier to extend or modify without touching the controller.
- Reusability: You can call the same service from REST APIs, Artisan commands, or queued jobs.
π§ͺ How to Unit Test the Service
use App\Models\User; use App\Services\OrderService; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class OrderServiceTest extends TestCase { use RefreshDatabase; public function test_it_creates_an_order() { $user = User::factory()->create(); $data = ['total' => 1000]; $service = new OrderService(); $order = $service->create($data, $user); $this->assertDatabaseHas('orders', [ 'user_id' => $user->id, 'total' => 1000, ]); } }
π Conclusion
Service classes in Laravel arenβt overengineering β theyβre a smart and maintainable way to structure your business logic. By adopting this pattern, your Laravel applications become cleaner, more modular, and ready for future growth.