حذف بدهی معماری: پیادهسازی اصول 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.phpInterface (قرارداد)
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 سازمانی، از طریق صفحه تماس با من در ارتباط باشید.