زیرساخت پرسوجو: راهنمای پیادهسازی
نمای کلی
زیرساخت پرسوجو یک روش استاندارد برای ماژولها فراهم میکند تا داده را بخوانند از ماژولهای دیگر بدون ایجاد وابستگی شدید. این راهنما فرآیند کامل پیادهسازی از ایجاد اولین رابط پرسوجو تا الگوهای استفاده پیشرفته را پوشش میدهد.
اصل اصلی
وارونگی وابستگی: ماژولهای مصرفکننده به انتزاعها (رابطها) تعریفشده در یک هسته مشترک وابستهاند، در حالی که ماژولهای ارائهدهنده این رابطها را در دامنه خود پیادهسازی میکنند.
اجزای معماری
1. قراردادهای پایه (پایه مشترک)
واقع در app/Contracts/:
- QueryInterface
- DomainId
- DataTransferObject
- BaseCustomCollection
<?php
declare(strict_types=1);
namespace App\Contracts;
interface QueryInterface
{
// رابط نشانگر - هیچ متدی مورد نیاز نیست
// همه رابطهای پرسوجو باید این را extend کنند
}
<?php
declare(strict_types=1);
namespace App\Contracts;
use InvalidArgumentException;
use Stringable;
abstract class DomainId implements Stringable
{
public function __construct(
public readonly int|string $value
) {
$this->validate($value);
}
protected function validate(int|string $value): void
{
if (is_string($value)) {
return;
}
if ($value <= 0) {
throw new InvalidArgumentException(
sprintf(
'%s expects a positive integer id. Given: %d',
static::class,
$value
)
);
}
}
public function equals(self $other): bool
{
return $this->value === $other->value
&& get_class($this) === get_class($other);
}
public function __toString(): string
{
return (string) $this->value;
}
}
<?php
declare(strict_types=1);
namespace App\Contracts;
interface DataTransferObject
{
public function toArray(): array;
}
<?php
declare(strict_types=1);
namespace App\Contracts;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use Traversable;
abstract class BaseCustomCollection implements IteratorAggregate, Countable
{
protected array $items = [];
public function __construct(array $items = [])
{
$this->items = $items;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
public function count(): int
{
return count($this->items);
}
public function isEmpty(): bool
{
return empty($this->items);
}
public function toArray(): array
{
return $this->items;
}
public static function empty(): static
{
return new static([]);
}
}
2. لایه دامنه (قراردادها)
واقع در app/Domains/{Context}/:
این لایه شامل:
- رابطهای پرسوجو
- DTOها (اشیاء انتقال داده)
- IDهای دامنه (Value Objects)
- مجموعههای DTO
app/Domains/User/
├── UserQuery.php # رابط
├── UserDTO.php # شیء انتقال داده
├── UserDTOCollection.php # مجموعه DTO
├── UserId.php # شناسه دامنه (Value Object)
└── UserIdCollection.php # مجموعه شناسه
3. لایه پیادهسازی
واقع در app/Core/{Module}/Services/Query/ یا app/Modules/{Module}/Services/Query/:
این لایه شامل:
- پیادهسازیهای مشخص رابطهای پرسوجو
- استفاده از ریپوزیتوری
- منطق نگاشت Entity به DTO
4. Service Locator
واقع در app/Services/QueryServiceLocator.php:
مدیریت ثبت و حل سرویسهای پرسوجو.
5. Service Provider
واقع در app/Providers/QueryServiceProvider.php:
ثبت همه رابطهای پرسوجو و پیادهسازیهای آنها با کانتینر سرویس Laravel.
پیادهسازی گام به گام
گام 1: ایجاد شناسه دامنه (Value Object)
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\DomainId;
final class OrderId extends DomainId
{
// همه قابلیتها را از DomainId به ارث میبرد
// در صورت نیاز میتوان متدهای خاص دامنه اضافه کرد
}
- کلاس پایه
DomainIdرا extend میکند - از کلیدواژه
finalبرای جلوگیری از وراثت استفاده کنید - ایمنی نوع برای شناسههای سفارش فراهم میکند
- اعتبارسنجی میکند که شناسه عدد صحیح مثبت باشد
گام 2: ایجاد مجموعه شناسه (اختیاری اما توصیه میشود)
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\BaseCustomCollection;
final class OrderIdCollection extends BaseCustomCollection
{
public function __construct(array $items = [])
{
// اعتبارسنجی همه آیتمها به عنوان نمونه OrderId
foreach ($items as $item) {
if (!$item instanceof OrderId) {
throw new \InvalidArgumentException(
'All items must be instances of OrderId'
);
}
}
parent::__construct($items);
}
/**
* تبدیل مجموعه به آرایه مقادیر شناسه
*/
public function toValues(): array
{
return array_map(
fn(OrderId $id) => $id->value,
$this->items
);
}
}
گام 3: ایجاد شیء انتقال داده (DTO)
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\DataTransferObject;
use App\Domains\User\UserId;
use DateTimeInterface;
final class OrderDTO implements DataTransferObject
{
public function __construct(
public readonly OrderId $id,
public readonly string $orderNumber,
public readonly UserId $userId,
public readonly int $totalAmount,
public readonly string $status,
public readonly DateTimeInterface $createdAt,
public readonly ?DateTimeInterface $completedAt = null,
) {
}
public function toArray(): array
{
return [
'id' => $this->id->value,
'order_number' => $this->orderNumber,
'user_id' => $this->userId->value,
'total_amount' => $this->totalAmount,
'status' => $this->status,
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
'completed_at' => $this->completedAt?->format('Y-m-d H:i:s'),
];
}
/**
* بررسی اینکه آیا سفارش تکمیل شده است
*/
public function isCompleted(): bool
{
return $this->status === 'completed';
}
/**
* بررسی اینکه آیا سفارش در انتظار است
*/
public function isPending(): bool
{
return $this->status === 'pending';
}
}
- همه ویژگیها
public readonly(تغییرناپذیر) - استفاده از type hint برای همه ویژگیها
- پارامترهای الزامی اول، پارامترهای اختیاری آخر
- میتواند شامل متدهای کمکی خاص دامنه باشد
- پیادهسازی
toArray()برای سریالسازی
گام 4: ایجاد مجموعه DTO
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\BaseCustomCollection;
final class OrderDTOCollection extends BaseCustomCollection
{
public function __construct(array $items = [])
{
// اعتبارسنجی همه آیتمها به عنوان نمونه OrderDTO
foreach ($items as $item) {
if (!$item instanceof OrderDTO) {
throw new \InvalidArgumentException(
'All items must be instances of OrderDTO'
);
}
}
parent::__construct($items);
}
/**
* فیلتر سفارشات تکمیلشده
*/
public function completed(): self
{
$completed = array_filter(
$this->items,
fn(OrderDTO $order) => $order->isCompleted()
);
return new self(array_values($completed));
}
/**
* فیلتر سفارشات در انتظار
*/
public function pending(): self
{
$pending = array_filter(
$this->items,
fn(OrderDTO $order) => $order->isPending()
);
return new self(array_values($pending));
}
/**
* دریافت مبلغ کل همه سفارشات
*/
public function totalAmount(): int
{
return array_reduce(
$this->items,
fn(int $carry, OrderDTO $order) => $carry + $order->totalAmount,
0
);
}
}
- مجموعه type-safe برای OrderDTO
- میتواند شامل متدهای فیلترینگ خاص دامنه باشد
- عملیات تغییرناپذیر (برگرداندن نمونههای جدید)
- عملیات کسبوکار معنادار فراهم میکند
گام 5: ایجاد رابط پرسوجو
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\QueryInterface;
use App\Domains\User\UserId;
interface OrderQuery extends QueryInterface
{
/**
* دریافت سفارش با شناسه (در صورت عدم یافتن استثنا پرتاب میکند)
*/
public function getById(OrderId $id): OrderDTO;
/**
* یافتن سفارش با شناسه (در صورت عدم یافتن null برمیگرداند)
*/
public function findById(OrderId $id): ?OrderDTO;
/**
* یافتن چندین سفارش با شناسهها
*/
public function findByIds(OrderIdCollection $ids): OrderDTOCollection;
/**
* بررسی وجود سفارش
*/
public function exists(OrderId $id): bool;
/**
* یافتن سفارش با شماره سفارش
*/
public function findByOrderNumber(string $orderNumber): ?OrderDTO;
/**
* دریافت همه سفارشات یک کاربر
*/
public function getOrdersByUser(UserId $userId): OrderDTOCollection;
/**
* دریافت سفارشات تکمیلشده یک کاربر
*/
public function getCompletedOrdersByUser(UserId $userId): OrderDTOCollection;
/**
* دریافت سفارشات در انتظار یک کاربر
*/
public function getPendingOrdersByUser(UserId $userId): OrderDTOCollection;
}
QueryInterfaceرا extend میکند- فقط متدهای فقط-خواندنی (بدون create/update/delete)
- استفاده از Value Objects برای پارامترها (OrderId، UserId)
- برگرداندن DTOها یا مجموعههای DTO
- استفاده از
get*برای متدهایی که استثنا پرتاب میکنند - استفاده از
find*برای متدهایی که null برمیگردانند - مستندسازی هر متد با PHPDoc
گام 6: ایجاد پیادهسازی پرسوجو
<?php
declare(strict_types=1);
namespace App\Modules\Orders\Services\Query;
use App\Domains\Order\OrderDTO;
use App\Domains\Order\OrderDTOCollection;
use App\Domains\Order\OrderId;
use App\Domains\Order\OrderIdCollection;
use App\Domains\Order\OrderQuery;
use App\Domains\User\UserId;
use App\Modules\Orders\Entities\Order;
use App\Modules\Orders\Repositories\OrderRepository;
final class OrderQueryImplementation implements OrderQuery
{
public function __construct(
private readonly OrderRepository $repository
) {
}
public function getById(OrderId $id): OrderDTO
{
$order = $this->repository->findOrFail($id->value);
return $this->toDTO($order);
}
public function findById(OrderId $id): ?OrderDTO
{
$order = $this->repository->find($id->value);
return $order ? $this->toDTO($order) : null;
}
public function findByIds(OrderIdCollection $ids): OrderDTOCollection
{
if ($ids->isEmpty()) {
return OrderDTOCollection::empty();
}
$orders = $this->repository
->newQuery()
->whereIn('id', $ids->toValues())
->get();
$dtos = array_map(
fn(Order $order) => $this->toDTO($order),
$orders->all()
);
return new OrderDTOCollection($dtos);
}
public function exists(OrderId $id): bool
{
return $this->repository
->newQuery()
->where('id', $id->value)
->exists();
}
public function findByOrderNumber(string $orderNumber): ?OrderDTO
{
$order = $this->repository
->newQuery()
->where('order_number', $orderNumber)
->first();
return $order ? $this->toDTO($order) : null;
}
public function getOrdersByUser(UserId $userId): OrderDTOCollection
{
$orders = $this->repository
->newQuery()
->where('user_id', $userId->value)
->orderBy('created_at', 'desc')
->get();
$dtos = array_map(
fn(Order $order) => $this->toDTO($order),
$orders->all()
);
return new OrderDTOCollection($dtos);
}
public function getCompletedOrdersByUser(UserId $userId): OrderDTOCollection
{
$orders = $this->repository
->newQuery()
->where('user_id', $userId->value)
->where('status', 'completed')
->orderBy('completed_at', 'desc')
->get();
$dtos = array_map(
fn(Order $order) => $this->toDTO($order),
$orders->all()
);
return new OrderDTOCollection($dtos);
}
public function getPendingOrdersByUser(UserId $userId): OrderDTOCollection
{
$orders = $this->repository
->newQuery()
->where('user_id', $userId->value)
->where('status', 'pending')
->orderBy('created_at', 'desc')
->get();
$dtos = array_map(
fn(Order $order) => $this->toDTO($order),
$orders->all()
);
return new OrderDTOCollection($dtos);
}
/**
* تبدیل موجودیت Order به OrderDTO
*/
private function toDTO(Order $order): OrderDTO
{
return new OrderDTO(
id: new OrderId($order->id),
orderNumber: $order->order_number,
userId: new UserId($order->user_id),
totalAmount: $order->total_amount,
status: $order->status,
createdAt: $order->created_at,
completedAt: $order->completed_at,
);
}
}
- رابط پرسوجو را پیادهسازی میکند
- از ریپوزیتوری برای دسترسی به داده استفاده میکند
- متد خصوصی
toDTO()برای نگاشت موجودیت - DTOها را برمیگرداند، هرگز موجودیتها را نه
- از Value Objects برای شناسهها استفاده میکند
- مجموعههای خالی را به خوبی مدیریت میکند
گام 7: ثبت در QueryServiceProvider
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Domains\Order\OrderQuery;
use App\Modules\Orders\Services\Query\OrderQueryImplementation;
use App\Services\QueryServiceLocator;
use Illuminate\Support\ServiceProvider;
class QueryServiceProvider extends ServiceProvider
{
private array $queryServices = [
'users' => UserQuery::class,
'carts' => CartQuery::class,
'orders' => OrderQuery::class, // ← این را اضافه کنید
// ... سایر پرسوجوها
];
private array $implementations = [
UserQuery::class => UserQueryImplementation::class,
CartQuery::class => CartQueryImplementation::class,
OrderQuery::class => OrderQueryImplementation::class, // ← این را اضافه کنید
// ... سایر پیادهسازیها
];
public function register(): void
{
// ثبت QueryServiceLocator به عنوان singleton
$this->app->singleton(QueryServiceLocator::class, function () {
$locator = new QueryServiceLocator();
foreach ($this->queryServices as $context => $interface) {
$locator->register($context, $interface);
}
return $locator;
});
// اتصال رابطها به پیادهسازیها
foreach ($this->implementations as $interface => $implementation) {
$this->app->bind($interface, $implementation);
}
}
public function boot(): void
{
// منطق boot در صورت نیاز
}
}
- نام زمینه را به آرایه
$queryServicesاضافه کنید - نگاشت رابط به پیادهسازی را اضافه کنید
- نام زمینه باید توصیفی باشد (مثلاً 'orders'، 'users')
- Laravel به صورت خودکار وابستگیها را حل میکند
الگوهای استفاده
- تزریق وابستگی مستقیم (توصیه میشود)
- QueryServiceLocator (پویا)
- استفاده در شنونده رویداد
- عملیات دستهای
بهترین برای: زمانی که میدانید در زمان کامپایل به کدام رابط پرسوجو نیاز دارید.
<?php
namespace App\Modules\Accounting\Services;
use App\Domains\Order\OrderQuery;
use App\Domains\Order\OrderId;
use App\Domains\User\UserQuery;
use App\Domains\User\UserId;
final class InvoiceService
{
public function __construct(
private readonly OrderQuery $orderQuery,
private readonly UserQuery $userQuery,
) {
}
public function generateInvoice(int $orderId): InvoiceDTO
{
// دریافت داده سفارش
$order = $this->orderQuery->getById(new OrderId($orderId));
// دریافت داده کاربر
$user = $this->userQuery->getById($order->userId);
// تولید فاکتور
return new InvoiceDTO(
orderNumber: $order->orderNumber,
customerName: $user->fullName,
totalAmount: $order->totalAmount,
// ...
);
}
}
- ✅ Type-safe (تکمیل خودکار IDE کار میکند)
- ✅ آسان برای تست (میتوان رابطها را Mock کرد)
- ✅ وابستگیهای واضح
- ✅ اعتبارسنجی زمان کامپایل
بهترین برای: زمانی که رابط پرسوجو در زمان اجرا تعیین میشود.
<?php
namespace App\Modules\Reports\Services;
use App\Services\QueryServiceLocator;
use App\Domains\Order\OrderId;
final class DynamicReportService
{
public function __construct(
private readonly QueryServiceLocator $locator
) {
}
public function generateReport(string $entityType, int $entityId): array
{
// حل سرویس پرسوجو بر اساس مقدار زمان اجرا
$query = $this->locator->resolve($entityType);
// استفاده از پرسوجو (نیاز به cast برای ایمنی نوع)
$entity = $query->getById(new OrderId($entityId));
return [
'type' => $entityType,
'data' => $entity->toArray(),
];
}
}
مزایا:
- ✅ انعطافپذیر برای سناریوهای پویا
- ✅ میتواند پرسوجوهای مختلف را در زمان اجرا حل کند
معایب:
- ⚠️ کمتر type-safe (QueryInterface برمیگرداند)
- ⚠️ ممکن است نیاز به cast برای متدهای خاص باشد
بهترین برای: شنوندگان رویداد که به داده از چندین ماژول نیاز دارند.
<?php
namespace App\Modules\Notifications\Listeners;
use App\Domains\Order\OrderQuery;
use App\Domains\Order\OrderId;
use App\Domains\User\UserQuery;
use App\Events\OrderCompletedEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
final class SendOrderCompletionEmail implements ShouldQueue
{
public function __construct(
private readonly OrderQuery $orderQuery,
private readonly UserQuery $userQuery,
) {
}
public function handle(OrderCompletedEvent $event): void
{
// دریافت جزئیات سفارش
$order = $this->orderQuery->getById(
new OrderId($event->orderId)
);
// دریافت جزئیات کاربر
$user = $this->userQuery->getById($order->userId);
// ارسال ایمیل
Mail::to($user->email->value)
->send(new OrderCompletedMail($order, $user));
}
}
بهترین برای: بارگذاری کارآمد چندین موجودیت.
<?php
namespace App\Modules\Analytics\Services;
use App\Domains\Order\OrderQuery;
use App\Domains\Order\OrderIdCollection;
use App\Domains\Order\OrderId;
final class OrderAnalyticsService
{
public function __construct(
private readonly OrderQuery $orderQuery
) {
}
public function calculateTotalRevenue(array $orderIds): int
{
// ایجاد مجموعه شناسه
$idCollection = new OrderIdCollection(
array_map(fn($id) => new OrderId($id), $orderIds)
);
// واکشی همه سفارشات در یک پرسوجو
$orders = $this->orderQuery->findByIds($idCollection);
// محاسبه مجموع
return $orders->totalAmount();
}
}
بهترین شیوهها
1. طراحی رابط
- ✅ انجام دهید
- ❌ انجام ندهید
interface OrderQuery extends QueryInterface
{
// نامهای متد واضح و توصیفی
public function getById(OrderId $id): OrderDTO;
public function findByOrderNumber(string $orderNumber): ?OrderDTO;
public function getCompletedOrdersByUser(UserId $userId): OrderDTOCollection;
}
interface OrderQuery extends QueryInterface
{
// خیلی عمومی
public function get(int $id): array;
// عملیات نوشتن در رابط پرسوجو
public function update(int $id, array $data): void;
// برگرداندن موجودیتها
public function getById(int $id): Order;
}
2. طراحی DTO
- ✅ انجام دهید
- ❌ انجام ندهید
final class OrderDTO implements DataTransferObject
{
public function __construct(
public readonly OrderId $id, // Value Objects
public readonly string $orderNumber, // انواع ابتدایی
public readonly UserId $userId, // VOهای مرتبط
public readonly DateTimeInterface $createdAt, // رابطها
) {
}
// متدهای کمکی مشکلی ندارند
public function isCompleted(): bool
{
return $this->status === 'completed';
}
}
class OrderDTO implements DataTransferObject
{
// ویژگیهای قابل تغییر
public OrderId $id;
public string $orderNumber;
// Setterها
public function setOrderNumber(string $number): void
{
$this->orderNumber = $number;
}
// منطق کسبوکار
public function processPayment(): void
{
// منطق کسبوکار در DTO جایی ندارد
}
}
3. الگوهای پیادهسازی
- ✅ انجام دهید
- ❌ انجام ندهید
final class OrderQueryImplementation implements OrderQuery
{
public function __construct(
private readonly OrderRepository $repository
) {
}
public function getById(OrderId $id): OrderDTO
{
$order = $this->repository->findOrFail($id->value);
return $this->toDTO($order);
}
private function toDTO(Order $order): OrderDTO
{
return new OrderDTO(
id: new OrderId($order->id),
orderNumber: $order->order_number,
// ...
);
}
}
class OrderQueryImplementation implements OrderQuery
{
// دسترسی مستقیم به پایگاه داده
public function getById(OrderId $id): OrderDTO
{
$order = DB::table('orders')->find($id->value);
return new OrderDTO(...);
}
// برگرداندن موجودیتها
public function getById(OrderId $id): Order
{
return $this->repository->findOrFail($id->value);
}
}
4. قراردادهای نامگذاری
| پیشوند متد | رفتار | نوع برگشتی |
|---|---|---|
get* | در صورت عدم یافتن استثنا پرتاب میکند | DTO یا Collection |
find* | در صورت عدم یافتن null برمیگرداند | ?DTO یا Collection |
exists* | وجود را بررسی میکند | bool |
count* | رکوردها را میشمارد | int |
public function getById(OrderId $id): OrderDTO; // در صورت عدم یافتن پرتاب میکند
public function findById(OrderId $id): ?OrderDTO; // در صورت عدم یافتن null برمیگرداند
public function exists(OrderId $id): bool; // true/false
public function countByUser(UserId $userId): int; // شمارش
5. مدیریت خطا
- ✅ انجام دهید
- ❌ انجام ندهید
public function getById(OrderId $id): OrderDTO
{
// اجازه دهید ریپوزیتوری ModelNotFoundException پرتاب کند
$order = $this->repository->findOrFail($id->value);
return $this->toDTO($order);
}
public function findById(OrderId $id): ?OrderDTO
{
// برای عدم یافتن null برگردانید
$order = $this->repository->find($id->value);
return $order ? $this->toDTO($order) : null;
}
public function getById(OrderId $id): OrderDTO
{
try {
$order = $this->repository->findOrFail($id->value);
return $this->toDTO($order);
} catch (ModelNotFoundException $e) {
// استثناها را گرفته و مخفی نکنید
return null;
}
}
استراتژیهای تست
تست واحد با Mock
<?php
namespace Tests\Unit\Modules\Accounting\Services;
use App\Domains\Order\OrderDTO;
use App\Domains\Order\OrderId;
use App\Domains\Order\OrderQuery;
use App\Domains\User\UserDTO;
use App\Domains\User\UserId;
use App\Domains\User\UserQuery;
use App\Modules\Accounting\Services\InvoiceService;
use Mockery;
use Tests\TestCase;
final class InvoiceServiceTest extends TestCase
{
public function test_generates_invoice_successfully(): void
{
// آمادهسازی
$orderId = new OrderId(1);
$userId = new UserId(10);
$orderQuery = Mockery::mock(OrderQuery::class);
$orderQuery->shouldReceive('getById')
->once()
->with(Mockery::on(fn($id) => $id->equals($orderId)))
->andReturn(new OrderDTO(
id: $orderId,
orderNumber: 'ORD-001',
userId: $userId,
totalAmount: 10000,
status: 'completed',
createdAt: now(),
));
$userQuery = Mockery::mock(UserQuery::class);
$userQuery->shouldReceive('getById')
->once()
->with(Mockery::on(fn($id) => $id->equals($userId)))
->andReturn(new UserDTO(
id: $userId,
fullName: 'جان دو',
morphClass: 'App\Models\User',
registeredAt: now(),
mobile: null,
));
$service = new InvoiceService($orderQuery, $userQuery);
// اجرا
$invoice = $service->generateInvoice(1);
// بررسی
$this->assertEquals('ORD-001', $invoice->orderNumber);
$this->assertEquals('جان دو', $invoice->customerName);
$this->assertEquals(10000, $invoice->totalAmount);
}
}
تست یکپارچگی
<?php
namespace Tests\Integration\Services\Query;
use App\Domains\Order\OrderId;
use App\Domains\Order\OrderQuery;
use App\Domains\User\UserId;
use App\Modules\Orders\Entities\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class OrderQueryImplementationTest extends TestCase
{
use RefreshDatabase;
private OrderQuery $orderQuery;
protected function setUp(): void
{
parent::setUp();
$this->orderQuery = app(OrderQuery::class);
}
public function test_gets_order_by_id(): void
{
// آمادهسازی
$order = Order::factory()->create([
'order_number' => 'ORD-001',
'total_amount' => 10000,
]);
// اجرا
$dto = $this->orderQuery->getById(new OrderId($order->id));
// بررسی
$this->assertEquals($order->id, $dto->id->value);
$this->assertEquals('ORD-001', $dto->orderNumber);
$this->assertEquals(10000, $dto->totalAmount);
}
public function test_returns_null_when_order_not_found(): void
{
// اجرا
$dto = $this->orderQuery->findById(new OrderId(999));
// بررسی
$this->assertNull($dto);
}
public function test_gets_orders_by_user(): void
{
// آمادهسازی
$userId = 1;
Order::factory()->count(3)->create(['user_id' => $userId]);
Order::factory()->count(2)->create(['user_id' => 2]);
// اجرا
$orders = $this->orderQuery->getOrdersByUser(new UserId($userId));
// بررسی
$this->assertCount(3, $orders);
}
}
الگوهای رایج و مثالها
الگو: پشتیبانی صفحهبندی
مشاهده پیادهسازی
interface OrderQuery extends QueryInterface
{
public function paginateByUser(
UserId $userId,
int $page = 1,
int $perPage = 15
): OrderPaginationDTO;
}
final class OrderPaginationDTO implements DataTransferObject
{
public function __construct(
public readonly OrderDTOCollection $items,
public readonly int $total,
public readonly int $currentPage,
public readonly int $perPage,
public readonly int $lastPage,
) {
}
public function toArray(): array
{
return [
'items' => array_map(
fn(OrderDTO $order) => $order->toArray(),
$this->items->toArray()
),
'total' => $this->total,
'current_page' => $this->currentPage,
'per_page' => $this->perPage,
'last_page' => $this->lastPage,
];
}
}
الگو: پرسوجوهای پیچیده با فیلترها
مشاهده پیادهسازی
final class OrderFilterDTO
{
public function __construct(
public readonly ?string $status = null,
public readonly ?DateTimeInterface $fromDate = null,
public readonly ?DateTimeInterface $toDate = null,
public readonly ?int $minAmount = null,
public readonly ?int $maxAmount = null,
) {
}
}
interface OrderQuery extends QueryInterface
{
public function findByFilters(OrderFilterDTO $filters): OrderDTOCollection;
}
// پیادهسازی
public function findByFilters(OrderFilterDTO $filters): OrderDTOCollection
{
$query = $this->repository->newQuery();
if ($filters->status !== null) {
$query->where('status', $filters->status);
}
if ($filters->fromDate !== null) {
$query->where('created_at', '>=', $filters->fromDate);
}
if ($filters->toDate !== null) {
$query->where('created_at', '<=', $filters->toDate);
}
if ($filters->minAmount !== null) {
$query->where('total_amount', '>=', $filters->minAmount);
}
if ($filters->maxAmount !== null) {
$query->where('total_amount', '<=', $filters->maxAmount);
}
$orders = $query->get();
$dtos = array_map(
fn(Order $order) => $this->toDTO($order),
$orders->all()
);
return new OrderDTOCollection($dtos);
}
الگو: DTOهای تو در تو
مشاهده پیادهسازی
final class OrderWithItemsDTO implements DataTransferObject
{
public function __construct(
public readonly OrderId $id,
public readonly string $orderNumber,
public readonly OrderItemDTOCollection $items, // مجموعه تو در تو
public readonly int $totalAmount,
) {
}
public function toArray(): array
{
return [
'id' => $this->id->value,
'order_number' => $this->orderNumber,
'items' => array_map(
fn(OrderItemDTO $item) => $item->toArray(),
$this->items->toArray()
),
'total_amount' => $this->totalAmount,
];
}
}
interface OrderQuery extends QueryInterface
{
public function getWithItems(OrderId $id): OrderWithItemsDTO;
}
عیبیابی
مشکل: رابط یافت نشد
Interface [App\Domains\Order\OrderQuery] does not exist
راهحل
- بررسی وجود فایل رابط در مسیر صحیح
- بررسی تطابق namespace با ساختار دایرکتوری
- اجرای
composer dump-autoload - پاک کردن کش Laravel:
php artisan cache:clear
مشکل: پیادهسازی حل نشد
Target [App\Domains\Order\OrderQuery] is not instantiable
راهحل
- بررسی ثبت پیادهسازی در
QueryServiceProvider - بررسی اتصال رابط به پیادهسازی
- اطمینان از وجود کلاس پیادهسازی
- اجرای
php artisan config:clear
مشکل: خطای نوع با Value Objects
Argument #1 must be of type OrderId, int given
راهحل
- ❌ اشتباه
- ✅ صحیح
$order = $orderQuery->getById(1);
$order = $orderQuery->getById(new OrderId(1));
مشکل: وابستگیهای دایرهای
Circular dependency detected
راهحل
- رابطهای پرسوجو باید فقط به رابطهای پرسوجوی دیگر وابسته باشند
- هرگز وابستگیهای دایرهای بین ماژولها ایجاد نکنید
- از رویدادها برای عملیات نوشتن به جای پرسوجوها استفاده کنید
مشکل: مشکلات عملکرد
- اجرای کند پرسوجو
- مشکلات پرسوجوی N+1
- استفاده بالای حافظه
راهحلها
1. استفاده از eager loading در پیادهسازی:
public function getWithItems(OrderId $id): OrderWithItemsDTO
{
$order = $this->repository
->newQuery()
->with('items') // Eager load
->findOrFail($id->value);
return $this->toDTO($order);
}
2. استفاده از عملیات دستهای:
- ❌ مشکل N+1
- ✅ بارگذاری دستهای
foreach ($orderIds as $id) {
$order = $orderQuery->getById(new OrderId($id));
}
$idCollection = new OrderIdCollection(
array_map(fn($id) => new OrderId($id), $orderIds)
);
$orders = $orderQuery->findByIds($idCollection);
3. افزودن کش در صورت نیاز:
public function getById(OrderId $id): OrderDTO
{
return Cache::remember(
"order:{$id->value}",
3600,
fn() => $this->fetchFromDatabase($id)
);
}
نتیجهگیری
زیرساخت پرسوجو یک روش قوی و type-safe برای خواندن داده در مرزهای ماژول در عین حفظ جداسازی سست فراهم میکند. با پیروی از این راهنمای پیادهسازی و بهترین شیوهها، میتوانید:
- ✅ کد قابل نگهداری و قابل تست ایجاد کنید
- ✅ مرزهای معماری را اعمال کنید
- ✅ تکامل مستقل ماژول را فعال کنید
- ✅ از نیازهای مقیاسپذیری آینده پشتیبانی کنید
از پرسوجوها برای خواندن داده استفاده کنید، از رویدادها برای فعالسازی عملیات.