arc42 §5 — عرض الكتل البنائية (C4 المستوى 3)
الحالة: مسودة v0.1 · التاريخ: 2026-07-20
يتتبّع إلى: CON-14 (modular monolith), QAS-MOD-03 (الاستخراج), R-08 (تآكل الحدود), SEAM-01…SEAM-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 & 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-01)، SubscriptionStatus |
🟢 الأعلى — 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
}
بندان غير قابلين للتفاوض:
- الحمولات أنواع أولية.
['booking_id' => 123, 'consultant_id' => 45]، ولا نموذج أبدًا. الحمولة التي تُسلسَل إلى JSON بنظافة اليوم تصبح رسالة وسيط غدًا بتكلفة صفرية. أما الحمولة التي تحمل نموذج Eloquent فهي إعادة كتابة. - الإصدار من اليوم الأول. إضافته بعد أن يصير لديك مشتركون تعني عملية ترحيل؛ أما وجوده مع تجاهله فلا يكلّف شيئًا.
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.
القواعد المطلوب ضبطها:
- كل module طبقة؛ و
Public/وInternal/طبقتان فرعيتان - الانتقال من module إلى module مسموح فقط عبر
Public/ - ترتيب الطبقات وفق §5.3 — لا حواف صاعدة، ولا دورات
Shared/لا يجوز أن تعتمد على شيء--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.