نمط المستودع
جزء من قسم Object Oriented Programming في رحلة PHP على Coddy — الدرس 81 من 91.
إن نمط المستودع (Repository Pattern) هو نمط تصميم يتوسط بين منطق العمل في تطبيقك وتخزين البيانات. فهو يوفر واجهة تشبه المجموعات للوصول إلى كائنات المجال (domain objects)، مع إخفاء تفاصيل كيفية تخزين البيانات أو استرجاعها فعلياً.
فكر في المستودع (repository) كمجموعة متخصصة تعرف كيفية حفظ واسترجاع الكائنات. يطلب كود تطبيقك من المستودع مستخدماً عن طريق المعرف (ID) أو يحفظ منتجاً جديداً، دون معرفة ما إذا كانت البيانات موجودة في قاعدة بيانات، أو ملف، أو واجهة برمجة تطبيقات (API). هذا الفصل يجعل الكود الخاص بك أكثر قابلية للاختبار ومرونة.
<?php
interface UserRepositoryInterface {
public function find(int $id): ?User;
public function findAll(): array;
public function save(User $user): void;
public function delete(int $id): void;
}
class User {
public function __construct(
public int $id,
public string $name,
public string $email
) {}
}
يتعامل تنفيذ المستودع مع آلية التخزين الفعلية:
<?php
class InMemoryUserRepository implements UserRepositoryInterface {
private array $users = [];
public function find(int $id): ?User {
return $this->users[$id] ?? null;
}
public function findAll(): array {
return array_values($this->users);
}
public function save(User $user): void {
$this->users[$user->id] = $user;
}
public function delete(int $id): void {
unset($this->users[$id]);
}
}
$repo = new InMemoryUserRepository();
$repo->save(new User(1, "Alice", "alice@example.com"));
$repo->save(new User(2, "Bob", "bob@example.com"));
echo $repo->find(1)->name . "\n";
echo count($repo->findAll());
المخرجات:
Alice
2جمال هذا النمط يكمن في أنه يمكنك تبديل التنفيذات دون تغيير منطق العمل (business logic) الخاص بك. هل تحتاج إلى الانتقال من التخزين في الذاكرة إلى قاعدة بيانات؟ قم بإنشاء فئة مستودع (repository class) جديدة تنفذ نفس الواجهة (interface). يظل كود تطبيقك دون تغيير لأنه يعتمد على الواجهة، وليس على التنفيذ الملموس.
التحدي
سهللنقم ببناء نظام مخزون منتجات باستخدام نمط المستودع (Repository Pattern). ستقوم بإنشاء فصل تام بين كائنات النطاق (domain objects) وكيفية تخزينها، مما يسمح لكود تطبيقك بالتعامل مع المنتجات دون معرفة تفاصيل التخزين.
ستقوم بتنظيم الكود الخاص بك عبر أربعة ملفات:
Product.php— أنشئ فئةProductتمثل العناصر في مخزونك. لكل منتجid(int)، وname(string)، وprice(float). استخدم خاصية constructor promotion مع خصائص عامة (public) لسهولة الوصول.ProductRepositoryInterface.php— حدد واجهةProductRepositoryInterfaceالتي تضع العقد لأي تنفيذ لتخزين المنتجات. يجب أن تعلن واجهتك عن هذه الطرق (methods):find(int $id): ?Product— استرجاع منتج بواسطة المعرف (ID)، أو null إذا لم يتم العثور عليهfindAll(): array— الحصول على جميع المنتجات كمصفوفةsave(Product $product): void— تخزين أو تحديث منتجdelete(int $id): void— إزالة منتج بواسطة المعرف (ID)
InMemoryProductRepository.php— قم بتضمين كل من فئة Product والواجهة. أنشئ فئةInMemoryProductRepositoryالتي تنفذ الواجهة باستخدام مصفوفة خاصة (private array) لتخزين المنتجات. يجب فهرسة المنتجات بواسطة معرفاتها (ID) للبحث الفعال. يجب أن تعيد طريقةfindAll()كائنات المنتجات فقط (استخدمarray_values()لإعادة تعيين مفاتيح المصفوفة).main.php— قم بتضمين ملف المستودع. ستتلقى مدخلاً واحداً: سلسلة JSON تحتوي على مصفوفة من المنتجات لإضافتها، متبوعة بمعرف (ID) للبحث عنه.تنسيق JSON سيكون:
[{"id": 1, "name": "Laptop", "price": 999.99}, {"id": 2, "name": "Mouse", "price": 29.99}]أنشئ
InMemoryProductRepository، واحفظ جميع المنتجات من مدخل JSON، ثم استخدم المدخل الثاني (المعرف ID) للعثور على منتج معين. اطبع النتائج بهذا التنسيق:Total products: [count] Found: [name] - $[price]قم بتنسيق السعر إلى منزلتين عشريتين. إذا لم يتم العثور على المنتج، اطبع
Found: Not foundبدلاً من ذلك.
يتجلى نمط المستودع (Repository Pattern) عندما تحتاج إلى تبديل تطبيقات التخزين. يعتمد الكود الرئيسي الخاص بك فقط على الواجهة — فتبديل التخزين من الذاكرة (in-memory) إلى قاعدة بيانات سيعني إنشاء فئة مستودع جديدة دون تغيير أي من منطق التطبيق.
ورقة مرجعية
يعمل نمط المستودع (Repository Pattern) كوسيط بين منطق الأعمال وتخزين البيانات، حيث يوفر واجهة تشبه المجموعات للوصول إلى كائنات النطاق مع إخفاء تفاصيل تنفيذ التخزين.
قم بتعريف واجهة مستودع (repository interface) تحدد العقد الخاص بالوصول إلى البيانات:
<?php
interface UserRepositoryInterface {
public function find(int $id): ?User;
public function findAll(): array;
public function save(User $user): void;
public function delete(int $id): void;
}قم بإنشاء فئة نطاق (domain class) باستخدام ترقية المنشئ (constructor promotion):
<?php
class User {
public function __construct(
public int $id,
public string $name,
public string $email
) {}
}قم بتنفيذ واجهة المستودع باستخدام آلية تخزين ملموسة:
<?php
class InMemoryUserRepository implements UserRepositoryInterface {
private array $users = [];
public function find(int $id): ?User {
return $this->users[$id] ?? null;
}
public function findAll(): array {
return array_values($this->users);
}
public function save(User $user): void {
$this->users[$user->id] = $user;
}
public function delete(int $id): void {
unset($this->users[$id]);
}
}استخدم المستودع من خلال واجهته:
$repo = new InMemoryUserRepository();
$repo->save(new User(1, "Alice", "alice@example.com"));
$user = $repo->find(1);
$allUsers = $repo->findAll();يسمح هذا النمط بتبديل التنفيذات دون تغيير منطق الأعمال — حيث يعتمد الكود الخاص بك على الواجهة، وليس على التنفيذ الملموس.
جرّب بنفسك
<?php
require_once 'InMemoryProductRepository.php';
// قراءة المدخلات
$jsonInput = trim(fgets(STDIN));
$searchId = intval(trim(fgets(STDIN)));
// تحليل مدخلات JSON إلى مصفوفة
$productsData = (array)json_decode($jsonInput, true);
// TODO: إنشاء مثيل من InMemoryProductRepository
// TODO: التكرار عبر $productsData وحفظ كل منتج
// كل عنصر يحتوي على المفاتيح 'id' و 'name' و 'price'
// TODO: الحصول على جميع المنتجات وطباعة العدد الإجمالي
// التنسيق: "Total products: [count]"
// TODO: البحث عن المنتج باستخدام $searchId
// إذا تم العثور عليه، اطبع: "Found: [name] - $[price]" (السعر منسق إلى منزلتين عشريتين)
// إذا لم يتم العثور عليه، اطبع: "Found: Not found"
?>يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات OOP
الملفات الخارجيةمقدمة في OOPClasses مقابل Objectsالكلمة المفتاحية $thisالـ Methodsالـ Propertiesالمنشئ (__construct)الهادم (__destruct)مراجعة - آلة حاسبة بسيطة4الوراثة
الوراثة الأساسيةالكلمة المفتاحية parent::إعادة تعريف الدوال (Method Overriding)الكلمة المفتاحية finalالأصناف المجردة (Abstract Classes)مراجعة - هيكلية الموظفين7التغليف (Encapsulation)
العام، المحمي، الخاص (Public, Protected, Private)نظرة متعمقة على معدلات الوصولدوال Getters و Settersإخفاء المعلوماتميزة Constructor Promotion (8.0)مراجعة - نظام سجلات الطلاب10مفاهيم البرمجة كائنية التوجه المتقدمة
التركيب مقابل الوراثةحقن التبعيةالأصناف المجهولةالتعدادات (PHP 8.1)الألياف (PHP 8.1)تعمق في استنساخ الكائناتالمولدات والمكررات13أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبنمط المستودع2مساحات الأسماء والتحميل التلقائي
مقدمة إلى مساحات الأسماءالكلمة المفتاحية useمعيار التحميل التلقائي PSR-4أداة التحميل التلقائي Composerملخص - مشروع منظم5الواجهات والعقود
مقدمة إلى الواجهاتتنفيذ الواجهاتتنفيذ واجهات متعددةالواجهة مقابل الـ Abstract Classالـ Type Hinting مع الواجهاتملخص - حاسبة الأشكال8الميثودات السحرية (Magic Methods)
مقدمة في الميثودات السحرية__toString و __debugInfo__get, __set, __isset, __unset__call و __callStatic__clone واستنساخ الكائنات__serialize و __unserializeمراجعة - Custom Collection3خصائص الكلاس
خصائص الـ Instance مقابل الخصائص الساكنة (Static)الثوابت في الكلاساتالدوال والخصائص الساكنة (Static)الخصائص الخاصة (Private) والمحمية (Protected)خصائص للقراءة فقط (Readonly) في PHP 8.1مراجعة - مدير الحساب البنكي6تعدد الأشكال (Polymorphism)
مراجعة إعادة تعريف الدوال (Method Overriding)تعدد الأشكال عبر الـ Interfacesتلميح النوع (Type Hinting) وأنواع الاتحاد (Union Types)الربط الاستاتيكي المتأخر (Late Static Binding)ملخص - معالج الدفع (Payment Processor)