اتخطّى للمحتوى

المرحلة 01 · الأساسياتدرس 6 من 2110 دقيقة قراءةآخر تحديث:

Lesson 06 / 21

The Entityمن الصفر.

قبل أي GraphQL أو REST، لازم يكون عندك الـ entity نفسها. هنبنيها كاملة خطوة بخطوة: الجدول، الـ Model، ResourceModel، Collection، الـ Data Interface، والـ Repository. دي الطبقة اللي كل حاجة بتتبني فوقها.

● Foundation db_schema Model · Resource Collection Repository

عشان تبني entity في Magento 2 بتعرّف الجدول في db_schema.xml، وبعدين Model بيمثّل الصف، و ResourceModel بيحفظ ويقرا من الجدول، و Collection بتجيب مجموعة صفوف. فوقهم Data Interface و Repository Interface بيكوّنوا الـ service contract، وبتربطهم بالتنفيذ في di.xml عشان باقي الكود والـ APIs يعتمدوا عليهم بدل التفاصيل.

// الترتيب مهم هنبني من تحت لفوق: نبدأ بالجدول في الـ DB، وننتهي بالـ Repository اللي بيغلّف كل ده في واجهة نظيفة. كل طبقة بتعتمد على اللي تحتها. اتبع الترتيب بالظبط وهتلاقي كل حاجة بتتركّب طبيعي. الـ entity هتكون Ticket (تذكرة دعم).
00

الصورة الكاملة

افهم إزاي الطبقات بتترتّب فوق بعض قبل ما تكتب أي كود.

DB
الجدول — تعريف الأعمدة في db_schema.xml.
MDL
Model — بيمثّل صف واحد من الجدول.
RES
ResourceModel — بيوصّل الـ Model بالجدول (load/save).
COL
Collection — بتجيب صفوف كتير مع فلاتر.
API
Data + Repository Interface — العقد العام.
IMP
Repository — بينفّذ العقد ويغلّف كل ده.
i
القاعدة: الطبقات السفلية (Model, Resource, Collection) "داخلية". الطبقات العليا (Interfaces, Repository) هي "الواجهة العامة" اللي باقي الكود والـ APIs بيتعاملوا معاها.
Vendor/Module/ ├── Api/ │ ├── TicketRepositoryInterface.php │ └── Data/ │ ├── TicketInterface.php │ └── TicketSearchResultsInterface.php ├── Model/ │ ├── Ticket.php # Model │ ├── TicketRepository.php # Repo impl │ └── ResourceModel/ │ ├── Ticket.php # Resource │ └── Ticket/ │ └── Collection.php └── etc/ ├── db_schema.xml └── di.xml
• • •
01

الجدول — db_schema

الأساس. بنعرّف الجدول وأعمدته بطريقة declarative، وMagento بيعمله في الـ DB.

وظيفته

بيوصّف الجدول وأعمدته والـ primary key. Magento بيقارنه بالـ DB الحالية وبيطبّق الفرق أوتوماتيك عند setup:upgrade.

etc/db_schema.xmlxml
<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>
i
identity="true" معناها auto-increment. default="CURRENT_TIMESTAMP" بيخلّي الـ DB يحط وقت الإنشاء أوتوماتيك من غير ما تلمسه في الكود.
ASKليه declarative schema بدل الـ install scripts القديمة؟
الإجابة: لأنه بيوصّف الحالة النهائية المطلوبة للجدول، وMagento بيحسب الفرق ويطبّقه. أنظف، بيدعم الـ rollback، ومش محتاج تكتب upgrade scripts متتالية لكل تغيير زي زمان.
• • •
02

الـ Model

بيمثّل صف واحد من الجدول. بيحمل البيانات والمنطق، بس مبيلمسش الـ DB بنفسه.

وظيفته

بيورّث من AbstractModel، وبيطبّق الـ TicketInterface (اللي هنعمله بعدين). بيربط نفسه بالـ ResourceModel في _construct().

إزاي بيشتغل

الـ getters/setters بتستخدم getData() وsetData() اللي جايين من الـ AbstractModel — دي بتخزّن القيم في array داخلي. الـ AbstractModel بيدّيك load, save, delete مجانًا عن طريق الـ ResourceModel.

Model/Ticket.phpphp
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);
    }
}
!
لاحظ _construct بشرطة واحدة (مش __construct بشرطتين). دي method داخلية بتاعة Magento للتهيئة — متخلطش بينها وبين الـ constructor العادي.
• • •
03

الـ ResourceModel

الجسر بين الـ Model والجدول. الطبقة الوحيدة اللي بتكتب SQL فعليًا.

وظيفته

بيورّث من AbstractDb، وبيقول للـ Model "أنت بتخزّن في الجدول ده، والـ primary key هو ده". بمجرد كده، بيبقى عندك load/save/delete جاهزين.

ليه منفصل عن الـ Model

عشان مبدأ Single Responsibility: الـ Model بيهتم بالبيانات والمنطق، والـ ResourceModel بيهتم بس بالتعامل مع الـ DB. لو غيّرت طريقة التخزين، بتعدّل هنا بس.

Model/ResourceModel/Ticket.phpphp
class Ticket extends AbstractDb
{
    protected function _construct()
    {
        // table name, primary key column
        $this->_init(
            'vendor_ticket',
            'entity_id'
        );
    }
}
i
ملف بسيط جدًا، بس هو اللي بيدّي كل قوة الـ CRUD للـ Model. الـ AbstractDb بيعمل الـ SQL كله ورا الكواليس بناءً على السطرين دول.
• • •
04

الـ Collection

للتعامل مع صفوف كتير مع فلاتر وترتيب. بترجّع مجموعة Models.

وظيفته

بتورّث من AbstractCollection، وبتربط نفسها بالـ Model والـ ResourceModel. بمجرد كده بتقدر تعمل filters و joins و sorting و pagination.

.../Ticket/Collection.phpphp
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
        );
    }
}
using it laterphp
$collection = $this->collectionFactory->create();
$collection
    ->addFieldToFilter('status', 'open')
    ->setOrder('created_at', 'DESC');
i
زي الـ ResourceModel، الملف بسيط — سطرين بس. الـ AbstractCollection بيدّيك كل أدوات الفلترة والترتيب مجانًا.
• • •
05

Data Interface

بداية الطبقة العامة. بيعرّف شكل بيانات الـ Ticket كـ عقد ثابت.

وظيفته

يعرّف الـ constants (أسماء الأعمدة) والـ getters/setters. ده العقد اللي الـ Model بيطبّقه، والـ APIs بتستخدمه للـ serialization.

ليه interface منفصل عن الـ Model

عشان تفصل "شكل البيانات" عن "التنفيذ". الـ REST والـ GraphQL بيعتمدوا على الـ interface ده — لو غيّرت الـ Model الداخلي، العقد يفضل ثابت والـ APIs متتكسرش.

Api/Data/TicketInterface.phpphp
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;
}
الـ SearchResults interface

محتاج كمان interface صغير للـ getList عشان يرجّع النتائج. بيورّث من SearchResultsInterface الجاهز.

Api/Data/TicketSearchResultsInterface.phpphp
interface TicketSearchResultsInterface
    extends SearchResultsInterface
{
    /** @return TicketInterface[] */
    public function getItems(): array;

    public function setItems(array $items);
}
• • •
06

Repository Interface

العقد اللي بيعرّف العمليات المتاحة على الـ Ticket. ده اللي الـ APIs والكود بيستخدموه.

وظيفته

يعرّف العمليات القياسية: save, getById, getList, delete, deleteById. كل حاجة typed بدقة.

Api/TicketRepositoryInterface.phpphp
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;
}
i
الـ interface ده هو نفسه اللي استخدمناه في الـ GraphQL والـ REST. دلوقتي شايف إزاي كل حاجة بتبني عليه — عشان كده هو أهم طبقة.
• • •
07

Repository — التنفيذ

هنا المنطق الحقيقي. بيستخدم الـ ResourceModel والـ Collection عشان ينفّذ العقد.

save و getById

الـ save بتستخدم الـ ResourceModel للحفظ. الـ getById بتعمل load وبترمي exception لو مش لاقية.

Model/TicketRepository.phpphp
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 و delete

الـ getList بتطبّق الـ SearchCriteria على الـ collection وبترجّع النتائج. الـ delete وdeleteById بيمسحوا عن طريق الـ ResourceModel.

Model/TicketRepository.php (تكملة)php
    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));
    }
}
i
الـ CollectionProcessor هو اللي بيترجم الـ SearchCriteria (فلاتر REST/GraphQL) لعمليات على الـ collection. مكوّن جاهز بتحقنه وبس.
• • •
08

الربط — di.xml

آخر خطوة: نربط الـ interfaces بالتنفيذ عشان الـ DI يعرف يوصّل كل حاجة.

وظيفته

بيقول للـ DI: لما حد يطلب الـ interface، اديله الـ class ده. من غير الربط ده، Magento مش هيعرف أي تنفيذ يستخدم لكل interface.

etc/di.xmlxml
<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>
ASKليه بنربط الـ TicketInterface بالـ Model؟
الإجابة: عشان لما أي كود (أو الـ Factory بتاعة الـ interface) يطلب TicketInterface، الـ DI يديله الـ Model\Ticket اللي بيطبّقه فعليًا. ده اللي بيخلّي الـ TicketInterfaceFactory تشتغل في الـ resolvers والـ APIs.

بعد كل الملفات، فعّل الـ module والجدول:

terminalbash
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento cache:clean
i
الـ setup:upgrade هو اللي بيقرا الـ db_schema.xml وبيعمل الجدول فعليًا في الـ DB. متنساهوش.

الـ Entity كاملة ✓

دلوقتي عندك entity شغّالة بالكامل: جدول في الـ DB، Model، ResourceModel، Collection، وRepository نظيف بواجهة عامة. دي الأرضية اللي الـ GraphQL والـ REST والـ admin grids كلهم بيتبنوا فوقها.

الترتيب اللي تفتكره: جدول → Model → Resource → Collection → Interfaces → Repository → di.xml. اتقن ده، وأي feature بعده بيبقى سهل.

// magento 2 backend · building the entity · foundation

خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.

كاتب الدرس

Abdulrahman Masoud

عندك سؤال على الدرس أو محتاج مساعدة في مشروع Magento؟ كلّمني.

أسئلة شائعة

ما الفرق بين 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.