The Entityمن الصفر.
قبل أي GraphQL أو REST، لازم يكون عندك الـ entity نفسها. هنبنيها كاملة خطوة بخطوة: الجدول، الـ Model، ResourceModel، Collection، الـ Data Interface، والـ Repository. دي الطبقة اللي كل حاجة بتتبني فوقها.
عشان تبني entity في Magento 2 بتعرّف الجدول في db_schema.xml، وبعدين Model بيمثّل الصف، و ResourceModel بيحفظ ويقرا من الجدول، و Collection بتجيب مجموعة صفوف. فوقهم Data Interface و Repository Interface بيكوّنوا الـ service contract، وبتربطهم بالتنفيذ في di.xml عشان باقي الكود والـ APIs يعتمدوا عليهم بدل التفاصيل.
الصورة الكاملة
افهم إزاي الطبقات بتترتّب فوق بعض قبل ما تكتب أي كود.
الجدول — db_schema
الأساس. بنعرّف الجدول وأعمدته بطريقة declarative، وMagento بيعمله في الـ DB.
بيوصّف الجدول وأعمدته والـ primary key. Magento بيقارنه بالـ DB الحالية وبيطبّق الفرق أوتوماتيك عند setup:upgrade.
<schema> <table name="vendor_ticket" resource="default" comment="Support Tickets"> <!-- primary key --> <column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="varchar" name="title" length="255" nullable="false" comment="Title"/> <column xsi:type="varchar" name="status" length="32" nullable="false" default="open" comment="Status"/> <column xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <!-- define the primary key --> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> </schema>
الـ Model
بيمثّل صف واحد من الجدول. بيحمل البيانات والمنطق، بس مبيلمسش الـ DB بنفسه.
بيورّث من AbstractModel، وبيطبّق الـ TicketInterface (اللي هنعمله بعدين). بيربط نفسه بالـ ResourceModel في _construct().
الـ getters/setters بتستخدم getData() وsetData() اللي جايين من الـ AbstractModel — دي بتخزّن القيم في array داخلي. الـ AbstractModel بيدّيك load, save, delete مجانًا عن طريق الـ ResourceModel.
class Ticket extends AbstractModel implements TicketInterface { // bind model to its resource model protected function _construct() { $this->_init(ResourceModel\Ticket::class); } public function getTitle(): ?string { return $this->getData(self::TITLE); } public function setTitle(string $title): TicketInterface { return $this->setData(self::TITLE, $title); } public function getStatus(): ?string { return $this->getData(self::STATUS); } public function setStatus(string $status): TicketInterface { return $this->setData(self::STATUS, $status); } }
الـ ResourceModel
الجسر بين الـ Model والجدول. الطبقة الوحيدة اللي بتكتب SQL فعليًا.
بيورّث من AbstractDb، وبيقول للـ Model "أنت بتخزّن في الجدول ده، والـ primary key هو ده". بمجرد كده، بيبقى عندك load/save/delete جاهزين.
عشان مبدأ Single Responsibility: الـ Model بيهتم بالبيانات والمنطق، والـ ResourceModel بيهتم بس بالتعامل مع الـ DB. لو غيّرت طريقة التخزين، بتعدّل هنا بس.
class Ticket extends AbstractDb { protected function _construct() { // table name, primary key column $this->_init( 'vendor_ticket', 'entity_id' ); } }
الـ Collection
للتعامل مع صفوف كتير مع فلاتر وترتيب. بترجّع مجموعة Models.
بتورّث من AbstractCollection، وبتربط نفسها بالـ Model والـ ResourceModel. بمجرد كده بتقدر تعمل filters و joins و sorting و pagination.
class Collection extends AbstractCollection { protected function _construct() { // bind: Model class, ResourceModel class $this->_init( \Vendor\Module\Model\Ticket::class, \Vendor\Module\Model\ResourceModel\Ticket::class ); } }
$collection = $this->collectionFactory->create(); $collection ->addFieldToFilter('status', 'open') ->setOrder('created_at', 'DESC');
Data Interface
بداية الطبقة العامة. بيعرّف شكل بيانات الـ Ticket كـ عقد ثابت.
يعرّف الـ constants (أسماء الأعمدة) والـ getters/setters. ده العقد اللي الـ Model بيطبّقه، والـ APIs بتستخدمه للـ serialization.
عشان تفصل "شكل البيانات" عن "التنفيذ". الـ REST والـ GraphQL بيعتمدوا على الـ interface ده — لو غيّرت الـ Model الداخلي، العقد يفضل ثابت والـ APIs متتكسرش.
interface TicketInterface { const ENTITY_ID = 'entity_id'; const TITLE = 'title'; const STATUS = 'status'; public function getId(); public function setId($id); public function getTitle(): ?string; public function setTitle(string $title): TicketInterface; public function getStatus(): ?string; public function setStatus(string $status): TicketInterface; }
محتاج كمان interface صغير للـ getList عشان يرجّع النتائج. بيورّث من SearchResultsInterface الجاهز.
interface TicketSearchResultsInterface extends SearchResultsInterface { /** @return TicketInterface[] */ public function getItems(): array; public function setItems(array $items); }
Repository Interface
العقد اللي بيعرّف العمليات المتاحة على الـ Ticket. ده اللي الـ APIs والكود بيستخدموه.
يعرّف العمليات القياسية: save, getById, getList, delete, deleteById. كل حاجة typed بدقة.
interface TicketRepositoryInterface { public function save(TicketInterface $ticket): TicketInterface; public function getById(int $id): TicketInterface; public function getList( SearchCriteriaInterface $criteria ): TicketSearchResultsInterface; public function delete(TicketInterface $ticket): bool; public function deleteById(int $id): bool; }
Repository — التنفيذ
هنا المنطق الحقيقي. بيستخدم الـ ResourceModel والـ Collection عشان ينفّذ العقد.
الـ save بتستخدم الـ ResourceModel للحفظ. الـ getById بتعمل load وبترمي exception لو مش لاقية.
class TicketRepository implements TicketRepositoryInterface { public function __construct( private readonly ResourceModel\Ticket $resource, private readonly TicketFactory $ticketFactory, private readonly CollectionFactory $collectionFactory, private readonly TicketSearchResultsInterfaceFactory $resultsFactory, private readonly CollectionProcessorInterface $processor ) {} public function save(TicketInterface $ticket): TicketInterface { try { $this->resource->save($ticket); } catch (\Exception $e) { throw new CouldNotSaveException( __($e->getMessage()) ); } return $ticket; } public function getById(int $id): TicketInterface { $ticket = $this->ticketFactory->create(); $this->resource->load($ticket, $id); if (!$ticket->getId()) { throw new NoSuchEntityException( __('Ticket with id "%1" not found', $id) ); } return $ticket; } }
الـ getList بتطبّق الـ SearchCriteria على الـ collection وبترجّع النتائج. الـ delete وdeleteById بيمسحوا عن طريق الـ ResourceModel.
public function getList( SearchCriteriaInterface $criteria ): TicketSearchResultsInterface { $collection = $this->collectionFactory->create(); // apply filters/sorting/paging from criteria $this->processor->process($criteria, $collection); $results = $this->resultsFactory->create(); $results->setSearchCriteria($criteria); $results->setItems($collection->getItems()); $results->setTotalCount($collection->getSize()); return $results; } public function delete(TicketInterface $ticket): bool { try { $this->resource->delete($ticket); } catch (\Exception $e) { throw new CouldNotDeleteException( __($e->getMessage()) ); } return true; } public function deleteById(int $id): bool { return $this->delete($this->getById($id)); } }
الربط — di.xml
آخر خطوة: نربط الـ interfaces بالتنفيذ عشان الـ DI يعرف يوصّل كل حاجة.
بيقول للـ DI: لما حد يطلب الـ interface، اديله الـ class ده. من غير الربط ده، Magento مش هيعرف أي تنفيذ يستخدم لكل interface.
<config> <!-- Repository --> <preference for="Vendor\Module\Api\TicketRepositoryInterface" type="Vendor\Module\Model\TicketRepository"/> <!-- Data model --> <preference for="Vendor\Module\Api\Data\TicketInterface" type="Vendor\Module\Model\Ticket"/> <!-- Search results --> <preference for="Vendor\Module\Api\Data\TicketSearchResultsInterface" type="Magento\Framework\Api\SearchResults"/> </config>
بعد كل الملفات، فعّل الـ module والجدول:
php bin/magento setup:upgrade php bin/magento setup:di:compile php bin/magento cache:clean
الـ Entity كاملة ✓
دلوقتي عندك entity شغّالة بالكامل: جدول في الـ DB، Model، ResourceModel، Collection، وRepository نظيف بواجهة عامة. دي الأرضية اللي الـ GraphQL والـ REST والـ admin grids كلهم بيتبنوا فوقها.
الترتيب اللي تفتكره: جدول → Model → Resource → Collection → Interfaces → Repository → di.xml. اتقن ده، وأي feature بعده بيبقى سهل.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
ما الفرق بين Model و ResourceModel و Collection في Magento 2؟
الـ Model بيمثّل صف واحد من الجدول وبيحمل البيانات عن طريق getData() وsetData()، بس مبيلمسش الـ DB بنفسه. الـ ResourceModel بيورّث من AbstractDb وهو الجسر بين الـ Model والجدول: بتقوله اسم الجدول والـ primary key وهو يتولّى الـ load/save/delete. أما الـ Collection فبتورّث من AbstractCollection وبتجيب صفوف كتير مع filters وsorting وpagination.
إزاي أعمل جدول جديد في Magento 2 باستخدام db_schema.xml؟
بتعرّف الجدول وأعمدته في etc/db_schema.xml، وبتحدد الـ primary key بـ constraint xsi:type="primary"، وidentity="true" على العمود معناها auto-increment. بعد كده بتشغّل php bin/magento setup:upgrade، وهو اللي بيقرا الملف وبيعمل الجدول فعليًا في الـ DB.
إيه الفرق بين _construct و __construct في Magento 2؟
الـ _construct() بشرطة واحدة method داخلية بتاعة Magento للتهيئة، بتستخدمها في الـ Model والـ ResourceModel والـ Collection عشان تنادي _init() وتربط الكلاسات ببعض أو بالجدول. أما __construct() بشرطتين فهو الـ constructor العادي بتاع PHP اللي بتحقن فيه الـ dependencies، فمتخلطش بينهم.
ليه أعمل Data Interface منفصل عن الـ Model في Magento 2؟
عشان تفصل شكل البيانات عن التنفيذ: interface زي TicketInterface بيعرّف أسماء الأعمدة كـ constants والـ getters/setters كعقد ثابت، والـ Model بيطبّقه. الـ REST والـ GraphQL بيعتمدوا على الـ interface ده، فلو غيّرت الـ Model من جوّه العقد بيفضل ثابت والـ APIs متتكسرش.
إزاي الـ Repository بيطبّق SearchCriteria في getList في Magento 2؟
الـ getList() بتعمل collection جديدة من الـ CollectionFactory، وبتدّي الـ SearchCriteria والـ collection لـ CollectionProcessorInterface اللي بيترجم الفلاتر والترتيب والـ paging لعمليات على الـ collection. بعدها بتملا الـ search results بـ setItems() وsetTotalCount() وترجّعها.
ليه بنستخدم preference في di.xml مع الـ Repository في Magento 2؟
الـ preference بيقول للـ DI: لما حد يطلب الـ interface ده، اديله الـ class ده. فبتربط TicketRepositoryInterface بـ TicketRepository وTicketInterface بالـ Model، ومن غير الربط ده Magento مش هيعرف أي تنفيذ يستخدم، ولا الـ TicketInterfaceFactory هتشتغل.
إيه ترتيب بناء Entity في Magento 2؟
بتبني من تحت لفوق: الجدول في db_schema.xml، بعدين الـ Model والـ ResourceModel والـ Collection، وبعدها الـ Data وRepository interfaces، والـ Repository نفسه، وفي الآخر الربط في di.xml. كل طبقة بتعتمد على اللي تحتها، ولما تخلّص بتشغّل setup:upgrade وsetup:di:compile وcache:clean.