Query Infrastructure: Implementation Guide
Overview
The Query Infrastructure provides a standardized way for modules to read data from other modules without creating tight coupling. This guide covers the complete implementation process from creating your first query interface to advanced usage patterns.
Core Principle
Dependency Inversion: Consumer modules depend on abstractions (interfaces) defined in a shared kernel, while provider modules implement these interfaces within their own domain.
Architecture Components
1. Base Contracts (Shared Foundation)
Located in app/Contracts/:
- QueryInterface
- DomainId
- DataTransferObject
- BaseCustomCollection
<?php
declare(strict_types=1);
namespace App\Contracts;
interface QueryInterface
{
// Marker interface - no methods required
// All query interfaces must extend this
}
<?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. Domain Layer (Contracts)
Located in app/Domains/{Context}/:
This layer contains:
- Query interfaces
- DTOs (Data Transfer Objects)
- Domain IDs (Value Objects)
- DTO Collections
app/Domains/User/
├── UserQuery.php # Interface
├── UserDTO.php # Data Transfer Object
├── UserDTOCollection.php # DTO Collection
├── UserId.php # Domain ID (Value Object)
└── UserIdCollection.php # ID Collection
3. Implementation Layer
Located in app/Core/{Module}/Services/Query/ or app/Modules/{Module}/Services/Query/:
This layer contains:
- Concrete implementations of query interfaces
- Repository usage
- Entity to DTO mapping logic
4. Service Locator
Located in app/Services/QueryServiceLocator.php:
Manages registration and resolution of query services.
5. Service Provider
Located in app/Providers/QueryServiceProvider.php:
Registers all query interfaces and their implementations with Laravel's service container.
Step-by-Step Implementation
Step 1: Create Domain ID (Value Object)
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\DomainId;
final class OrderId extends DomainId
{
// Inherits all functionality from DomainId
// Can add domain-specific methods if needed
}
- Extends
DomainIdbase class - Use
finalkeyword to prevent inheritance - Provides type safety for order IDs
- Validates that ID is positive integer
Step 2: Create ID Collection (Optional but Recommended)
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\BaseCustomCollection;
final class OrderIdCollection extends BaseCustomCollection
{
public function __construct(array $items = [])
{
// Validate all items are OrderId instances
foreach ($items as $item) {
if (!$item instanceof OrderId) {
throw new \InvalidArgumentException(
'All items must be instances of OrderId'
);
}
}
parent::__construct($items);
}
/**
* Convert collection to array of ID values
*/
public function toValues(): array
{
return array_map(
fn(OrderId $id) => $id->value,
$this->items
);
}
}
Step 3: Create Data Transfer Object (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'),
];
}
/**
* Check if order is completed
*/
public function isCompleted(): bool
{
return $this->status === 'completed';
}
/**
* Check if order is pending
*/
public function isPending(): bool
{
return $this->status === 'pending';
}
}
- All properties are
public readonly(immutable) - Use type hints for all properties
- Required parameters first, optional parameters last
- Can include domain-specific helper methods
- Implements
toArray()for serialization
Step 4: Create DTO Collection
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\BaseCustomCollection;
final class OrderDTOCollection extends BaseCustomCollection
{
public function __construct(array $items = [])
{
// Validate all items are OrderDTO instances
foreach ($items as $item) {
if (!$item instanceof OrderDTO) {
throw new \InvalidArgumentException(
'All items must be instances of OrderDTO'
);
}
}
parent::__construct($items);
}
/**
* Filter completed orders
*/
public function completed(): self
{
$completed = array_filter(
$this->items,
fn(OrderDTO $order) => $order->isCompleted()
);
return new self(array_values($completed));
}
/**
* Filter pending orders
*/
public function pending(): self
{
$pending = array_filter(
$this->items,
fn(OrderDTO $order) => $order->isPending()
);
return new self(array_values($pending));
}
/**
* Get total amount of all orders
*/
public function totalAmount(): int
{
return array_reduce(
$this->items,
fn(int $carry, OrderDTO $order) => $carry + $order->totalAmount,
0
);
}
}
- Type-safe collection for OrderDTO
- Can include domain-specific filtering methods
- Immutable operations (return new instances)
- Provides meaningful business operations
Step 5: Create Query Interface
<?php
declare(strict_types=1);
namespace App\Domains\Order;
use App\Contracts\QueryInterface;
use App\Domains\User\UserId;
interface OrderQuery extends QueryInterface
{
/**
* Get order by ID (throws exception if not found)
*/
public function getById(OrderId $id): OrderDTO;
/**
* Find order by ID (returns null if not found)
*/
public function findById(OrderId $id): ?OrderDTO;
/**
* Find multiple orders by IDs
*/
public function findByIds(OrderIdCollection $ids): OrderDTOCollection;
/**
* Check if order exists
*/
public function exists(OrderId $id): bool;
/**
* Find order by order number
*/
public function findByOrderNumber(string $orderNumber): ?OrderDTO;
/**
* Get all orders for a user
*/
public function getOrdersByUser(UserId $userId): OrderDTOCollection;
/**
* Get completed orders for a user
*/
public function getCompletedOrdersByUser(UserId $userId): OrderDTOCollection;
/**
* Get pending orders for a user
*/
public function getPendingOrdersByUser(UserId $userId): OrderDTOCollection;
}
- Extends
QueryInterface - Only read-only methods (no create/update/delete)
- Use Value Objects for parameters (OrderId, UserId)
- Return DTOs or DTO Collections
- Use
get*for methods that throw exceptions - Use
find*for methods that return null - Document each method with PHPDoc
Step 6: Create Query Implementation
<?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);
}
/**
* Convert Order entity to 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,
);
}
}
- Implements the query interface
- Uses repository for data access
- Private
toDTO()method for entity mapping - Returns DTOs, never entities
- Uses Value Objects for IDs
- Handles empty collections gracefully
Step 7: Register in 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, // ← Add this
// ... other queries
];
private array $implementations = [
UserQuery::class => UserQueryImplementation::class,
CartQuery::class => CartQueryImplementation::class,
OrderQuery::class => OrderQueryImplementation::class, // ← Add this
// ... other implementations
];
public function register(): void
{
// Register QueryServiceLocator as singleton
$this->app->singleton(QueryServiceLocator::class, function () {
$locator = new QueryServiceLocator();
foreach ($this->queryServices as $context => $interface) {
$locator->register($context, $interface);
}
return $locator;
});
// Bind interfaces to implementations
foreach ($this->implementations as $interface => $implementation) {
$this->app->bind($interface, $implementation);
}
}
public function boot(): void
{
// Boot logic if needed
}
}
- Add context name to
$queryServicesarray - Add interface-to-implementation mapping
- Context name should be descriptive (e.g., 'orders', 'users')
- Laravel will automatically resolve dependencies
Usage Patterns
- Direct Dependency Injection (Recommended)
- QueryServiceLocator (Dynamic)
- Event Listener Usage
- Batch Operations
Best for: When you know which query interface you need at compile time.
<?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
{
// Get order data
$order = $this->orderQuery->getById(new OrderId($orderId));
// Get user data
$user = $this->userQuery->getById($order->userId);
// Generate invoice
return new InvoiceDTO(
orderNumber: $order->orderNumber,
customerName: $user->fullName,
totalAmount: $order->totalAmount,
// ...
);
}
}
- ✅ Type-safe (IDE autocomplete works)
- ✅ Easy to test (can mock interfaces)
- ✅ Clear dependencies
- ✅ Compile-time validation
Best for: When the query interface is determined at runtime.
<?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
{
// Resolve query service based on runtime value
$query = $this->locator->resolve($entityType);
// Use the query (need to cast for type safety)
$entity = $query->getById(new OrderId($entityId));
return [
'type' => $entityType,
'data' => $entity->toArray(),
];
}
}
Advantages:
- ✅ Flexible for dynamic scenarios
- ✅ Can resolve different queries at runtime
Disadvantages:
- ⚠️ Less type-safe (returns QueryInterface)
- ⚠️ May need casting for specific methods
Best for: Event listeners that need data from multiple modules.
<?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
{
// Get order details
$order = $this->orderQuery->getById(
new OrderId($event->orderId)
);
// Get user details
$user = $this->userQuery->getById($order->userId);
// Send email
Mail::to($user->email->value)
->send(new OrderCompletedMail($order, $user));
}
}
Best for: Loading multiple entities efficiently.
<?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
{
// Create ID collection
$idCollection = new OrderIdCollection(
array_map(fn($id) => new OrderId($id), $orderIds)
);
// Fetch all orders in one query
$orders = $this->orderQuery->findByIds($idCollection);
// Calculate total
return $orders->totalAmount();
}
}
Best Practices
1. Interface Design
- ✅ DO
- ❌ DON'T
interface OrderQuery extends QueryInterface
{
// Clear, descriptive method names
public function getById(OrderId $id): OrderDTO;
public function findByOrderNumber(string $orderNumber): ?OrderDTO;
public function getCompletedOrdersByUser(UserId $userId): OrderDTOCollection;
}
interface OrderQuery extends QueryInterface
{
// Too generic
public function get(int $id): array;
// Write operations in query interface
public function update(int $id, array $data): void;
// Returning entities
public function getById(int $id): Order;
}
2. DTO Design
- ✅ DO
- ❌ DON'T
final class OrderDTO implements DataTransferObject
{
public function __construct(
public readonly OrderId $id, // Value Objects
public readonly string $orderNumber, // Primitives
public readonly UserId $userId, // Related VOs
public readonly DateTimeInterface $createdAt, // Interfaces
) {
}
// Helper methods are OK
public function isCompleted(): bool
{
return $this->status === 'completed';
}
}
class OrderDTO implements DataTransferObject
{
// Mutable properties
public OrderId $id;
public string $orderNumber;
// Setters
public function setOrderNumber(string $number): void
{
$this->orderNumber = $number;
}
// Business logic
public function processPayment(): void
{
// Business logic doesn't belong in DTO
}
}
3. Implementation Patterns
- ✅ DO
- ❌ DON'T
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
{
// Direct database access
public function getById(OrderId $id): OrderDTO
{
$order = DB::table('orders')->find($id->value);
return new OrderDTO(...);
}
// Returning entities
public function getById(OrderId $id): Order
{
return $this->repository->findOrFail($id->value);
}
}
4. Naming Conventions
| Method Prefix | Behavior | Return Type |
|---|---|---|
get* | Throws exception if not found | DTO or Collection |
find* | Returns null if not found | ?DTO or Collection |
exists* | Checks existence | bool |
count* | Counts records | int |
public function getById(OrderId $id): OrderDTO; // Throws if not found
public function findById(OrderId $id): ?OrderDTO; // Returns null if not found
public function exists(OrderId $id): bool; // true/false
public function countByUser(UserId $userId): int; // Count
5. Error Handling
- ✅ DO
- ❌ DON'T
public function getById(OrderId $id): OrderDTO
{
// Let repository throw ModelNotFoundException
$order = $this->repository->findOrFail($id->value);
return $this->toDTO($order);
}
public function findById(OrderId $id): ?OrderDTO
{
// Return null for not found
$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) {
// Don't catch and hide exceptions
return null;
}
}
Testing Strategies
Unit Testing with Mocks
<?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
{
// Arrange
$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: 'John Doe',
morphClass: 'App\Models\User',
registeredAt: now(),
mobile: null,
));
$service = new InvoiceService($orderQuery, $userQuery);
// Act
$invoice = $service->generateInvoice(1);
// Assert
$this->assertEquals('ORD-001', $invoice->orderNumber);
$this->assertEquals('John Doe', $invoice->customerName);
$this->assertEquals(10000, $invoice->totalAmount);
}
}
Integration Testing
<?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
{
// Arrange
$order = Order::factory()->create([
'order_number' => 'ORD-001',
'total_amount' => 10000,
]);
// Act
$dto = $this->orderQuery->getById(new OrderId($order->id));
// Assert
$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
{
// Act
$dto = $this->orderQuery->findById(new OrderId(999));
// Assert
$this->assertNull($dto);
}
public function test_gets_orders_by_user(): void
{
// Arrange
$userId = 1;
Order::factory()->count(3)->create(['user_id' => $userId]);
Order::factory()->count(2)->create(['user_id' => 2]);
// Act
$orders = $this->orderQuery->getOrdersByUser(new UserId($userId));
// Assert
$this->assertCount(3, $orders);
}
}
Common Patterns and Examples
Pattern: Pagination Support
View Implementation
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,
];
}
}
Pattern: Complex Queries with Filters
View Implementation
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;
}
// Implementation
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);
}
Pattern: Nested DTOs
View Implementation
final class OrderWithItemsDTO implements DataTransferObject
{
public function __construct(
public readonly OrderId $id,
public readonly string $orderNumber,
public readonly OrderItemDTOCollection $items, // Nested collection
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;
}
Troubleshooting
Issue: Interface Not Found
Interface [App\Domains\Order\OrderQuery] does not exist
Solution
- Verify interface file exists at correct path
- Check namespace matches directory structure
- Run
composer dump-autoload - Clear Laravel cache:
php artisan cache:clear
Issue: Implementation Not Resolved
Target [App\Domains\Order\OrderQuery] is not instantiable
Solution
- Verify implementation is registered in
QueryServiceProvider - Check interface-to-implementation binding
- Ensure implementation class exists
- Run
php artisan config:clear
Issue: Type Error with Value Objects
Argument #1 must be of type OrderId, int given
Solution
- ❌ Wrong
- ✅ Correct
$order = $orderQuery->getById(1);
$order = $orderQuery->getById(new OrderId(1));
Issue: Circular Dependencies
Circular dependency detected
Solution
- Query interfaces should only depend on other query interfaces
- Never create circular dependencies between modules
- Use events for write operations instead of queries
Issue: Performance Problems
- Slow query execution
- N+1 query problems
- High memory usage
Solutions
1. Use eager loading in implementation:
public function getWithItems(OrderId $id): OrderWithItemsDTO
{
$order = $this->repository
->newQuery()
->with('items') // Eager load
->findOrFail($id->value);
return $this->toDTO($order);
}
2. Use batch operations:
- ❌ N+1 Problem
- ✅ Batch Load
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. Add caching if needed:
public function getById(OrderId $id): OrderDTO
{
return Cache::remember(
"order:{$id->value}",
3600,
fn() => $this->fetchFromDatabase($id)
);
}
Conclusion
The Query Infrastructure provides a robust, type-safe way to read data across module boundaries while maintaining loose coupling. By following this implementation guide and best practices, you can:
- ✅ Create maintainable, testable code
- ✅ Enforce architectural boundaries
- ✅ Enable independent module evolution
- ✅ Support future scalability needs
Use queries for reading data, use events for triggering operations.