المعمارية English

arc42 §5 — عرض الكتل البنائية (C4 المستوى 3)

الحالة: مسودة v0.1 · التاريخ: 2026-07-20 يتتبّع إلى: CON-14 (modular monolith), QAS-MOD-03 (الاستخراج), R-08 (تآكل الحدود), SEAM-01SEAM-07

لماذا هذه الوثيقة هي الأهم بالنسبة للتكلفة على المدى الطويل. R-08 هو الخطر الوحيد في السجل الموسوم بأنه غير قابل للعكس عمليًا. لا تفشل الأنماط الأحادية المعيارية (modular monoliths) لأن الحدود رُسمت خطأً — بل تفشل لأن لا شيء أوقف تآكلها. هذه الوثيقة تعرّف الحدود و الآلية التي تفرضها. الحد الذي لا تفحصه الآلة مجرد تعليق.


5.1 تفكيك الوحدات (Modules)#

flowchart TB
    subgraph EDGE["HTTP Edge"]
        API["API Controllers · Form Requests · Resources"]
        ADMUI["Admin Portal"]
    end

    subgraph MODULES["Business Modules — each owns its data"]
        direction TB
        IAM["<b>Identity</b><br/>users · credentials · tokens<br/>sessions · actor profiles"]
        ONB["<b>Onboarding</b><br/>documents · review queue<br/>approval state machine"]
        CONS["<b>Consultancy</b><br/>consultant profiles · availability<br/>bookings · sessions"]
        VID["<b>Video</b><br/>session lifecycle · tokens<br/>provider port"]
        TPL["<b>Documents</b><br/>templates · categories<br/>custom requests"]
        LRN["<b>Learning</b><br/>modules · lessons<br/>assignments · progress"]
        ASMT["<b>Assessment</b><br/>question banks · attempts<br/>grading"]
        CERT["<b>Certification</b><br/>certificates · verification<br/>revocation"]
        MKT["<b>Directory</b><br/>provider listings · categories<br/>featured placements · indexing"]
        PAY["<b>Billing</b><br/>payments · subscriptions<br/>payouts · payment port"]
        NOTIF["<b>Notifications</b><br/>dispatch · preferences<br/>templates"]
        ADM["<b>Administration</b><br/>admin roles · queues<br/>audit · reporting"]
    end

    subgraph SHARED["Shared Kernel — value objects &amp; interfaces ONLY"]
        SK["EventBus · DomainEvent · Money · Locale<br/>Clock · TenantContext · typed IDs"]
    end

    subgraph INFRA["Infrastructure"]
        DB[("PostgreSQL<br/>module-prefixed tables · RLS")]
        REDIS[("Redis")]
        OBJ[("Object Storage")]
        SRCH[("Search Engine")]
    end

    API --> MODULES
    ADMUI --> ADM
    MODULES --> SHARED
    MODULES --> INFRA

    style SHARED fill:#fff4e5,stroke:#d98c1f
    style IAM fill:#e8f0fe,stroke:#1168bd

فهرس الوحدات#

الوحدة (module) تملك (جداول ببادئة) العقد العام يكشف جاهزية الاستخراج
Identity identity_* — المستخدمون، بيانات الاعتماد، الرموز، ملفات الجهات الفاعلة IdentityQuery (ملخصات المستخدمين، دفعات)، AccountState 🟢 عالية — نقية، اعتماديات واردة قليلة
Onboarding onboarding_* — المستندات، النسخ، المراجعات، الانتقالات AccountApprovalStatus، الأحداث 🟢 عالية
Consultancy consultancy_* — ملفات المستشارين، الإتاحة، الحجوزات BookingQuery، ConsultantSummary 🟡 متوسطة — مقترنة بـ Billing وVideo عبر الأحداث
Video video_* — الجلسات، أحداث المشاركين، مراجع المزوّد VideoSessionPort (SEAM-02) 🟢 الأعلى — port بالفعل
Documents documents_* — القوالب، الأقسام، البنود، الطلبات المخصصة TemplateQuery، CustomRequestStatus 🟢 عالية
Learning learning_* — الوحدات، الدروس، التكليفات، التقدّم AssignmentQuery، ProgressSummary 🟢 عالية (SEAM-04)
Assessment assessment_* — البنوك، الأسئلة، المحاولات AssessmentResult — وأبدًا مفتاح الإجابات 🟢 عالية (SEAM-04)
Certification certification_* — الشهادات، عمليات الإلغاء CertificateQuery، التحقق العام 🟢 عالية
Directory directory_* — الإدراجات، الفئات، المواضع المميّزة (featured) ListingQuery، FeaturedStatus 🟡 متوسطة — تملك إسقاط (projection) البحث (SEAM-05)
Billing billing_* — المدفوعات، الاشتراكات، الكشوف PaymentPort (SEAM-01SubscriptionStatus 🟢 الأعلى — port بالفعل
Notifications notifications_* — سجل الإرسال، التفضيلات NotificationDispatcher 🟢 عالية — مستهلك محض
Administration admin_* — أدوار المسؤولين، الطوابير، سجل التدقيق، نماذج القراءة — (تستهلك فقط) 🔴 منخفضة — تقرأ عبر كل الوحدات بحكم التصميم

Administration هي أقل الوحدات قابلية للاستخراج عن قصد، وهذا صحيح. فهي سطح تشغيلي شامل، لا مجال أعمال. تستهلك العقود العامة والـ projections من كل مكان. ومحاولة جعلها قابلة للنشر باستقلال ستؤدي إما إلى تكرار بيانات كل وحدة، أو إلى تحويل كل شاشة إدارية إلى شبكة متفرّعة من نداءات الشبكة.


5.2 البنية الداخلية للوحدة#

كل وحدة لها الشكل نفسه. والفصل بين Public/ وInternal/ هو الاتفاقية الحاملة للبناء.

src/Modules/Consultancy/
├── Public/                      ← the ONLY namespace other modules may import
│   ├── Contracts/
│   │   └── BookingQuery.php     ← interface, batch-first
│   ├── Events/
│   │   ├── BookingConfirmed.php ← primitives only, versioned
│   │   └── ConsultationCompleted.php
│   └── Dto/
│       └── ConsultantSummary.php ← immutable, no Eloquent
├── Internal/                    ← invisible to every other module
│   ├── Models/                  ← Eloquent models live HERE and never leave
│   ├── Services/
│   ├── Repositories/
│   ├── Policies/
│   └── StateMachines/
├── Http/                        ← controllers, requests, resources
├── Jobs/
├── Database/
│   └── migrations/              ← this module's tables, prefixed
├── Providers/
│   └── ConsultancyServiceProvider.php  ← binds Public contracts to Internal impls
└── Tests/

قاعدة واحدة تؤدي معظم العمل#

لا يجوز لأي module أن يشير إلى فضاء الأسماء Internal الخاص بـ module آخر.

قاعدة لا لبس فيها، وقابلة للفحص آليًا، وتنتج رسالة خطأ واضحة. وقاعدة Deptrac الواحدة هذه تساوي أكثر من دليل أسلوب كامل، لأنها لا تقبل الجدال في مراجعة الكود.


5.3 قواعد الاعتمادية#

flowchart TB
    subgraph L1["Layer 1 — no module dependencies"]
        IAM2["Identity"]
        NOTIF2["Notifications"]
    end
    subgraph L2["Layer 2 — depends on Layer 1 only"]
        ONB2["Onboarding"]
        TPL2["Documents"]
        LRN2["Learning"]
        ASMT2["Assessment"]
        MKT2["Directory"]
        PAY2["Billing"]
        VID2["Video"]
    end
    subgraph L3["Layer 3 — orchestrating"]
        CONS2["Consultancy"]
        CERT2["Certification"]
    end
    subgraph L4["Layer 4 — consumes everything"]
        ADM2["Administration"]
    end

    L2 --> L1
    L3 --> L2
    L3 --> L1
    L4 --> L3
    L4 --> L2
    L4 --> L1

    SK2["Shared Kernel<br/><i>value objects + interfaces only</i>"]
    L1 & L2 & L3 & L4 --> SK2

    style SK2 fill:#fff4e5,stroke:#d98c1f

القواعد#

# القاعدة يفرضها
1 الاعتماديات تتجه للأسفل فقط. لا صعود، ولا دورات. قواعد الطبقات في Deptrac
2 لا يجوز لأي module أن يستورد إلا Public/ الخاص بـ module آخر. قاعدة فضاء الأسماء في Deptrac
3 Shared Kernel لا يجوز أن يعتمد على شيء. Deptrac
4 لا قيود مفاتيح خارجية (foreign key) عابرة للوحدات. خزّن المعرّف المجرد فقط. مراجعة الـ migrations + اختبار المخطط
5 لا علاقة Eloquent عابرة للوحدات، ولا whereHas/join. اختبار معماري
6 لا تمرّر نموذج Eloquent عبر حد أبدًا. مرّر DTO. اختبار معماري (لا يجوز لـ Public/ أن تشير إلى Internal\Models)
7 لا معاملة قاعدة بيانات تمتد عبر الوحدات. المراجعة + التصميم المدفوع بالأحداث
8 لا يجوز إلا لـ Identity أن تشير إلى الصنف User. اختبار معماري

القاعدة 8 تستحق شرحًا خاصًا بها#

نموذج User هو الفخ المحدد الذي يقتل الـ modular monoliths في Laravel. في التطبيق النمطي تعلّق كل وحدة علاقاتها على User حتى تصبح فيه أربعون دالة واستيرادات من كل فضاء أسماء — وعندها يعني استخراج أي شيء المساس بكل شيء.

بدلًا من ذلك: Identity تملك User. وكل وحدة أخرى تخزّن user_id كعمود عادي، وتحتفظ بإسقاطها المحلي حيث تحتاجه — Consultancy\ConsultantProfile، Directory\ProviderProfile، Learning\Learner. كلٌّ منها مفتاحه user_id، ومملوك لوحدته، ويحمل فقط ما تهتم به تلك الوحدة.

يبدو هذا تكرارًا زائدًا في اليوم الأول. وهو بالضبط ما يجعل الاستخراج ممكنًا في اليوم الألف.


5.4 التواصل بين الوحدات#

آليتان اثنتان، والاختيار بينهما ليس مسألة ذوق.

flowchart LR
    subgraph SYNC["Synchronous — query, needs an answer now"]
        A["Module A"] -->|"IdentityQuery::findSummaries([ids])"| B["Module B<br/>Public/Contracts"]
        B -->|"UserSummary[]"| A
    end

    subgraph ASYNC["Asynchronous — fact, already happened"]
        C["Module C"] -->|"publish(BookingConfirmed)"| OUT["Transactional<br/>Outbox"]
        OUT --> REL["Relay"]
        REL --> D["Module D<br/>subscriber"]
        REL --> E["Module E<br/>subscriber"]
    end

    style OUT fill:#e8f5e9,stroke:#2d8659
الاستخدام متى
العقد العام (متزامن) تحتاج البيانات الآن لإتمام العملية الجارية
حدث المجال (domain event) (غير متزامن) وقع شيء ما؛ قد تهتم به وحدات أخرى؛ ويجب ألّا يعرف المُصدِر من هم

العقود مصمَّمة للدفعات أولًا#

interface IdentityQuery {
    public function findSummary(UserId $id): ?UserSummary;

    /** @return array<string, UserSummary> keyed by id */
    public function findSummaries(UserId ...$ids): array;
}

العقد الذي يجلب عنصرًا واحدًا فقط لا بأس به داخل العملية، لكنه يتحول إلى عاصفة N+1 من نداءات HTTP يوم تُستخرج الوحدة. والتصميم للدفعات الآن يكلّف دالة إضافية واحدة.

صندوق الصادر المعاملاتي (outbox) — اشحنه في الأسبوع الأول#

sequenceDiagram
    participant M as Module
    participant DB as PostgreSQL
    participant R as Relay
    participant S as Subscribers

    M->>DB: BEGIN TX
    M->>DB: Write state change
    M->>DB: Write outbox_events row
    M->>DB: COMMIT
    Note over DB: State + event committed atomically.<br/>Either both happen or neither.
    R->>DB: Poll unpublished events
    R->>S: Dispatch (in-process today,<br/>broker tomorrow)
    S-->>R: Ack
    R->>DB: Mark published

لماذا هذا أعلى استثمار مردودًا في اليوم الأول (~يوم عمل واحد):

  • الذرّية. لا يمكن تأكيد حجز دون حدثه، ولا يمكن أن يُطلق حدث لحجز جرى التراجع عنه. والنشر خارج المعاملة ينتج هذين الخطأين بالضبط، وبشكل متقطّع.
  • استبدال الوسيط مجاني لاحقًا. كود الناشر والمشترك لا يتغير أبدًا — يتغير مقصد الـ relay فقط.
  • إعادة التشغيل والتدقيق يأتيان معه دون تكلفة إضافية.

تصميم الأحداث#

interface DomainEvent {
    public function eventName(): string;              // 'consultancy.booking_confirmed'
    public function occurredAt(): DateTimeImmutable;
    public function payload(): array;                 // PRIMITIVES ONLY
    public function version(): int;                   // from day one
}

بندان غير قابلين للتفاوض:

  1. الحمولات أنواع أولية. ['booking_id' => 123, 'consultant_id' => 45]، ولا نموذج أبدًا. الحمولة التي تُسلسَل إلى JSON بنظافة اليوم تصبح رسالة وسيط غدًا بتكلفة صفرية. أما الحمولة التي تحمل نموذج Eloquent فهي إعادة كتابة.
  2. الإصدار من اليوم الأول. إضافته بعد أن يصير لديك مشتركون تعني عملية ترحيل؛ أما وجوده مع تجاهله فلا يكلّف شيئًا.

5.5 فهرس الأحداث#

المفردات المنشورة بين الوحدات. هذه هي عقد التكامل.

الحدث يُنشره يستهلكه
identity.user_registered Identity Notifications, Administration
identity.credentials_changed Identity Notifications
onboarding.documents_submitted Onboarding Administration (queue)
onboarding.account_approved Onboarding Identity, Directory, Consultancy, Notifications
onboarding.account_rejected Onboarding Identity, Notifications
consultancy.booking_created Consultancy Billing
consultancy.booking_confirmed Consultancy Video, Notifications, Administration
consultancy.booking_cancelled Consultancy Billing, Video, Notifications
consultancy.consultation_completed Consultancy Billing (payout eligibility), Administration
video.session_ended Video Consultancy
video.connection_failed Video Consultancy, Administration
documents.template_downloaded Documents Administration (KPI)
documents.custom_request_paid Documents Administration (queue)
documents.custom_request_delivered Documents Notifications, Administration
learning.assignment_created Learning Notifications
learning.module_completed Learning Assessment, Administration
assessment.attempt_completed Assessment Learning, Administration
assessment.attempt_passed Assessment Certification, Notifications
certification.certificate_issued Certification Notifications, Administration
certification.certificate_revoked Certification Notifications, Administration
directory.listing_changed Directory (self — reindex)
billing.payment_captured Billing Consultancy, Documents, Directory, Administration
billing.payment_failed Billing Notifications, Administration
billing.subscription_lapsed Billing Directory (delist), Notifications
billing.featured_expired Billing Directory (reindex)

اقرأ صف Certification. المسار assessment.attempt_passed ← Certification هو السبيل الوحيد الذي تأتي به الشهادة إلى الوجود. Certification لا تمدّ يدها إلى جداول Assessment أبدًا. ذلك السهم الواحد هو سبب نجاح SEAM-04.


5.6 الفرض في CI#

هذا القسم هو مغزى الوثيقة. كل ما سبق يبقى إرشاديًا بدونه.

flowchart LR
    PR["Pull Request"] --> D["Deptrac<br/><i>--fail-on-uncovered</i>"]
    D --> A["Architecture tests<br/><i>AST-level, per-module</i>"]
    A --> S["Schema test<br/><i>no cross-module FKs</i>"]
    S --> T["Cross-tenant<br/>adversarial suite"]
    T --> P["PHPStan"]
    P --> OK["✅ Merge allowed"]

    D -.->|violation| FAIL["❌ Build fails"]
    A -.->|violation| FAIL
    S -.->|violation| FAIL
    T -.->|leak| FAIL

    style FAIL fill:#fde8e8,stroke:#c94a4a
    style OK fill:#e8f5e9,stroke:#2d8659

Deptrac#

⚠️ الحزمة انتقلت. qossmic/deptrac متروكة (abandoned). استخدم deptrac/deptrac (v4.6.2، 2026-07-01). Composer يحذّر من الاسم القديم؛ وأي شرح قديم سيضع الاسم الخطأ في composer.json.

القواعد المطلوب ضبطها:

  1. كل module طبقة؛ وPublic/ وInternal/ طبقتان فرعيتان
  2. الانتقال من module إلى module مسموح فقط عبر Public/
  3. ترتيب الطبقات وفق §5.3 — لا حواف صاعدة، ولا دورات
  4. Shared/ لا يجوز أن تعتمد على شيء
  5. --fail-on-uncovered — بدونه، أي فضاء أسماء جديد لم يُسنده أحد إلى طبقة يفلت بصمت من كل القواعد. وهذه أكثر طريقة شائعة يتحلل بها ضبط Deptrac.

الاختبارات المعمارية (على مستوى AST، سريعة، مع كل PR)#

الاختبار ما يلتقطه
Identity وحدها تشير إلى User فخ كرة الطين (القاعدة 8)
لا صنف في Public/ يشير إلى Internal\Models تسرّب النماذج عبر الحدود
كل نموذج فيه عمود company_id يستخدم trait تحديد نطاق المستأجر تسرّب المستأجرين الذي سيُضاف العام القادم
لكل نموذج policy مسجّلة نقاط نهاية بلا تخويل
لا env() خارج config/ يكسر config:cache بصمت في الإنتاج
لا حالة ساكنة قابلة للتغيير؛ ولا حقن Application/Request/Config في الـ singletons يبقي الكود قابلًا لتبنّي Octane لاحقًا
موارد تسليم Assessment لا تحتوي أي حقول صحّة الإجابة تسرّب مفتاح الإجابات QAS-INT-01

اختبار المخطط#

يتحقق من أن لا قيد مفتاح خارجي يعبر بادئة جداول أي module. ويعمل على المخطط بعد الترحيل، لا على ملفات الـ migrations — فيلتقط بذلك القيود التي تضيفها حزمة أو migration مكتوبة يدويًا.


5.7 Shared Kernel — الانضباط الذي يبقيه صغيرًا#

Shared/
├── Contracts/    EventBus · DomainEvent · Clock · TenantContext
├── ValueObjects/ Money · Locale · UserId · CompanyId · Percentage
└── Enums/        Currency · Country · ActorType

قاعدة صارمة: Shared/ تحتوي فقط على value objects وواجهات بلا اعتماديات.

في اللحظة التي تحتوي فيها Shared/ على خدمة ذات سلوك، تصبح هي كرة الطين الجديدة — كل module يعتمد عليها، وهي تعتمد على كل شيء، ويصبح رسم الاعتماديات كذبة. قاعدة Deptrac رقم 4 موجودة تحديدًا لمنع هذا، ويجب التعامل معها كقاعدة لا تُخترق، لا كإرشاد يقبل الاستثناءات.

TenantContext مكانها هنا لأنها شاملة بطبيعتها، ولأنها ترمي استثناءً عند قراءتها وهي غير مضبوطة بدلًا من إرجاع null — وهو سلوك الرفض عند الشك (fail closed) الذي يعتمد عليه QAS-SEC-01.


5.8 دليل الاستخراج#

حين يتوجب على module أن يصبح خدمة قابلة للنشر باستقلال، تُختزل العملية بفضل الحدود أعلاه إلى خمس خطوات آلية:

flowchart TB
    S1["1. Swap the contract binding<br/><i>local impl → HTTP/gRPC client</i><br/>Callers unchanged"]
    S2["2. Point the outbox relay<br/>at a real broker<br/><i>Publishers/subscribers unchanged</i>"]
    S3["3. Move the module's tables<br/><i>No cross-module FKs to drop</i>"]
    S4["4. Deploy as a service<br/><i>same image, different entrypoint</i>"]
    S5["5. Replace the reconciliation job<br/>with cross-service checks"]
    S1 --> S2 --> S3 --> S4 --> S5

التكلفة التقديرية: ≤ 20 يوم-شخص لكل module (QAS-MOD-03) — شريطة أن تكون القواعد قد صمدت. فإن لم تصمد، فالتقدير بلا معنى والرقم الحقيقي هو إعادة كتابة.

المفاضلات المقبولة#

التكلفة ما يقابلها
لا تكامل مرجعي عابر للوحدات domain events للتسلسل التعاقبي + مهمة تسوية (reconciliation) تُبلّغ عن السجلات اليتيمة كمقياس، لا ككتابة فاشلة
الـ projections المحلية تكرّر بعض البيانات كل module يملك بالضبط ما يحتاجه؛ ولا join موزّع
كود قالبي أكثر — DTOs وعقود وأحداث يبقى الاستخراج إعادة هيكلة لا إعادة كتابة
تطوير أولي أبطأ نطاق المرحلة 2 في CONF-04 يجعل تآكل الحدود أعلى إخفاق مكلف احتمالًا

5.9 ما لا تفرضه هذه الوثيقة#

مُوثَّق ليكون نطاق هذه الوثيقة واضحًا:

  • لا تُفرض بنية داخلية لأي module. سواء استخدمت الوحدة services أو actions أو نموذج مجال أغنى، فذلك شأنها وحدها. العقد هو سطح Public/ الخاص بها، لا داخلها.
  • لا تجريد سابق لأوانه. الوحدة التي لها تطبيق واحد تحصل على تطبيق واحد. الـ ports موجودة حيث يكون القرار مؤجَّلًا عن قصد (SEAM-01 المدفوعات، SEAM-02 الفيديو) — لا في كل مكان.
  • CQRS وevent sourcing والنقاء السداسي (hexagonal) ليست مطلوبة. الـ outbox موجود من أجل الذرّية والاستخراج، لا لأن الأحداث موضة رائجة.

الهدف ليس الأناقة المعمارية. الهدف أن يكون نطاق المرحلة 2 في CONF-04 — POS/PMS، المخزون، CRM، التكامل الحكومي — قابلًا للبناء بشكل إضافي تراكمي، على يد فريق قد لا يضم أحدًا ممن كتب الـ MVP.

القائد التقني · أمير هارون مسودة v0.1 · بحث وتصميم فقط