وقتی یک پروژه PHP کوچک است، معمولاً اطلاعات مستقیماً از داخل Controller یا Service از دیتابیس خوانده میشوند. برای مثال ممکن است در چندین بخش از پروژه، کدهایی مانند Product::find()، کوئریهای PDO یا دستورات SQL تکرار شوند.
این روش در پروژههای کوچک قابل استفاده است؛ اما با بزرگتر شدن پروژه، مشکلاتی مانند وابستگی شدید به دیتابیس، تکرار کوئریها، دشوار شدن تستنویسی و پیچیده شدن تغییر منبع داده به وجود میآید.
Repository Pattern یکی از الگوهای طراحی است که برای مدیریت دسترسی به Entityها و جداسازی منطق برنامه از جزئیات ذخیرهسازی استفاده میشود. این الگو یک رابط شبیه Collection در اختیار بخشهای دیگر برنامه قرار میدهد و جزئیات مربوط به PDO، Eloquent، SQL یا هر منبع داده دیگری را در لایه Repository نگه میدارد. <Cite refs={["turn0search5"]} />
در این مقاله ابتدا مفهوم Repository Pattern را بررسی میکنیم، سپس آن را با PHP خام پیادهسازی کرده و در ادامه نمونهای از استفاده آن در Laravel را میبینیم.
Repository Pattern چیست؟
Repository Pattern یک الگوی طراحی برای ایجاد یک لایه واسط بین منطق دامنه برنامه و لایه دسترسی به داده است.
به جای اینکه Service یا Controller مستقیماً با دیتابیس کار کند، با یک Repository ارتباط برقرار میکند.
برای مثال، در حالت مستقیم ممکن است کد زیر را داشته باشیم:
$product = Product::find($id);
یا:
$stmt = $pdo->prepare(
'SELECT * FROM products WHERE id = :id'
);
$stmt->execute([
'id' => $id,
]);
$product = $stmt->fetch();
در این حالت، کدی که به محصول نیاز دارد، به جزئیات Eloquent یا PDO وابسته است.
اما در Repository Pattern میتوانیم بنویسیم:
$product = $productRepository->findById($id);
کدی که محصول را مصرف میکند، دیگر لازم نیست بداند اطلاعات از Eloquent، PDO، API، فایل یا حافظه خوانده میشوند.
چرا به Repository Pattern نیاز داریم؟
فرض کنید در یک فروشگاه اینترنتی، اطلاعات محصولات در بخشهای زیر استفاده میشوند:
- صفحه فهرست محصولات
- صفحه جزئیات محصول
- سبد خرید
- سیستم سفارشها
- پنل مدیریت
- API محصولات
- سیستم گزارشگیری
اگر در همه این بخشها کوئریهای دیتابیس نوشته شوند، تغییر ساختار دیتابیس یا روش ذخیرهسازی دشوار خواهد شد.
برای مثال، اگر در ابتدا از MySQL و PDO استفاده کنیم و بعداً بخواهیم از یک API یا Elasticsearch استفاده کنیم، باید بخشهای زیادی از برنامه را تغییر دهیم.
Repository این وابستگی را کاهش میدهد. بخش اصلی برنامه فقط با Interface کار میکند و پیادهسازی واقعی میتواند تغییر کند.
ساختار کلی Repository Pattern
ساختار معمول این الگو شامل بخشهای زیر است:
Application Service
|
v
Repository Interface
|
v
Concrete Repository
|
v
PDO / Eloquent / API / Database
۱. Entity
Entity شیء اصلی دامنه است؛ برای مثال:
- Product
- User
- Order
- Invoice
- Service
- Ticket
۲. Repository Interface
Interface قرارداد Repository را مشخص میکند. این قرارداد تعیین میکند که Repository چه عملیاتی را پشتیبانی میکند.
۳. Concrete Repository
کلاس Concrete Repository قرارداد Interface را پیادهسازی میکند. این کلاس میتواند از PDO، Eloquent یا هر منبع داده دیگری استفاده کند.
۴. Service یا Client
Service بخشی از برنامه است که به داده نیاز دارد. این بخش به جای استفاده مستقیم از دیتابیس، Repository را صدا میزند.
پیادهسازی Repository Pattern در PHP خام
برای شروع یک Entity ساده به نام Product میسازیم.
ساخت Entity محصول
<?php
declare(strict_types=1);
final class Product
{
public function __construct(
private int $id,
private string $name,
private int $price
) {
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): int
{
return $this->price;
}
}
در این مثال، محصول سه ویژگی دارد:
- شناسه
- نام
- قیمت
این Entity هیچ وابستگی مستقیمی به PDO، MySQL یا Laravel ندارد.
این موضوع مهم است؛ زیرا Entity باید بتواند بدون نیاز به دیتابیس نیز در منطق دامنه استفاده شود.
ساخت Repository Interface
اکنون قراردادی برای دسترسی به محصولات ایجاد میکنیم:
<?php
declare(strict_types=1);
interface ProductRepositoryInterface
{
public function findById(int $id): ?Product;
/**
* @return Product[]
*/
public function findAll(): array;
public function save(Product $product): void;
public function delete(Product $product): void;
}
در این Interface چهار عملیات تعریف شده است:
- یافتن محصول با شناسه
- دریافت همه محصولات
- ذخیره محصول
- حذف محصول
نکته مهم این است که Interface نباید جزئیات پیادهسازی را مشخص کند. برای مثال، نباید داخل آن از PDO، Eloquent یا SQL استفاده کنیم.
ساخت In-Memory Repository
برای اینکه ابتدا مفهوم Repository را بدون دیتابیس درک کنیم، یک Repository حافظهای میسازیم.
این نوع Repository اطلاعات را در یک آرایه نگهداری میکند و برای تستنویسی نیز بسیار مفید است.
<?php
declare(strict_types=1);
final class InMemoryProductRepository
implements ProductRepositoryInterface
{
/**
* @var Product[]
*/
private array $products = [];
public function findById(int $id): ?Product
{
foreach ($this->products as $product) {
if ($product->getId() === $id) {
return $product;
}
}
return null;
}
public function findAll(): array
{
return array_values($this->products);
}
public function save(Product $product): void
{
$this->products[$product->getId()] = $product;
}
public function delete(Product $product): void
{
unset($this->products[$product->getId()]);
}
}
اکنون میتوانیم از Repository استفاده کنیم:
$repository = new InMemoryProductRepository();
$product = new Product(
id: 1,
name: 'Laptop',
price: 45000000
);
$repository->save($product);
$foundProduct = $repository->findById(1);
if ($foundProduct !== null) {
echo $foundProduct->getName();
}
خروجی:
Laptop
در این مثال، کد مصرفکننده هیچ اطلاعی از نحوه ذخیره محصول ندارد.
استفاده از Repository در یک Service
بهتر است منطق کسبوکار را داخل Repository قرار ندهیم. Repository مسئول دسترسی به داده است، اما تصمیمهای تجاری باید در Service یا Domain Service قرار بگیرند.
برای مثال، یک Service برای تغییر قیمت محصول میسازیم:
<?php
declare(strict_types=1);
final class ProductService
{
public function __construct(
private ProductRepositoryInterface $repository
) {
}
public function getProduct(int $id): ?Product
{
return $this->repository->findById($id);
}
public function saveProduct(Product $product): void
{
if ($product->getPrice() < 0) {
throw new InvalidArgumentException(
'Product price cannot be negative.'
);
}
$this->repository->save($product);
}
}
در این ساختار:
- Repository محصول را ذخیره میکند.
- Service قوانین مربوط به قیمت را بررسی میکند.
- Entity اطلاعات محصول را نگهداری میکند.
این جداسازی باعث میشود مسئولیت هر کلاس مشخص باشد.
پیادهسازی Repository با PDO
در پروژه واقعی معمولاً به یک Repository نیاز داریم که با دیتابیس ارتباط برقرار کند.
فرض میکنیم جدول زیر را داریم:
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price BIGINT NOT NULL
);
سپس Repository مربوط به PDO را ایجاد میکنیم:
<?php
declare(strict_types=1);
final class PdoProductRepository
implements ProductRepositoryInterface
{
public function __construct(
private PDO $pdo
) {
}
public function findById(int $id): ?Product
{
$stmt = $this->pdo->prepare(
'SELECT id, name, price
FROM products
WHERE id = :id'
);
$stmt->execute([
'id' => $id,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row === false) {
return null;
}
return new Product(
id: (int) $row['id'],
name: (string) $row['name'],
price: (int) $row['price']
);
}
public function findAll(): array
{
$stmt = $this->pdo->query(
'SELECT id, name, price
FROM products
ORDER BY id DESC'
);
$products = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$products[] = new Product(
id: (int) $row['id'],
name: (string) $row['name'],
price: (int) $row['price']
);
}
return $products;
}
public function save(Product $product): void
{
$stmt = $this->pdo->prepare(
'INSERT INTO products (id, name, price)
VALUES (:id, :name, :price)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price)'
);
$stmt->execute([
'id' => $product->getId(),
'name' => $product->getName(),
'price' => $product->getPrice(),
]);
}
public function delete(Product $product): void
{
$stmt = $this->pdo->prepare(
'DELETE FROM products WHERE id = :id'
);
$stmt->execute([
'id' => $product->getId(),
]);
}
}
اکنون میتوانیم PDO را به Repository تزریق کنیم:
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4',
'root',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$repository = new PdoProductRepository($pdo);
$product = $repository->findById(1);
در این حالت، Service همچنان فقط با ProductRepositoryInterface کار میکند و از نوع Repository واقعی اطلاعی ندارد.
Repository Pattern در Laravel
در Laravel، Eloquent خودش امکانات زیادی برای کار با دیتابیس فراهم میکند. بنابراین استفاده از Repository در همه پروژهها ضروری نیست.
برای CRUD ساده، استفاده مستقیم از Eloquent معمولاً کافی است:
$product = Product::findOrFail($id);
اما زمانی که منطق دسترسی به داده پیچیده شود یا بخواهیم بخشهای مختلف برنامه را از Eloquent جدا کنیم، Repository میتواند مفید باشد.
ساخت Interface
ابتدا Interface را ایجاد میکنیم:
php artisan make:interface Repositories/ProductRepositoryInterface
در بعضی نسخهها یا تنظیمات Laravel ممکن است این دستور به صورت پیشفرض وجود نداشته باشد. در این حالت میتوان فایل را به صورت دستی ایجاد کرد:
app/Repositories/ProductRepositoryInterface.php
محتوای فایل:
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Product;
interface ProductRepositoryInterface
{
public function findById(int $id): ?Product;
public function findAll();
public function save(Product $product): Product;
public function delete(Product $product): bool;
}
ساخت Repository با Eloquent
فایل زیر را ایجاد میکنیم:
app/Repositories/EloquentProductRepository.php
کد:
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Product;
final class EloquentProductRepository
implements ProductRepositoryInterface
{
public function findById(int $id): ?Product
{
return Product::query()->find($id);
}
public function findAll()
{
return Product::query()
->latest('id')
->get();
}
public function save(Product $product): Product
{
$product->save();
return $product;
}
public function delete(Product $product): bool
{
return (bool) $product->delete();
}
}
این Repository جزئیات Eloquent را در یک کلاس متمرکز میکند.
اتصال Repository به Service Container
برای اینکه Laravel بتواند Interface را به پیادهسازی واقعی متصل کند، باید Binding ایجاد کنیم.
در Laravel میتوان این Binding را در Service Provider قرار داد.
برای مثال:
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\ProductRepositoryInterface;
use App\Repositories\EloquentProductRepository;
final class RepositoryServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
ProductRepositoryInterface::class,
EloquentProductRepository::class
);
}
}
پس از ثبت Provider، Laravel هنگام دریافت ProductRepositoryInterface، کلاس EloquentProductRepository را تزریق میکند.
استفاده از Repository در Controller
اکنون Controller میتواند به Interface وابسته باشد:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Repositories\ProductRepositoryInterface;
final class ProductController
{
public function __construct(
private ProductRepositoryInterface $products
) {
}
public function show(int $id)
{
$product = $this->products->findById($id);
abort_if($product === null, 404);
return response()->json($product);
}
}
Controller دیگر مستقیماً از Product::find() استفاده نمیکند.
اگر در آینده بخواهیم دادهها را از API یا یک دیتابیس دیگر بخوانیم، میتوانیم پیادهسازی Repository را تغییر دهیم، بدون اینکه Controller تغییر زیادی کند.
تفاوت Repository با Data Mapper
Repository و Data Mapper دو مفهوم متفاوت هستند، اما میتوانند در کنار یکدیگر استفاده شوند.
Data Mapper
Data Mapper وظیفه تبدیل دادههای ذخیرهشده به Entity و برعکس را بر عهده دارد.
برای مثال:
Database Row → Product Entity
Product Entity → Database Row
Repository
Repository یک رابط شبیه مجموعه برای دسترسی به Entityها فراهم میکند.
برای مثال:
$productRepository->findById(10);
$productRepository->findAll();
$productRepository->save($product);
در یک معماری کامل ممکن است Repository از Data Mapper استفاده کند:
Service
|
Repository
|
Data Mapper
|
Database
بنابراین Repository جایگزین Data Mapper نیست. Repository نحوه دسترسی به Entityها را مدیریت میکند و Data Mapper تبدیل بین Entity و داده ذخیرهشده را انجام میدهد. <Cite refs={["turn0search5","turn0search4"]} />
تفاوت Repository با DAO
DAO مخفف Data Access Object است.
DAO معمولاً مسئول اجرای عملیات مستقیم روی منبع داده است؛ برای مثال:
- اجرای SQL
- اجرای Query
- خواندن رکورد
- ذخیره داده
Repository معمولاً سطح انتزاع بالاتری دارد و با مفاهیم دامنه کار میکند.
برای مثال DAO ممکن است چنین متدی داشته باشد:
public function selectProductById(int $id): array;
اما Repository چنین متدی دارد:
public function findById(int $id): ?Product;
در Repository نتیجه معمولاً به شکل Entity یا مجموعهای از Entityها ارائه میشود.
تفاوت Repository با Service
Repository مسئول دسترسی به داده است.
Service مسئول اجرای یک فرآیند یا قانون کسبوکار است.
برای مثال در فروشگاه:
ProductRepository:
- findById()
- findAll()
- save()
- delete()
OrderService:
- createOrder()
- calculateTotal()
- applyDiscount()
- confirmPayment()
نباید محاسبه تخفیف، اعتبارسنجی پرداخت یا تصمیمگیریهای تجاری را داخل Repository قرار دهیم.
مزایای Repository Pattern
۱. جداسازی منطق برنامه از دیتابیس
بخشهای مختلف برنامه مستقیماً به PDO یا Eloquent وابسته نیستند.
۲. تستپذیری بهتر
در تستها میتوان به جای دیتابیس واقعی از In-Memory Repository یا Fake Repository استفاده کرد.
۳. کاهش تکرار کوئریها
کوئریهای مربوط به یک Entity در یک محل متمرکز میشوند.
۴. امکان تغییر منبع داده
در صورت نیاز میتوان Repository جدیدی برای API، فایل، Redis یا دیتابیس دیگر ایجاد کرد.
۵. خوانایی بهتر کد
کدی مانند زیر، هدف برنامه را بهتر نشان میدهد:
$productRepository->findById($id);
در مقایسه با قرار دادن SQL در Controller، این روش خواناتر و قابل نگهداریتر است.
معایب Repository Pattern
۱. افزایش تعداد کلاسها
برای یک Entity ساده ممکن است به Interface، Repository، تست و Provider نیاز داشته باشیم.
۲. احتمال ایجاد Abstraction غیرضروری
اگر پروژه فقط چند عملیات ساده CRUD دارد، Repository ممکن است پیچیدگی اضافه ایجاد کند.
۳. خطر Repository عمومی و بیش از حد بزرگ
کلاسی مانند زیر معمولاً طراحی خوبی نیست:
GenericRepository
که برای همه Entityها متدهای بسیار عمومی و نامرتبط دارد.
بهتر است Repositoryها بر اساس نیاز دامنه طراحی شوند.
۴. پنهان کردن بیش از حد امکانات Eloquent
در Laravel اگر Repository فقط تمام متدهای Eloquent را بدون هدف تکرار کند، ممکن است ارزش معماری آن کم شود.
چه زمانی از Repository Pattern استفاده کنیم؟
Repository Pattern در شرایط زیر میتواند مفید باشد:
- پروژه دارای منطق دسترسی به داده پیچیده است.
- چند منبع داده وجود دارد.
- پروژه از معماری لایهای یا Clean Architecture استفاده میکند.
- نیاز به تستنویسی مستقل از دیتابیس وجود دارد.
- چند بخش برنامه از یک مجموعه کوئری استفاده میکنند.
- میخواهیم Domain یا Application به Eloquent وابسته نباشد.
- احتمال تغییر روش ذخیرهسازی وجود دارد.
چه زمانی Repository نسازیم؟
برای پروژههای کوچک و ساده، استفاده مستقیم از Eloquent یا Query Builder ممکن است مناسبتر باشد.
اگر تنها کاری که Repository انجام میدهد این است:
public function find(int $id)
{
return Product::find($id);
}
و هیچ منطق، جداسازی یا ارزش معماری دیگری ایجاد نمیکند، بهتر است ابتدا نیاز واقعی پروژه را بررسی کنیم.
Repository یک قانون اجباری برای تمام پروژههای Laravel نیست.
تست Repository با PHPUnit
یکی از مزیتهای مهم Repository این است که میتوانیم Service را بدون اتصال به دیتابیس تست کنیم.
برای مثال:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ProductServiceTest extends TestCase
{
public function test_it_can_save_and_find_product(): void
{
$repository = new InMemoryProductRepository();
$service = new ProductService($repository);
$product = new Product(
id: 1,
name: 'Keyboard',
price: 1500000
);
$service->saveProduct($product);
$result = $service->getProduct(1);
$this->assertNotNull($result);
$this->assertSame('Keyboard', $result->getName());
$this->assertSame(1500000, $result->getPrice());
}
public function test_it_rejects_negative_price(): void
{
$repository = new InMemoryProductRepository();
$service = new ProductService($repository);
$product = new Product(
id: 2,
name: 'Mouse',
price: -100
);
$this->expectException(InvalidArgumentException::class);
$service->saveProduct($product);
}
}
در این تست:
- دیتابیس واقعی وجود ندارد.
- Migration اجرا نمیشود.
- اطلاعات داخل حافظه ذخیره میشوند.
- فقط منطق Service بررسی میشود.
این نوع تست معمولاً سریعتر و مستقلتر از تستی است که به دیتابیس واقعی متصل میشود.
اشتباهات رایج در Repository Pattern
قرار دادن منطق کسبوکار در Repository
کدی مانند محاسبه تخفیف یا بررسی موجودی تجاری بهتر است در Service یا Domain قرار بگیرد.
ساخت Repository برای تمام مدلها بدون نیاز
هر مدل الزاماً به Repository نیاز ندارد. ابتدا پیچیدگی پروژه را بررسی کنید.
استفاده از متدهای بیش از حد عمومی
متدهایی مانند findByAnything() معمولاً باعث میشوند Repository به یک کلاس پیچیده و نامشخص تبدیل شود.
وابستگی Interface به Eloquent
اگر هدف جداسازی Domain از Laravel است، نباید Interface شامل نوعهایی مانند Builder یا کلاسهای Eloquent باشد.
بازگرداندن آرایه خام در همه بخشها
اگر لایه دامنه با Entity کار میکند، بهتر است Repository نیز Entity یا Collection مشخصی از Entityها بازگرداند.
مخلوط کردن Query و منطق کسبوکار
Repository باید مسئول دریافت و ذخیره داده باشد، نه مدیریت تمام فرآیندهای برنامه.
نمونه معماری پیشنهادی
یک ساختار مناسب برای پروژه Laravel میتواند به شکل زیر باشد:
app/
├── Domain/
│ └── Products/
│ └── Product.php
├── Application/
│ └── Products/
│ └── ProductService.php
├── Repositories/
│ ├── ProductRepositoryInterface.php
│ └── EloquentProductRepository.php
├── Models/
│ └── Product.php
└── Providers/
└── RepositoryServiceProvider.php
در پروژههای بزرگتر میتوان Repository را داخل ماژول یا Bounded Context مربوط به همان بخش قرار داد.
برای مثال:
Modules/
└── Billing/
├── Domain/
├── Application/
├── Infrastructure/
└── Repositories/
جمعبندی
Repository Pattern در PHP یک لایه انتزاعی برای دسترسی به Entityها ایجاد میکند و جزئیات مربوط به ذخیرهسازی را از منطق اصلی برنامه جدا میسازد.
در این الگو:
- Entity مسئول مدلسازی داده و رفتار دامنه است.
- Repository Interface قرارداد دسترسی به داده را مشخص میکند.
- Concrete Repository با PDO، Eloquent یا منبع داده دیگر کار میکند.
- Service منطق کسبوکار را اجرا میکند.
- Controller فقط با Service یا Interface مورد نیاز ارتباط دارد.
Repository Pattern در پروژههای پیچیده، معماریهای لایهای و سیستمهایی که به تستپذیری بالا نیاز دارند مفید است. با این حال، در پروژههای ساده نباید صرفاً به دلیل استفاده از یک Design Pattern، کلاسهای اضافی ایجاد کنیم.
مهمترین نکته این است که Repository را بر اساس نیاز واقعی دامنه طراحی کنیم، نه اینکه فقط یک لایه اضافی برای تکرار متدهای Eloquent بسازیم.
سوالات متداول
Repository Pattern در PHP چیست؟
Repository Pattern یک الگوی طراحی برای جداسازی منطق دسترسی به داده از منطق اصلی برنامه است. این الگو معمولاً یک Interface و یک پیادهسازی مشخص برای کار با Entityها دارد.
آیا Repository Pattern فقط در Laravel استفاده میشود؟
خیر. Repository Pattern یک مفهوم معماری مستقل از فریمورک است و میتوان آن را با PHP خام، PDO، Doctrine، Laravel یا حتی منابع داده غیر از دیتابیس پیادهسازی کرد.
آیا استفاده از Repository در Laravel اجباری است؟
خیر. برای CRUD ساده، Eloquent معمولاً کافی است. Repository زمانی ارزش بیشتری دارد که پروژه پیچیده باشد یا به جداسازی، تستپذیری و تغییر منبع داده نیاز داشته باشیم.
تفاوت Repository و Data Mapper چیست؟
Data Mapper مسئول تبدیل دادههای ذخیرهشده به Entity و برعکس است؛ اما Repository یک رابط برای دسترسی به مجموعهای از Entityها فراهم میکند. این دو میتوانند در کنار یکدیگر استفاده شوند.
آیا Repository باعث افزایش سرعت برنامه میشود؟
Repository بهصورت مستقیم تضمین نمیکند که برنامه سریعتر شود. هدف اصلی آن جداسازی مسئولیتها، کاهش وابستگی و بهتر شدن تستپذیری است. سرعت برنامه بیشتر به طراحی کوئریها، ایندکسها، کش و روش ذخیرهسازی بستگی دارد.
آیا میتوان Repository را با PHPUnit تست کرد؟
بله. میتوان با استفاده از Fake Repository یا In-Memory Repository، منطق Service را بدون اتصال به دیتابیس تست کرد.
آیا باید برای هر Model یک Repository بسازیم؟
خیر. ساخت Repository برای هر Model یک قانون ثابت نیست. بهتر است فقط زمانی Repository ایجاد کنیم که پیچیدگی یا نیاز معماری مشخصی وجود داشته باشد.





