جمعه, 29 خرداد 1405

لایه سرویس در لاراول

...

” اگه controllerهات پر شدن از کارهای اضافه و نمی‌دونی این مسئولیت‌ها رو کجا باید انجام بدی، وقتشه با Service Layer آشنا بشی. توی این مقاله به‌صورت ساده و عملی یاد می‌گیری چطور کنترلر رو خلوت کنی و کدت رو مرتب‌تر از قبل بنویسی. “

توی مقاله قبلی درباره ریپازیتوری توی لاراول حرف زدیم و فهمیدیم که چطور می‌تونیم منطق مربوط به دیتابیس رو از کنترلر جدا کنیم و ببریمش توی یه لایه میانی به اسم Repository.

اونجا گفتیم که:

کنترلر فقط مسئول گرفتن درخواست و برگردوندن جوابه. اینکه داده‌ها چطور به دست بیان، به ریپازیتوری واگذار میشه.

اما خب، یه سوال پیش میاد: همه منطق پروژه از جنس کار با دیتابیس نیست!

تصور کن یه سناریوی واقعی داری:

  • کاربر ثبت‌نام میکنه

  • باید یه ایمیل خوش‌آمدگویی براش ارسال بشه

  • یه پروفایل پیش‌فرض براش ساخته بشه

  • یه لاگ از ثبت‌نام ذخیره بشه

  • (و احتمالاً) یه نوتیفیکیشن به ادمین سایت بره

حالا این‌همه کار رو کجا باید نوشت؟ توی کنترلر؟ قطعاً نه. توی ریپازیتوری؟ اون که فقط کارش داده‌ست.

اینجاست که Service Layer وارد میشه.


Service Layer دقیقاً چیه؟

به زبان ساده، سرویس یه لایه میانی دیگه‌ست که منطق بیزینسی پروژه توش قرار میگیره.

منطق بیزینسی (Business Logic) یعنی تصمیم‌گیری‌هایی که برنامه باید انجام بده تا یه فرآیند رو کامل کنه. مثلاً: «وقتی کاربر ثبت‌نام کرد، ایمیل بفرست و پروفایل بساز». این تصمیم‌ها نه از جنس دیتابیس هستن (که برن توی ریپازیتوری)، نه از جنس نمایش (که برن توی View) و نه از جنس دریافت/پاسخ (که برن توی کنترلر).

پس نقش‌ها اینطوری تقسیم میشن:

  • کنترلر: دریافت درخواست، صدا زدن سرویس، برگردوندن پاسخ

  • سرویس: انجام منطق بیزینسی، هماهنگ‌سازی بین بخش‌های مختلف

  • ریپازیتوری: کار با دیتابیس، خوندن و نوشتن داده‌ها


بیا با یه مثال واقعی پیش بریم

می‌خوایم فرآیند «ثبت‌نام کاربر» رو پیاده‌سازی کنیم. اول نشونت میدم که خیلی از ما چطور می‌نویسیمش، بعد قدم به قدم می‌بریمش توی Service Layer.


مرحله ۰: کدی که معمولاً می‌نویسیم (و چرا مشکل داره)

یه نگاه به این کنترلر بنداز. شاید برات آشنا باشه:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Profile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;

class RegisterController extends Controller
{
    public function register(Request $request)
    {
        // ۱. اعتبارسنجی ورودی
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ]);

        // ۲. ساخت کاربر توی دیتابیس
        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        // ۳. ساخت پروفایل پیش‌فرض
        Profile::create([
            'user_id' => $user->id,
            'bio' => null,
            'avatar' => null,
        ]);

        // ۴. ارسال ایمیل خوش‌آمدگویی
        Mail::to($user->email)->send(new WelcomeEmail($user));

        // ۵. لاگ کردن ثبت‌نام
        Log::info('کاربر جدید ثبت‌نام کرد', ['user_id' => $user->id]);

        // ۶. برگردوندن پاسخ
        return redirect()->route('dashboard')
            ->with('success', 'ثبت‌نام با موفقیت انجام شد!');
    }
}

این کد کار میکنه، ولی چندتا مشکل داره:

۱. کنترلر بیش از حد شلوغ شده. ۶ تا کار مختلف توی یه متد داریم.
۲. منطق بیزینسی پخش و پلا شده. اگه بخوایم ثبت‌نام رو از یه جای دیگه (مثلاً API) انجام بدیم، باید همه این منطق رو دوباره تکرار کنیم.
۳. تست کردنش سخته. چطور می‌خوای تست کنی که ایمیل ارسال میشه یا نه، وقتی همه‌چیز چسبیده به هم دیگه‌ست؟
۴. تغییر دادنش ریسکه. اگه یه روز بخوایم یه قدم اضافه کنیم (مثلاً ارسال پیامک)، باید دست توی کنترلر ببریم.

حالا بیا ببینیم چطور با Service Layer درستش می‌کنیم.


قدم ۱: ریپازیتوری‌ها رو بسازیم

قبل از اینکه بریم سراغ سرویس، بهتره کار با دیتابیس رو مثل همون مقاله قبلی ببریم توی ریپازیتوری. اینجوری سرویس‌مون تمیزتر میشه.

توی پروژه، یه پوشه به اسم Repositories توی مسیر app می‌سازیم (اگه از قبل نساختیم) و داخلش یه پوشه دیگه به اسم Contracts برای Interfaceها در نظر می‌گیریم.

Interface مربوط به کاربر

اول توی پوشه Contracts یه فایل می‌سازیم به اسم UserRepositoryInterface.php که توش قرارداد کار با کاربر رو مشخص کنیم:

<?php

namespace App\Repositories\Contracts;

interface UserRepositoryInterface
{
    public function create(array $data);
    public function findByEmail(string $email);
}

اینجا فقط داریم میگیم: «هر کلاسی که میخواد کار عملیات کاربر رو انجام بده، باید این دو تا متد رو داشته باشه.»

Interface مربوط به پروفایل

حالا یه فایل دیگه توی همون پوشه Contracts می‌سازیم به اسم ProfileRepositoryInterface.php:

<?php

namespace App\Repositories\Contracts;

interface ProfileRepositoryInterface
{
    public function createForUser(int $userId);
}

پیاده‌سازی ریپازیتوری کاربر

حالا که قراردادهامون رو نوشتیم، بریم سراغ پیاده‌سازی واقعی. توی پوشه Repositories (نه Contracts) یه فایل به اسم UserRepository.php می‌سازیم:

<?php

namespace App\Repositories;

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

class UserRepository implements UserRepositoryInterface
{
    protected $model;

    public function __construct(User $model)
    {
        $this->model = $model;
    }

    /**
     * ساخت کاربر جدید با داده‌های اعتبارسنجی‌شده
     */
    public function create(array $data): User
    {
        return $this->model->create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }

    /**
     * پیدا کردن کاربر بر اساس ایمیل
     */
    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
    }
}

پیاده‌سازی ریپازیتوری پروفایل

باز هم توی پوشه Repositories یه فایل دیگه به اسم ProfileRepository.php می‌سازیم:

<?php

namespace App\Repositories;

use App\Models\Profile;
use App\Repositories\Contracts\ProfileRepositoryInterface;

class ProfileRepository implements ProfileRepositoryInterface
{
    protected $model;

    public function __construct(Profile $model)
    {
        $this->model = $model;
    }

    /**
     * ساخت پروفایل پیش‌فرض برای یک کاربر
     */
    public function createForUser(int $userId): Profile
    {
        return $this->model->create([
            'user_id' => $userId,
            'bio' => null,
            'avatar' => null,
        ]);
    }
}

قدم ۲: یه سرویس برای ارسال ایمیل بسازیم

ارسال ایمیل هم خودش یه مسئولیت جداست. بیایم یه سرویس کوچیک براش درست کنیم.

برای منظم نگه داشتن پروژه، یه پوشه جدید توی مسیر app می‌سازیم به اسم Services. قراره همه سرویس‌هامون رو بذاریم اینجا.

توی این پوشه یه فایل می‌سازیم به اسم MailService.php:

<?php

namespace App\Services;

use App\Models\User;
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;

class MailService
{
    /**
     * ارسال ایمیل خوش‌آمدگویی به کاربر تازه ثبت‌نام‌کرده
     */
    public function sendWelcomeEmail(User $user): void
    {
        Mail::to($user->email)->send(new WelcomeEmail($user));
    }
}

اینجا مسئولیت ارسال ایمیل رو دادیم به یه کلاس جدا. حالا هرجای پروژه که خواستیم ایمیل خوش‌آمدگویی بفرستیم، می‌تونیم از این سرویس استفاده کنیم. دیگه لازم نیست کد ارسال ایمیل رو توی کنترلر یا جای دیگه تکرار کنیم.


قدم ۳: سرویس اصلی رو بسازیم – اینجا Service Layer شکل میگیره

حالا می‌رسیم به قسمت اصلی کار. سرویس‌هامون رو توی پوشه app/Services می‌سازیم. برای اینکه پروژه منظم‌تر بشه، می‌تونیم زیرپوشه‌هایی هم تویش بسازیم. مثلاً برای قسمت احراز هویت، یه پوشه به اسم Auth می‌سازیم و سرویس مربوط به ثبت‌نام رو می‌ذاریم اونجا.

توی این مسیر (app/Services/Auth) یه کلاس می‌سازیم به اسم RegisterService که کارش هماهنگ‌کردن همه قدم‌های ثبت‌نام هست:

<?php

namespace App\Services\Auth;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use App\Repositories\Contracts\ProfileRepositoryInterface;
use App\Services\MailService;
use Illuminate\Support\Facades\Log;

class RegisterService
{
    protected $userRepository;
    protected $profileRepository;
    protected $mailService;

    /**
     * توی سازنده، هرچیزی که سرویس نیاز داره رو تزریق می‌کنیم
     */
    public function __construct(
        UserRepositoryInterface $userRepository,
        ProfileRepositoryInterface $profileRepository,
        MailService $mailService
    ) {
        $this->userRepository = $userRepository;
        $this->profileRepository = $profileRepository;
        $this->mailService = $mailService;
    }

    /**
     * انجام کامل فرآیند ثبت‌نام
     *
     * این متد همه قدم‌های لازم رو به ترتیب انجام میده:
     * ۱. ساخت کاربر
     * ۲. ساخت پروفایل
     * ۳. ارسال ایمیل
     * ۴. لاگ کردن
     */
    public function register(array $validatedData): User
    {
        // ۱. ساخت کاربر توی دیتابیس
        $user = $this->userRepository->create($validatedData);

        // ۲. ساخت پروفایل پیش‌فرض برای کاربر
        $this->profileRepository->createForUser($user->id);

        // ۳. ارسال ایمیل خوش‌آمدگویی
        $this->mailService->sendWelcomeEmail($user);

        // ۴. ثبت رویداد در لاگ
        Log::info('کاربر جدید با موفقیت ثبت‌نام کرد', [
            'user_id' => $user->id,
            'email' => $user->email,
        ]);

        // ۵. برگردوندن کاربر ساخته‌شده
        return $user;
    }
}

نکته: دقت کن که سرویس، هیچ کاری مستقیم با دیتابیس انجام نمیده. از ریپازیتوری‌ها برای این کار استفاده میکنه. همچنین خودش مستقیماً ایمیل نمیفرسته، بلکه از MailService کمک میگیره. این یعنی هر کلاس فقط یه مسئولیت داره (Single Responsibility Principle). اگه فردا خواستیم به جای ایمیل، پیامک بفرستیم، فقط کافیه MailService رو با یه SmsService عوض کنیم. بقیه کد دست‌نخورده می‌مونه.


قدم ۴: معرفی کلاس‌ها به لاراول

برای اینکه لاراول بدونه موقع تزریق هر Interface باید کدوم کلاس رو بسازه، میریم توی فایل AppServiceProvider.php که از قبل توی پوشه app/Providers وجود داره و توی متد register تنظیمات رو انجام میدیم:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\Contracts\UserRepositoryInterface;
use App\Repositories\Contracts\ProfileRepositoryInterface;
use App\Repositories\UserRepository;
use App\Repositories\ProfileRepository;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind کردن ریپازیتوری‌ها
        $this->app->bind(
            UserRepositoryInterface::class,
            UserRepository::class
        );

        $this->app->bind(
            ProfileRepositoryInterface::class,
            ProfileRepository::class
        );

        // سرویس‌هایی مثل RegisterService و MailService
        // که Interface ندارن، خود لاراول به‌صورت خودکار
        // می‌تونه بسازتشون و نیازی به Bind دستی نداریم.
    }
}

نکته: سرویس‌هایی مثل RegisterService و MailService که Interface ندارن، لاراول به‌صورت خودکار می‌تونه از روی اسم کلاس، وابستگی‌هاش رو تشخیص بده و بسازتشون. به این قابلیت میگن Reflection یا Autowiring. پس تا وقتی که برای سرویست Interface نساختی، نیازی به Bind دستی نداری.


قدم ۵: حالا کنترلر چقدر تمیز شده!

بریم ببینیم کنترلرمون بعد از این همه تغییر چه شکلی شده. فایل RegisterController.php که توی پوشه app/Http/Controllers/Auth قرار داره رو باز می‌کنیم و تبدیلش می‌کنیم به این:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Services\Auth\RegisterService;
use Illuminate\Http\Request;

class RegisterController extends Controller
{
    protected $registerService;

    /**
     * فقط سرویس رو تزریق می‌کنیم، نه هیچ چیز دیگه‌ای
     */
    public function __construct(RegisterService $registerService)
    {
        $this->registerService = $registerService;
    }

    /**
     * ثبت‌نام کاربر جدید
     */
    public function register(Request $request)
    {
        // ۱. اعتبارسنجی ورودی
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ]);

        // ۲. انجام ثبت‌نام (همه منطقش توی سرویس هست)
        $this->registerService->register($validated);

        // ۳. برگردوندن پاسخ مناسب
        return redirect()->route('dashboard')
            ->with('success', 'ثبت‌نام با موفقیت انجام شد!');
    }
}

فقط سه کار انجام میده:

۱. اعتبارسنجی ورودی (که اینم می‌تونیم بعداً با Form Request تمیزترش کنیم)
۲. صدا زدن سرویس
۳. برگردوندن پاسخ

همین! کنترلر دیگه نه ایمیل میفرسته، نه پروفایل میسازه، نه لاگ میکنه. فقط درخواست میگیره و جواب میده. دقیقاً همون کاری که از اول گفتیم وظیفه‌شه.


چرا Service Layer مهمه؟

Service Layer کمک میکنه:

۱. کنترلر تمیز بمونه. کنترلر فقط درخواست میگیره و جواب میده.
۲. ریپازیتوری تمیز بمونه. ریپازیتوری فقط با دیتابیس کار میکنه.
۳. منطق بیزینسی یه جا جمع بشه. اگه فرآیند ثبت‌نام تغییر کنه (مثلاً بخوایم پیامک هم اضافه کنیم)، فقط کافیه بریم توی RegisterService و یه خط اضافه کنیم. هیچ جای دیگه‌ای رو دست نمیزنیم.
۴. تست کردن راحت‌تر بشه. وقتی هر بخش از کد مسئولیت مشخص خودش رو داره، می‌تونیم هر کدوم رو جداگانه بررسی و تست کنیم. مثلاً می‌تونیم مطمئن بشیم که RegisterService بدون نیاز به باز کردن مرورگر، کارش رو درست انجام میده.
۵. تیم‌ها بهتر کار کنن. وقتی ساختار مشخص باشه، هر کس میدونه کدش رو کجا بذاره.


یه نکته مهم

شاید با خودت بگی «اینهمه فایل و کلاس برای یه ثبت‌نام ساده؟» بله، برای پروژه کوچیک شاید زیادی به نظر بیاد. ولی وقتی پروژه‌ات ۵۰ تا کنترلر داره، هرکدوم با منطق خاص خودش، اونوقت ارزش این ساختار رو می‌فهمی. همونطور که توی مقاله ریپازیتوری گفتیم: پروژه کوچیک بدون اینا هم میشه، ولی هرچی پروژه بزرگ‌تر بشه، نیازش بیشتر حس میشه.


حرف آخر

Service Layer ادامه‌دهنده همون مسیریه که با Repository شروع کردیم. هر دوی این‌ها ابزارهایی هستن برای اینکه کدمون تمیزتر، قابل نگهداری‌تر و حرفه‌ای‌تر بشه.

توی این مقاله یاد گرفتیم که:

  • منطق بیزینسی رو از کنترلر و ریپازیتوری جدا کنیم

  • چطور چندتا ریپازیتوری و سرویس رو توی یه سرویس بزرگ‌تر هماهنگ کنیم

  • کنترلرمون رو به کمترین حد مسئولیت برسونیم

اگه جایی سوالی بود، نکته‌ای به ذهنت رسید، یا حتی اگه فکر می‌کنی یه جای کار رو میشه بهتر انجام داد، حتماً توی نظرات بهم بگو. همه با هم یاد میگیریم! 🙏

ارسال دیدگاه

دیدگاه و یا پرسش خود را برای ما ارسال کنید.

وارد شوید

برای ارسال دیدگاه یا پرسش خود ابتدا وارد سایت شوید

ورود یا ثبت نام

دیدگاه کاربران

هنوز دیدگاه یا پرسشی ایجاد نشده است :/

تازه‌ترین نوشته‌ها

دیدن همه

تجربه‌ها، دیدگاه‌ها و نکات الهام‌بخشی که با شما به اشتراک می‌گذاریم.