Loading
Alireza Shokrani

Digital Architect

AI Solutions Engineer

Founder @ CoreBiz ERP

GenAI & RAG Specialist

Full-Stack Systems Developer

Alireza Shokrani

Digital Architect

AI Solutions Engineer

Founder @ CoreBiz ERP

GenAI & RAG Specialist

Full-Stack Systems Developer

Solution

حذف بدهی معماری: پیاده‌سازی اصول SOLID و الگوهای Repository در PHP و Laravel مدرن

حذف بدهی معماری: پیاده‌سازی اصول SOLID و الگوهای Repository در PHP و Laravel مدرن

بیشتر نرم‌افزارها به این دلیل شکست نمی‌خورند که بد ساخته شده‌اند. آن‌ها شکست می‌خورند چون بدون نقشه‌ای برای تغییر ساخته شده‌اند.

الگو قابل پیش‌بینی است:

  • سال ۱: کدبیس تمیز است. ویژگی‌ها سریع منتشر می‌شوند. تیم مولد است.

  • سال ۲: نیازمندی‌های کسب‌وکار تکامل می‌یابند. کنترلرها رشد می‌کنند. Modelها مسئولیت‌ها را بر عهده می‌گیرند. تست‌ها شکننده می‌شوند.

  • سال ۳: توسعه‌دهندگان جدید هفته‌ها برای درک سیستم نیاز دارند. یک تغییر در صورتحساب، موجودی را می‌شکند. استقرارها خطرناک می‌شوند.

  • سال ۴: تیم از Refactoring اجتناب می‌کند چون «کار می‌کند». بدهی فنی انباشته می‌شود.

  • سال ۵: سیستم غیرقابل نگهداری است. تنها گزینه بازنویسی است — که بیشتر از ساخت اولیه هزینه دارد.

این‌ها بدهی معماری هستند — و اجتناب‌ناپذیر نیستند. آن‌ها نتیجه تصمیمات معماری خاص (یا فقدان آن‌ها) هستند که در سال ۱ گرفته شده‌اند.

این مقاله درباره دیسیپلین‌هایی است که آن‌ها را جلوگیری می‌کنند:

اصول SOLID، الگوهای Repository، Dependency Injection و تست‌های خودکار — به صورت عمل‌گرایانه در PHP و Laravel مدرن اعمال می‌شوند، جایی که واقعاً اهمیت دارند.

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

بخش ۱ — بدهی معماری واقعاً چیست

بدهی فنی یک استعاره است. بدهی معماری یک شرایط ساختاری است.

علائم

علامتعلت ریشه‌ای
تغییرات شکنندهجفت‌شدگی محکم — یک تغییر ویژگی‌های نامرتبط را می‌شکند
کد غیرقابل تستوابستگی‌های Hard-coded، بدون درز برای Mocking
منطق تکراریبدون منبع واحد حقیقت برای قوانین کسب‌وکار
Onboarding کندبدون مرزهای واضح بین نگرانی‌ها
ترس از Refactoringبدون پوشش تست برای گرفتن رگرسیون‌ها
قفل شدن به فریم‌ورکمنطق کسب‌وکار درهم‌تنیده با کد فریم‌ورک

علت ریشه‌ای: مرزهای نقض‌شده

بدهی معماری زمانی انباشته می‌شود که مسئولیت‌ها جدا نشوند:

کنترلر Laravel معمولی (سال ۳):
  ┌─────────────────────────────────────────────────────────┐
  │  class OrderController                                  │
  │  {                                                      │
  │      public function store(Request $request)            │
  │      {                                                  │
  │          // اعتبارسنجی (باید جدا باشد)                  │
  │          // منطق کسب‌وکار (باید در دامنه باشد)          │
  │          // پرس‌وجوهای پایگاه داده (باید در Repository) │
  │          // ارسال ایمیل (باید در Service باشد)          │
  │          // پردازش پرداخت (باید در Service باشد)        │
  │          // Logging (باید فرابخشی باشد)                 │
  │          // قالب‌بندی پاسخ (باید در Resource باشد)      │
  │      }                                                  │
  │  }                                                      │
  └─────────────────────────────────────────────────────────┘

این کنترلر هفت مسئولیت دارد. اصل Single Responsibility را نقض می‌کند قبل از اینکه حتی درباره SOLID بحث کنیم.

بخش ۲ — اصول SOLID در Laravel: جایی که واقعاً مهم هستند

SOLID یک چک‌لیست نیست. مجموعه‌ای از محدودیت‌های معماری است که حالت‌های شکست خاص را جلوگیری می‌کند.

S — اصل Single Responsibility

یک کلاس باید یک، و فقط یک، دلیل برای تغییر داشته باشد.

حالت شکست Laravel: «God Controller» یا «God Model».

راه‌حل مهندسی: نگرانی‌ها را به لایه‌های مجزا جدا کنید:

App/
├── Http/
│   ├── Controllers/      ← فقط پردازش HTTP
│   ├── Requests/         ← فقط اعتبارسنجی
│   └── Resources/        ← فقط قالب‌بندی پاسخ
├── Actions/              ← عملیات کسب‌وکار واحد
├── Services/             ← منطق کسب‌وکار چندمرحله‌ای
├── Repositories/         ← فقط دسترسی داده
└── Models/               ← فقط Entityهای Eloquent

مثال عملی:

// قبل: کنترلر همه کار را انجام می‌دهد
class OrderController {
    public function store(Request $request) {
        $validated = $request->validate([...]);
        $order = Order::create($validated);
        Mail::to($order->customer)->send(new OrderConfirmation($order));
        return new OrderResource($order);
    }
}

// بعد: تک مسئولیت به ازای هر کلاس
class OrderController {
    public function store(StoreOrderRequest $request, CreateOrderAction $action) {
        $order = $action->execute($request->validated());
        return new OrderResource($order);
    }
}

class CreateOrderAction {
    public function __construct(
        private OrderRepository $orders,
        private OrderConfirmationMailer $mailer
    ) {}
    
    public function execute(array $data): Order {
        $order = $this->orders->create($data);
        $this->mailer->send($order);
        return $order;
    }
}

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

O — اصل Open/Closed

موجودیت‌های نرم‌افزاری باید برای توسعه باز، اما برای تغییر بسته باشند.

حالت شکست Laravel: افزودن یک روش پرداخت جدید نیازمند تغییر یک دستور Switch در کنترلر است.

راه‌حل مهندسی: یک Interface تعریف کنید؛ رفتارهای جدید را به عنوان کلاس‌های جدید پیاده کنید.

interface PaymentGateway {
    public function charge(Order $order): PaymentResult;
}

class StripeGateway implements PaymentGateway { ... }
class PayPalGateway implements PaymentGateway { ... }
class CryptoGateway implements PaymentGateway { ... }

// افزودن Gateway جدید = افزودن کلاس، نه تغییر کد موجود

تأثیر مهندسی: ویژگی‌های جدید سیستم را بدون بی‌ثبات کردن کد موجود گسترش می‌دهند.

L — اصل Liskov Substitution

زیرنوع‌ها باید برای نوع پایه‌شان قابل جایگزینی باشند.

حالت شکست Laravel: یک CachedUserRepository که وقتی کش خالی است Exception می‌اندازد — کدی که رفتار UserRepository را انتظار دارد می‌شکند.

راه‌حل مهندسی: Interfaceها قرارداد تعریف می‌کنند؛ پیاده‌سازی‌ها آن‌ها را کامل انجام می‌دهند.

interface UserRepository {
    public function find(int $id): ?User;  // باید null برگرداند، هرگز Exception ندهد
}

class EloquentUserRepository implements UserRepository { ... }
class CachedUserRepository implements UserRepository { ... }

تأثیر مهندسی: هر پیاده‌سازی می‌تواند بدون تغییر فراخواننده جایگزین شود — کش، Mocking و تست را ممکن می‌کند.

I — اصل Interface Segregation

کلاینت‌ها نباید مجبور به وابستگی به Interfaceهایی باشند که استفاده نمی‌کنند.

حالت شکست Laravel: یک RepositoryInterface واحد با ۳۰ متد که اکثر پیاده‌سازی‌ها فقط ۵ تا را استفاده می‌کنند.

راه‌حل مهندسی: Interfaceهای کوچک و متمرکز.

// قبل: Interface چاق
interface RepositoryInterface {
    public function all();
    public function find($id);
    public function create(array $data);
    public function update($id, array $data);
    public function delete($id);
    public function paginate($perPage);
    public function search($query);
    // ... ۲۰ متد دیگر
}

// بعد: Interfaceهای تفکیک‌شده
interface ReadableRepository {
    public function find(int $id): ?Model;
    public function findBy(array $criteria): Collection;
}

interface WritableRepository {
    public function create(array $data): Model;
    public function update(int $id, array $data): Model;
    public function delete(int $id): bool;
}

تأثیر مهندسی: پیاده‌سازی‌ها فقط چیزی را پیاده می‌کنند که واقعاً پشتیبانی می‌کنند.

D — اصل Dependency Inversion

ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دو باید به Abstractionها وابسته باشند.

حالت شکست Laravel: منطق کسب‌وکار مستقیماً به DB::table() یا Model::query() وابسته است.

راه‌حل مهندسی: به Interfaceها وابسته باشید؛ پیاده‌سازی‌ها را در Service Container Bind کنید.

// قبل: وابستگی مستقیم
class OrderService {
    public function create(array $data) {
        return Order::create($data);  // محکم به Eloquent جفت‌شده
    }
}

// بعد: وابستگی معکوس‌شده
class OrderService {
    public function __construct(
        private OrderRepository $orders  // Interface، نه Eloquent
    ) {}
    
    public function create(array $data) {
        return $this->orders->create($data);
    }
}

// Bind در Service Provider
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);

تأثیر مهندسی: منطق کسب‌وکار فریم‌ورک-آگنوستیک و قابل تست در انزوا می‌شود.

بخش ۳ — الگوی Repository: دسترسی داده به عنوان یک قرارداد

الگوی Repository مؤثرترین تصمیم معماری برای اپلیکیشن‌های Laravel بلندمدت است.

الگوی Repository چه چیزی را حل می‌کند

مشکلراه‌حل Repository
منطق کسب‌وکار به Eloquent وابسته استRepository دسترسی داده را پشت یک Interface پنهان می‌کند
پرس‌وجوها در سراسر کدبیس پراکنده‌اندRepository دسترسی داده را متمرکز می‌کند
Mock کردن در تست‌ها سخت استInterface می‌تواند Mock شود — بدون نیاز به پایگاه داده
ریسک مهاجرت پایگاه دادهپیاده‌سازی‌ها را بدون دست زدن به منطق کسب‌وکار جایگزین کنید
منطق پرس‌وجوی تکراریمنبع واحد حقیقت برای دسترسی داده

ساختار Repository

App/
├── Contracts/
│   └── Repositories/
│       └── OrderRepositoryInterface.php
├── Repositories/
│   └── EloquentOrderRepository.php
└── Providers/
    └── RepositoryServiceProvider.php

Interface (قرارداد)

namespace App\Contracts\Repositories;

interface OrderRepositoryInterface
{
    public function find(int $id): ?Order;
    public function findByCustomer(int $customerId): Collection;
    public function create(array $data): Order;
    public function update(int $id, array $data): Order;
    public function delete(int $id): bool;
    public function paginate(int $perPage = 15): LengthAwarePaginator;
}

پیاده‌سازی

namespace App\Repositories;

use App\Contracts\Repositories\OrderRepositoryInterface;

class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function __construct(
        private Order $model
    ) {}
    
    public function find(int $id): ?Order
    {
        return $this->model->find($id);
    }
    
    public function findByCustomer(int $customerId): Collection
    {
        return $this->model->where('customer_id', $customerId)
            ->orderBy('created_at', 'desc')
            ->get();
    }
    
    public function create(array $data): Order
    {
        return $this->model->create($data);
    }
    
    public function paginate(int $perPage = 15): LengthAwarePaginator
    {
        return $this->model->paginate($perPage);
    }
}

Bind

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\Repositories\OrderRepositoryInterface;
use App\Repositories\EloquentOrderRepository;

class RepositoryServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            OrderRepositoryInterface::class,
            EloquentOrderRepository::class
        );
    }
}

چه زمانی از Repository استفاده کنیم (و چه زمانی نه)

از Repository استفاده کنید وقتیاز Repository استفاده نکنید وقتی
✅ منطق کسب‌وکار باید بدون پایگاه داده قابل تست باشد❌ اپ یک Interface CRUD ساده است
✅ چندین منبع داده (Eloquent + API خارجی)❌ Eloquent تنها کافی است
✅ پرس‌وجوهای پیچیده در چندین جا استفاده می‌شوند❌ هر پرس‌وجو فقط یک بار استفاده می‌شود
✅ قابلیت نگهداری بلندمدت مهم است❌ نمونه اولیه یا پروژه دورانداختنی
✅ تیم چندین توسعه‌دهنده دارد❌ یک توسعه‌دهنده، دامنه کوچک

Repositoryها یک لایه اضافه می‌کنند. آن لایه باید هزینه‌اش را توجیه کند. در اپلیکیشن‌های بزرگ و بلندمدت، این کار را می‌کند.

بخش ۴ — Dependency Injection: چسب Clean Architecture

Service Container در Laravel مورد کم‌استفاده‌ترین ویژگی در اپلیکیشن‌های تولیدی است. این جادو نیست — مدیریت وابستگی است.

تزریق سازنده (Constructor Injection)

الگوی استاندارد برای وابستگی‌های تزریق‌شدنی:

class CreateOrderAction
{
    public function __construct(
        private OrderRepositoryInterface $orders,
        private PaymentGateway $payment,
        private OrderConfirmationMailer $mailer
    ) {}
    
    public function execute(array $data): Order
    {
        $order = $this->orders->create($data);
        $this->payment->charge($order);
        $this->mailer->send($order);
        return $order;
    }
}

تأثیر مهندسی: وابستگی‌ها صریح، قابل تست و قابل جایگزینی هستند.

Bind در Service Provider

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class);
    $this->app->bind(PaymentGateway::class, StripeGateway::class);
    $this->app->singleton(OrderConfirmationMailer::class);
}

Bind زمینه‌ای (Contextual Binding)

پیاده‌سازی‌های مختلف برای مصرف‌کنندگان مختلف:

$this->app->when(AdminOrderService::class)
    ->needs(OrderRepositoryInterface::class)
    ->give(AdminOrderRepository::class);

$this->app->when(CustomerOrderService::class)
    ->needs(OrderRepositoryInterface::class)
    ->give(CustomerOrderRepository::class);

تأثیر مهندسی: همان Interface بر اساس زمینه به پیاده‌سازی‌های مختلف حل می‌شود — بدون تغییر منطق کسب‌وکار.

بخش ۵ — استراتژی تست PHPUnit: تور ایمنی

تست‌ها مستندات نیستند. آن‌ها تور ایمنی هستند که Refactoring را ممکن می‌کند.

هرم تست در Laravel

        ┌─────────────┐
        │   E2E /     │  ← کم: HTTP + DB + خارجی کامل
        │   Feature   │
        ├─────────────┤
        │ Integration │  ← برخی: Repository + DB
        ├─────────────┤
        │    Unit     │  ← بسیاری: Actionها، Serviceها، منطق دامنه
        └─────────────┘

اصل مهندسی: اکثریت تست‌ها باید Unit Test باشند — سریع، ایزوله و متمرکز بر منطق کسب‌وکار.

Unit Testing Actionها (بدون پایگاه داده)

class CreateOrderActionTest extends TestCase
{
    public function test_it_creates_order_and_sends_confirmation(): void
    {
        // Arrange
        $order = new Order(['id' => 1]);
        
        $orders = $this->createMock(OrderRepositoryInterface::class);
        $orders->expects($this->once())
            ->method('create')
            ->willReturn($order);
        
        $payment = $this->createMock(PaymentGateway::class);
        $payment->expects($this->once())
            ->method('charge')
            ->with($order);
        
        $mailer = $this->createMock(OrderConfirmationMailer::class);
        $mailer->expects($this->once())
            ->method('send')
            ->with($order);
        
        $action = new CreateOrderAction($orders, $payment, $mailer);
        
        // Act
        $result = $action->execute(['customer_id' => 42]);
        
        // Assert
        $this->assertSame($order, $result);
    }
}

چرا این مهم است: بدون پایگاه داده. بدون HTTP. بدون سرویس خارجی. تست در میلی‌ثانیه اجرا می‌شود و منطق کسب‌وکار را در انزوا تأیید می‌کند.

تست Feature برای کنترلرها

class OrderControllerTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_it_creates_order_via_api(): void
    {
        $this->postJson('/api/orders', [
            'customer_id' => 42,
            'items' => [['sku' => 'ABC', 'qty' => 2]]
        ])
        ->assertStatus(201)
        ->assertJsonStructure(['id', 'customer_id', 'total']);
        
        $this->assertDatabaseHas('orders', ['customer_id' => 42]);
    }
}

تست Repository

class EloquentOrderRepositoryTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_it_finds_orders_by_customer(): void
    {
        Order::factory()->count(3)->create(['customer_id' => 42]);
        Order::factory()->create(['customer_id' => 99]);
        
        $repo = new EloquentOrderRepository(new Order());
        $results = $repo->findByCustomer(42);
        
        $this->assertCount(3, $results);
    }
}

اهداف پوشش تست

لایههدف پوششمنطق
منطق دامنه / Actionها۹۰٪+قوانین کسب‌وکار باید تأیید شوند
Repositoryها۸۰٪+صحت دسترسی داده
کنترلرها۷۰٪+پردازش HTTP + اعتبارسنجی
E2Eفقط مسیرهای حیاتیگران، کند، شکننده

اصل مهندسی: پوشش یک سیگنال است، نه یک هدف. ۱۰۰٪ پوشش با Assertionهای بی‌معنا بدتر از ۸۰٪ پوشش با Assertionهای معنادار است.

بخش ۶ — معماری پنج‌ساله

ترکیب تمام دیسیپلین‌ها در کدبیسی که تکامل کسب‌وکار را زنده می‌ماند:

┌─────────────────────────────────────────────────────────────────────────────┐
│                         لایه HTTP                                           │
│  کنترلرها (نازک) │ Form Requestها (اعتبارسنجی) │ Resourceها (پاسخ)         │
├─────────────────────────────────────────────────────────────────────────────┤
│                         لایه اپلیکیشن                                       │
│  Actionها (عملیات واحد) │ Serviceها (جریان‌های چندمرحله‌ای)                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                         لایه دامنه                                          │
│  Interfaceها (قراردادها) │ Value Objectها │ Domain Eventها                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                         لایه زیرساخت                                        │
│  Repositoryها (دسترسی داده) │ سرویس‌های خارجی │ Mailerها │ Payment          │
├─────────────────────────────────────────────────────────────────────────────┤
│                         لایه فریم‌ورک                                       │
│  Laravel │ Eloquent │ Service Container │ Queue │ Cache                     │
└─────────────────────────────────────────────────────────────────────────────┘

قوانین معماری

قانونمنطق
کنترلرها هرگز پایگاه داده را پرس‌وجو نمی‌کنندمنطق کسب‌وکار به Actionها/Serviceها تعلق دارد
منطق کسب‌وکار هرگز مستقیماً به Eloquent وابسته نیستInterface Repository دسترسی داده را Abstraction می‌کند
هر وابستگی تزریق می‌شود، نه Instantiateقابلیت تست و جایگزینی
Interfaceها قرارداد تعریف می‌کنند؛ پیاده‌سازی‌ها آن‌ها را انجام می‌دهندجفت‌شدگی ضعیف
Unit Testها منطق دامنه را پوشش می‌دهند؛ Feature Testها یکپارچگی رابازخورد سریع + صحت
ویژگی‌های جدید Interfaceها را گسترش می‌دهند؛ کد موجود تغییر نمی‌کنداصل Open/Closed

بخش ۷ — چه زمانی این معماری را اعمال کنیم (و چه زمانی نه)

SOLID + Repository + DI + Testing را اعمال کنید وقتی:

  • ✅ انتظار می‌رود اپلیکیشن ۳+ سال زندگی کند

  • ✅ چندین توسعه‌دهنده روی کدبیس کار می‌کنند

  • ✅ قوانین کسب‌وکار پیچیده و در حال تکامل هستند

  • ✅ تست یک الزام است (نه اختیاری)

  • ✅ قابلیت نگهداری بلندمدت از سرعت اولیه مهم‌تر است

  • ✅ دامنه Bounded Contextهای واضح دارد

آن را اعمال نکنید وقتی:

  • ❌ پروژه یک نمونه اولیه یا MVP با آینده نامعلوم است

  • ❌ تیم یک توسعه‌دهنده روی دامنه کوچک است

  • ❌ اپلیکیشن CRUD ساده با حداقل منطق کسب‌وکار است

  • ❌ سرعت به بازار از ساختار بلندمدت پیشی می‌گیرد

  • ❌ تیم تجربه‌ای با این الگوها ندارد (ریسک اضافه می‌کند)

معماری یک سرمایه‌گذاری است. بازده خود را در قابلیت نگهداری می‌پردازد. هزینه آن پیچیدگی اولیه است. سؤال این است که آیا کدبیس به اندازه کافی زنده می‌ماند که بازده را جمع‌آوری کند.

نتیجه‌گیری — جلوگیری از پوسیدگی یک تصمیم معماری است

نرم‌افزار سازمانی پوسیده نمی‌شود چون توسعه‌دهندگان بی‌دقت هستند. پوسیده می‌شود چون هیچ مرز معماری برای جلوگیری از آن ایجاد نشده است.

اصول SOLID، الگوهای Repository، Dependency Injection و تست خودکار تمرین‌های دانشگاهی نیستند. دیسیپلین‌های خاصی هستند که جلوگیری می‌کنند از:

  • تغییرات شکننده → جفت‌شدگی ضعیف

  • کد غیرقابل تست → Dependency Injection

  • منطق تکراری → Single Responsibility

  • قفل شدن به فریم‌ورک → قراردادهای Interface

  • ترس از Refactoring → پوشش تست

کدبیس پنج‌ساله یک خیال نیست. نتیجه تصمیماتی است که در سال ۱ گرفته شده — و در سال‌های ۲ تا ۵ رعایت می‌شوند.

کد برای کامپیوتر نوشته نمی‌شود. برای توسعه‌دهنده‌ای نوشته می‌شود که سال آینده آن را تغییر خواهد داد.

یادداشت نویسنده

این مقاله الگوهای معماری توسعه‌یافته در هنگام ساخت CoreBiz ERP — یک پلتفرم سازمانی ماژولار طراحی‌شده برای قابلیت نگهداری بلندمدت — را بازتاب می‌دهد. برای همکاری در معماری PHP/Laravel سازمانی، از طریق صفحه تماس با من در ارتباط باشید.

Tags:
Write a comment