# Repository Pattern

## Overview

The Repository Pattern provides an abstraction layer between the domain and data mapping layers. It centralizes data access logic and provides a collection-like interface for accessing domain objects.

## Benefits

- **Testability**: Easy to mock for unit testing
- **Flexibility**: Switch data sources without changing business logic
- **Separation of Concerns**: Isolates data access from business logic
- **Consistency**: Standardized data access methods across the application

## Implementation Structure

```
app/Domains/{Domain}/
├── Contracts/
│   └── {Entity}RepositoryContract.php    # Interface definition
└── Repositories/
    └── {Entity}Repository.php            # Implementation
```

## Contract (Interface)

Contracts define the methods that repositories must implement.

**Location**: `app/Domains/Catalog/Categories/Contracts/CategoryRepositoryContract.php`

```php
<?php

namespace App\Domains\Catalog\Categories\Contracts;

use App\Domains\Catalog\Categories\Models\Category;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

interface CategoryRepositoryContract
{
    /**
     * Get all categories
     */
    public function all(): Collection;

    /**
     * Get paginated categories
     */
    public function paginate(int $perPage = 15): LengthAwarePaginator;

    /**
     * Find category by ID
     */
    public function find(int $id): ?Category;

    /**
     * Find category by ID or throw exception
     */
    public function findOrFail(int $id): Category;

    /**
     * Create a new category
     */
    public function create(array $data): Category;

    /**
     * Update an existing category
     */
    public function update(Category $category, array $data): bool;

    /**
     * Delete a category (soft delete)
     */
    public function delete(Category $category): bool;

    /**
     * Force delete a category
     */
    public function forceDelete(Category $category): bool;

    /**
     * Restore a soft-deleted category
     */
    public function restore(Category $category): bool;

    /**
     * Find category by slug
     */
    public function findBySlug(string $slug): ?Category;

    /**
     * Find category by code
     */
    public function findByCode(string $code): ?Category;

    /**
     * Get all active categories
     */
    public function active(): Collection;

    /**
     * Get children of a parent category
     */
    public function withChildren(int $parentId): Collection;

    /**
     * Get root categories (no parent)
     */
    public function rootCategories(): Collection;

    /**
     * Search categories by query
     */
    public function search(string $query): Collection;
}
```

## Repository Implementation

The repository implements the contract and contains actual data access logic.

**Location**: `app/Domains/Catalog/Categories/Repositories/CategoryRepository.php`

```php
<?php

namespace App\Domains\Catalog\Categories\Repositories;

use App\Domains\Catalog\Categories\Contracts\CategoryRepositoryContract;
use App\Domains\Catalog\Categories\Models\Category;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

class CategoryRepository implements CategoryRepositoryContract
{
    protected Category $model;

    public function __construct(Category $model)
    {
        $this->model = $model;
    }

    public function all(): Collection
    {
        return $this->model->all();
    }

    public function paginate(int $perPage = 15): LengthAwarePaginator
    {
        return $this->model
            ->orderBy('created_at', 'desc')
            ->paginate($perPage);
    }

    public function find(int $id): ?Category
    {
        return $this->model->find($id);
    }

    public function findOrFail(int $id): Category
    {
        return $this->model->findOrFail($id);
    }

    public function create(array $data): Category
    {
        return $this->model->create($data);
    }

    public function update(Category $category, array $data): bool
    {
        return $category->update($data);
    }

    public function delete(Category $category): bool
    {
        return $category->delete();
    }

    public function forceDelete(Category $category): bool
    {
        return $category->forceDelete();
    }

    public function restore(Category $category): bool
    {
        return $category->restore();
    }

    public function findBySlug(string $slug): ?Category
    {
        return $this->model->where('slug', $slug)->first();
    }

    public function findByCode(string $code): ?Category
    {
        return $this->model->where('code', $code)->first();
    }

    public function active(): Collection
    {
        return $this->model->where('is_active', true)->get();
    }

    public function withChildren(int $parentId): Collection
    {
        return $this->model
            ->where('parent_id', $parentId)
            ->with('children')
            ->get();
    }

    public function rootCategories(): Collection
    {
        return $this->model
            ->whereNull('parent_id')
            ->get();
    }

    public function search(string $query): Collection
    {
        return $this->model
            ->where('name->en', 'like', "%{$query}%")
            ->orWhere('name->ar', 'like', "%{$query}%")
            ->orWhere('code', 'like', "%{$query}%")
            ->get();
    }
}
```

## Service Provider Binding

Register the repository binding in `AppServiceProvider`.

**Location**: `app/Providers/AppServiceProvider.php`

```php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind contract to implementation
        $this->app->bind(
            \App\Domains\Catalog\Categories\Contracts\CategoryRepositoryContract::class,
            \App\Domains\Catalog\Categories\Repositories\CategoryRepository::class
        );

        // More bindings...
        $this->app->bind(
            \App\Domains\Catalog\Products\Contracts\ProductRepositoryContract::class,
            \App\Domains\Catalog\Products\Repositories\ProductRepository::class
        );

        $this->app->bind(
            \App\Domains\Catalog\Inventory\Contracts\InventoryTypeRepositoryContract::class,
            \App\Domains\Catalog\Inventory\Repositories\InventoryTypeRepository::class
        );
    }
}
```

## Usage in Services

Inject the contract (not the implementation) into services.

```php
<?php

namespace App\Domains\Catalog\Categories\Services;

use App\Domains\Catalog\Categories\Contracts\CategoryRepositoryContract;
use App\Domains\Catalog\Categories\DTOs\CategoryDTO;
use App\Domains\Catalog\Categories\Models\Category;

class CategoryService
{
    public function __construct(
        private CategoryRepositoryContract $repository
    ) {}

    public function getAllCategories(): Collection
    {
        return $this->repository->all();
    }

    public function findCategory(int $id): ?Category
    {
        return $this->repository->find($id);
    }

    public function createCategory(CategoryDTO $dto): Category
    {
        // Business logic validation
        $this->validateBusinessRules($dto);

        // Create via repository
        return $this->repository->create($dto->toArray());
    }

    public function updateCategory(Category $category, CategoryDTO $dto): bool
    {
        // Business logic
        return $this->repository->update($category, $dto->toArray());
    }

    public function deleteCategory(Category $category): bool
    {
        // Check for dependencies
        if ($category->products()->exists()) {
            throw new \Exception('Cannot delete category with products');
        }

        return $this->repository->delete($category);
    }

    public function searchCategories(string $query): Collection
    {
        return $this->repository->search($query);
    }

    private function validateBusinessRules(CategoryDTO $dto): void
    {
        // Custom business validation
    }
}
```

## Usage in Controllers

```php
<?php

namespace App\Http\Controllers\V1\Catalog;

use App\Domains\Catalog\Categories\Services\CategoryService;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Catalog\StoreCategoryRequest;
use App\Http\Resources\V1\Catalog\CategoryResource;
use Illuminate\Http\JsonResponse;

class CategoryController extends Controller
{
    public function __construct(
        private CategoryService $categoryService
    ) {}

    public function index(): JsonResponse
    {
        $categories = $this->categoryService->getAllCategories();

        return response()->json([
            'success' => true,
            'data' => CategoryResource::collection($categories)
        ]);
    }

    public function show(int $id): JsonResponse
    {
        $category = $this->categoryService->findCategory($id);

        if (!$category) {
            return response()->json([
                'success' => false,
                'message' => 'Category not found'
            ], 404);
        }

        return response()->json([
            'success' => true,
            'data' => new CategoryResource($category)
        ]);
    }
}
```

## Testing with Mock Repositories

```php
<?php

namespace Tests\Unit\Services;

use App\Domains\Catalog\Categories\Contracts\CategoryRepositoryContract;
use App\Domains\Catalog\Categories\Models\Category;
use App\Domains\Catalog\Categories\Services\CategoryService;
use Mockery;
use Tests\TestCase;

class CategoryServiceTest extends TestCase
{
    private CategoryRepositoryContract $mockRepository;
    private CategoryService $service;

    protected function setUp(): void
    {
        parent::setUp();

        $this->mockRepository = Mockery::mock(CategoryRepositoryContract::class);
        $this->service = new CategoryService($this->mockRepository);
    }

    public function test_find_category_returns_category(): void
    {
        $category = new Category(['id' => 1, 'name' => 'Test']);

        $this->mockRepository
            ->shouldReceive('find')
            ->with(1)
            ->once()
            ->andReturn($category);

        $result = $this->service->findCategory(1);

        $this->assertEquals($category, $result);
    }

    public function test_find_category_returns_null_when_not_found(): void
    {
        $this->mockRepository
            ->shouldReceive('find')
            ->with(999)
            ->once()
            ->andReturn(null);

        $result = $this->service->findCategory(999);

        $this->assertNull($result);
    }
}
```

## Repository Contracts in the Project

| Domain | Contract | Location |
|--------|----------|----------|
| Catalog | `CategoryRepositoryContract` | `Domains/Catalog/Categories/Contracts/` |
| Catalog | `ProductRepositoryContract` | `Domains/Catalog/Products/Contracts/` |
| Catalog | `InventoryTypeRepositoryContract` | `Domains/Catalog/Inventory/Contracts/` |
| Maintenance | `MaintenanceAssetRepositoryContract` | `Domains/Maintenance/SubDomains/Assets/Contracts/` |
| Supply | `QuotationRepositoryContract` | `Domains/Supply/SubDomains/Quotations/Contracts/` |

## Best Practices

### Do

- Define clear, focused contracts
- Use dependency injection for repositories
- Keep repository methods simple and focused
- Use transactions for multi-step operations
- Return domain objects, not arrays

### Don't

- Put business logic in repositories
- Expose query builder methods directly
- Create "god" repositories with too many methods
- Skip the contract for "simple" repositories
- Use repositories in other repositories

## Related Patterns

- [Service Pattern](SERVICE_PATTERN.md) - Uses repositories for data access
- [DTO Pattern](DTO_PATTERN.md) - Transforms data for repository operations
