Laravel المتقدم

نمط المستودع في Laravel

20 دقيقة الدرس 3 من 40

فهم نمط المستودع

نمط المستودع هو نمط تصميم يُنشئ طبقة تجريد بين منطق الأعمال لتطبيقك وطبقة الوصول إلى البيانات. يوفر رؤية أكثر توجهاً نحو الكائنات لقاعدة البيانات ويساعد في الحفاظ على كودك نظيفاً وقابلاً للاختبار وسهل الصيانة.

فوائد نمط المستودع:
  • فصل منطق الأعمال عن تنفيذ الوصول إلى البيانات
  • يجعل الكود أكثر قابلية للاختبار من خلال حقن التبعية
  • مركزية منطق الوصول إلى البيانات في مكان واحد
  • يسهل التبديل بين مصادر البيانات المختلفة
  • يقلل من تكرار الكود عبر Controllers والخدمات
  • يوفر واجهة برمجة تطبيقات متسقة لعمليات البيانات
متى لا تستخدم نمط المستودع: بالنسبة لتطبيقات CRUD البسيطة مع الوصول المباشر للبيانات، قد يضيف نمط المستودع تعقيداً غير ضروري. استخدمه عندما يكون لديك منطق أعمال معقد، أو تحتاج إلى قابلية اختبار أفضل، أو قد تقوم بتبديل مصادر البيانات في المستقبل.

تنفيذ المستودع الأساسي

لنبدأ بتنفيذ مستودع بسيط لنموذج Post.

إنشاء واجهة المستودع:
// app/Repositories/Contracts/PostRepositoryInterface.php
namespace App\Repositories\Contracts;

use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;

interface PostRepositoryInterface
{
    public function all(): Collection;

    public function find(int $id): ?Post;

    public function create(array $data): Post;

    public function update(int $id, array $data): bool;

    public function delete(int $id): bool;

    public function published(): Collection;

    public function findBySlug(string $slug): ?Post;
}
تنفيذ المستودع:
// app/Repositories/PostRepository.php
namespace App\Repositories;

use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;

class PostRepository implements PostRepositoryInterface
{
    protected $model;

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

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

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

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

    public function update(int $id, array $data): bool
    {
        $post = $this->find($id);

        if (!$post) {
            return false;
        }

        return $post->update($data);
    }

    public function delete(int $id): bool
    {
        $post = $this->find($id);

        if (!$post) {
            return false;
        }

        return $post->delete();
    }

    public function published(): Collection
    {
        return $this->model->where('status', 'published')
            ->orderBy('published_at', 'desc')
            ->get();
    }

    public function findBySlug(string $slug): ?Post
    {
        return $this->model->where('slug', $slug)->first();
    }
}
ربط المستودع:
// app/Providers/RepositoryServiceProvider.php
namespace App\Providers;

use App\Repositories\Contracts\PostRepositoryInterface;
use App\Repositories\PostRepository;
use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            PostRepositoryInterface::class,
            PostRepository::class
        );
    }
}

// لا تنسَ التسجيل في config/app.php
'providers' => [
    // ...
    App\Providers\RepositoryServiceProvider::class,
],
استخدام المستودع في Controllers:
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Http\Request;

class PostController extends Controller
{
    protected $postRepository;

    public function __construct(PostRepositoryInterface $postRepository)
    {
        $this->postRepository = $postRepository;
    }

    public function index()
    {
        $posts = $this->postRepository->published();

        return view('posts.index', compact('posts'));
    }

    public function show(string $slug)
    {
        $post = $this->postRepository->findBySlug($slug);

        if (!$post) {
            abort(404);
        }

        return view('posts.show', compact('post'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'status' => 'required|in:draft,published',
        ]);

        $post = $this->postRepository->create($validated);

        return redirect()
            ->route('posts.show', $post->slug)
            ->with('success', 'Post created successfully');
    }

    public function update(Request $request, int $id)
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'status' => 'required|in:draft,published',
        ]);

        $updated = $this->postRepository->update($id, $validated);

        if (!$updated) {
            return back()->with('error', 'Post not found');
        }

        return back()->with('success', 'Post updated successfully');
    }

    public function destroy(int $id)
    {
        $deleted = $this->postRepository->delete($id);

        if (!$deleted) {
            return back()->with('error', 'Post not found');
        }

        return redirect()
            ->route('posts.index')
            ->with('success', 'Post deleted successfully');
    }
}

مستودع متقدم مع فئة أساسية

أنشئ مستودعاً أساسياً لتقليل تكرار الكود عبر المستودعات المتعددة.

واجهة المستودع الأساسي:
// app/Repositories/Contracts/BaseRepositoryInterface.php
namespace App\Repositories\Contracts;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

interface BaseRepositoryInterface
{
    public function all(array $columns = ['*']): Collection;

    public function paginate(int $perPage = 15, array $columns = ['*']);

    public function find(int $id, array $columns = ['*']): ?Model;

    public function findOrFail(int $id, array $columns = ['*']): Model;

    public function findBy(string $field, $value, array $columns = ['*']): ?Model;

    public function findWhere(array $where, array $columns = ['*']): Collection;

    public function create(array $data): Model;

    public function update(int $id, array $data): bool;

    public function delete(int $id): bool;

    public function with(array $relations): self;

    public function orderBy(string $column, string $direction = 'asc'): self;
}
تنفيذ المستودع الأساسي:
// app/Repositories/BaseRepository.php
namespace App\Repositories;

use App\Repositories\Contracts\BaseRepositoryInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

abstract class BaseRepository implements BaseRepositoryInterface
{
    protected $model;
    protected $query;

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

    protected function resetQuery(): void
    {
        $this->query = $this->model->newQuery();
    }

    public function all(array $columns = ['*']): Collection
    {
        $result = $this->query->get($columns);
        $this->resetQuery();

        return $result;
    }

    public function paginate(int $perPage = 15, array $columns = ['*'])
    {
        $result = $this->query->paginate($perPage, $columns);
        $this->resetQuery();

        return $result;
    }

    public function find(int $id, array $columns = ['*']): ?Model
    {
        return $this->model->find($id, $columns);
    }

    public function findOrFail(int $id, array $columns = ['*']): Model
    {
        return $this->model->findOrFail($id, $columns);
    }

    public function findBy(string $field, $value, array $columns = ['*']): ?Model
    {
        return $this->model->where($field, $value)->first($columns);
    }

    public function findWhere(array $where, array $columns = ['*']): Collection
    {
        $query = $this->model->newQuery();

        foreach ($where as $field => $value) {
            if (is_array($value)) {
                [$field, $operator, $val] = $value;
                $query->where($field, $operator, $val);
            } else {
                $query->where($field, $value);
            }
        }

        return $query->get($columns);
    }

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

    public function update(int $id, array $data): bool
    {
        $model = $this->find($id);

        if (!$model) {
            return false;
        }

        return $model->update($data);
    }

    public function delete(int $id): bool
    {
        $model = $this->find($id);

        if (!$model) {
            return false;
        }

        return $model->delete();
    }

    public function with(array $relations): self
    {
        $this->query->with($relations);

        return $this;
    }

    public function orderBy(string $column, string $direction = 'asc'): self
    {
        $this->query->orderBy($column, $direction);

        return $this;
    }
}
توسيع المستودع الأساسي:
// app/Repositories/UserRepository.php
namespace App\Repositories;

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;

class UserRepository extends BaseRepository
{
    public function __construct(User $model)
    {
        parent::__construct($model);
    }

    public function findByEmail(string $email): ?User
    {
        return $this->findBy('email', $email);
    }

    public function active(): Collection
    {
        return $this->findWhere(['is_active' => true]);
    }

    public function verified(): Collection
    {
        return $this->findWhere([
            ['email_verified_at', '!=', null]
        ]);
    }

    public function admins(): Collection
    {
        return $this->findWhere(['role' => 'admin']);
    }

    public function withPosts(): self
    {
        return $this->with(['posts']);
    }
}

نمط المعايير للاستعلامات المعقدة

نمط المعايير يسمح لك ببناء شروط استعلام معقدة وقابلة لإعادة الاستخدام يمكن تطبيقها على المستودعات.

إنشاء واجهة المعايير:
// app/Repositories/Criteria/CriteriaInterface.php
namespace App\Repositories\Criteria;

use Illuminate\Database\Eloquent\Builder;

interface CriteriaInterface
{
    public function apply(Builder $query): Builder;
}
تنفيذ معايير محددة:
// app/Repositories/Criteria/PublishedCriteria.php
namespace App\Repositories\Criteria;

use Illuminate\Database\Eloquent\Builder;

class PublishedCriteria implements CriteriaInterface
{
    public function apply(Builder $query): Builder
    {
        return $query->where('status', 'published')
            ->where('published_at', '<=', now());
    }
}

// app/Repositories/Criteria/RecentCriteria.php
namespace App\Repositories\Criteria;

use Illuminate\Database\Eloquent\Builder;

class RecentCriteria implements CriteriaInterface
{
    protected $days;

    public function __construct(int $days = 30)
    {
        $this->days = $days;
    }

    public function apply(Builder $query): Builder
    {
        return $query->where('created_at', '>=', now()->subDays($this->days));
    }
}

// app/Repositories/Criteria/PopularCriteria.php
namespace App\Repositories\Criteria;

use Illuminate\Database\Eloquent\Builder;

class PopularCriteria implements CriteriaInterface
{
    protected $threshold;

    public function __construct(int $threshold = 100)
    {
        $this->threshold = $threshold;
    }

    public function apply(Builder $query): Builder
    {
        return $query->where('views', '>=', $this->threshold)
            ->orderBy('views', 'desc');
    }
}
مستودع محسّن مع دعم المعايير:
// app/Repositories/BaseRepository.php (محسّن)
namespace App\Repositories;

use App\Repositories\Contracts\BaseRepositoryInterface;
use App\Repositories\Criteria\CriteriaInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

abstract class BaseRepository implements BaseRepositoryInterface
{
    protected $model;
    protected $query;
    protected $criteria = [];

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

    protected function resetQuery(): void
    {
        $this->query = $this->model->newQuery();
    }

    public function pushCriteria(CriteriaInterface $criteria): self
    {
        $this->criteria[] = $criteria;

        return $this;
    }

    public function applyCriteria(): self
    {
        foreach ($this->criteria as $criteria) {
            $this->query = $criteria->apply($this->query);
        }

        $this->criteria = [];

        return $this;
    }

    public function all(array $columns = ['*']): Collection
    {
        $this->applyCriteria();
        $result = $this->query->get($columns);
        $this->resetQuery();

        return $result;
    }

    // ... الطرق الأخرى تبقى كما هي
}
استخدام المعايير في Controllers:
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use App\Repositories\Contracts\PostRepositoryInterface;
use App\Repositories\Criteria\PublishedCriteria;
use App\Repositories\Criteria\RecentCriteria;
use App\Repositories\Criteria\PopularCriteria;

class PostController extends Controller
{
    protected $postRepository;

    public function __construct(PostRepositoryInterface $postRepository)
    {
        $this->postRepository = $postRepository;
    }

    public function index()
    {
        // الحصول على المنشورات المنشورة الأخيرة
        $posts = $this->postRepository
            ->pushCriteria(new PublishedCriteria())
            ->pushCriteria(new RecentCriteria(30))
            ->orderBy('published_at', 'desc')
            ->paginate(15);

        return view('posts.index', compact('posts'));
    }

    public function popular()
    {
        // الحصول على المنشورات الشعبية المنشورة
        $posts = $this->postRepository
            ->pushCriteria(new PublishedCriteria())
            ->pushCriteria(new PopularCriteria(500))
            ->paginate(10);

        return view('posts.popular', compact('posts'));
    }
}
ميزة الاختبار: مع المستودعات، يمكنك بسهولة محاكاة واجهة المستودع في الاختبارات، مما يجعل اختباراتك أسرع وأكثر عزلة:
// في الاختبار الخاص بك
$mockRepo = $this->mock(PostRepositoryInterface::class);
$mockRepo->shouldReceive('published')
    ->once()
    ->andReturn(collect([/* بيانات اختبار */]));

تمرين 1: بناء مستودع كامل

أنشئ نظام مستودع كامل لنموذج Product مع ما يلي:

  • واجهة: ProductRepositoryInterface مع طرق لـ CRUD والتصفية حسب الفئة ونطاق السعر والبحث
  • تنفيذ: ProductRepository يمتد BaseRepository
  • ربط المستودع في مزود خدمة
  • استخدامه في controller لسرد المنتجات مع الترقيم

تمرين 2: تنفيذ نمط المعايير

أنشئ المعايير التالية لمستودع Order:

  • CompletedCriteria - الطلبات بحالة "completed"
  • DateRangeCriteria - الطلبات ضمن نطاق تاريخ (مرر تواريخ البداية والنهاية إلى المنشئ)
  • CustomerCriteria - الطلبات لعميل محدد
  • MinimumAmountCriteria - الطلبات أعلى من مبلغ معين

ثم استخدم هذه المعايير معاً للعثور على الطلبات المكتملة لعميل خلال الشهر الماضي أعلى من 100 دولار.

تمرين 3: اختبر مستودعك

اكتب اختبارات PHPUnit لـ PostRepository:

  • اختبر طريقة create() تنشئ منشوراً بشكل صحيح
  • اختبر طريقة update() تحدث المنشورات الموجودة
  • اختبر طريقة published() ترجع فقط المنشورات المنشورة
  • اختبر طريقة findBySlug() تجد المنشور الصحيح
  • حاكِ المستودع في اختبار controller
النقاط الرئيسية:
  • نمط المستودع يفصل الوصول للبيانات عن منطق الأعمال
  • الواجهات تجعل كودك قابلاً للاختبار من خلال حقن التبعية
  • المستودعات الأساسية تقلل من تكرار الكود
  • نمط المعايير يسمح بشروط استعلام مرنة وقابلة لإعادة الاستخدام
  • المستودعات تسهل التبديل بين مصادر البيانات
  • لا تفرط في الهندسة للتطبيقات البسيطة - استخدمها عندما يبرر التعقيد ذلك