الـ API والتكامل

الوصول بين التطبيقات (تطبيقك يحتاج بيانات تطبيق آخر)

تطبيق الجدول يحتاج يعرف مين غاب اليوم. بيانات الحضور تعيش في قاعدة بيانات تطبيق الحضور — مخطّط منفصل لا يصل إليه غيره. فالسؤال ليس «كيف أقرأ الجدول الفلاني»، بل «كيف أطلب من مطوّر تطبيق الحضور أن يفتح لي جزءاً محدَّداً من بياناته».

الجواب موافقتان لا واحدة:

مَن يوافق على ماذا أين
مطوّر التطبيق صاحب البيانات «يحقّ لهذا التطبيق أن يطلب» /dashboard/app-links
كل جهة تُثبِّت التطبيقين «نعم، اقرأ بيانات طلابنا نحن» صفحة التطبيق في المتجر

الثانية موجودة في okta-web أصلاً. الأولى هي ما نتحدّث عنه هنا، ولا يمكن تخطّيها: إعلان وصول بلا موافقة مطوّر لا يُنشَر.

1. إن كنت صاحب البيانات: أعلِن ما تشاركه

لا أحد يستطيع أن يطلب منك شيئاً لم تعلنه. في محرّر التطبيق (أو الإصدار) → «الوصول بين التطبيقات» → «ما يشاركه هذا التطبيق»:

"provides": [
  {
    "resource": "records",
    "access": "read",
    "label": "سجلّات الحضور اليومية",
    "label_en": "Daily attendance records",
    "description": "حضور الطالب وغيابه وتأخّره، بالتاريخ والحصّة.",
    "reader": "Modules\\Attendance\\Services\\PartnerApi\\RecordsReader"
  }
]
  • resource — حروف صغيرة و_ فقط، حتى ٤٠ حرفاً. هذا هو المفتاح الذي تُخزَّن به موافقة الجهة في okta-web، فتغييره لاحقاً يفصل كل موافقة قائمة عن معناها.
  • access — read أو write. ليستا درجتين على سلّم: عرض read لا يعني أنك عرضت write، والعكس. إن أردت الاثنين فأعلن صفّين.
  • label وdescription — تكتبهما للمدرسة لا للمبرمج. هذا النص نفسه هو ما يظهر على شاشة الموافقة في المتجر. «سجلّات الحضور اليومية» تُقرَأ ويُقرَّر بناءً عليها؛ att_rec_v2 لا.
  • reader — الحقل الذي يجعل العرض يُثمِر شيئاً. بقيّة الحقول أوراق موافقة: تُثبِت أن الرابط يجوز. هذا الصنف هو ما تشغّله المنصّة فعلاً لتُنتِج الصفوف. بدونه تستطيع المدرسة أن توافق على السطر، ثم لا يُرجِع شيئاً. مطلوب أن يكون تحت Modules\ في كودك أنت، وأن ينفّذ الواجهة — انظر القسم التالي.
  • writer — نظير reader على عرض access: write: الصنف الذي يقبل ما يرسله التطبيق الآخر ويتحقّق منه ويخزّنه بقواعدك. عرض كتابة بلا كاتب موافقةٌ تُمنَح ولا تُخزِّن شيئاً. انظر «الكتابة في بيانات تطبيق آخر» أدناه.

اكتب القارئ

namespace Modules\Attendance\Services\PartnerApi;

use App\Services\PartnerApi\Contracts\CrossAppReader;
use Modules\Attendance\Models\AttendanceRecord;

final class RecordsReader implements CrossAppReader
{
    public function read(string $resource, array $filters, string $consumerSlug): array
    {
        return AttendanceRecord::query()
            ->when($filters['from'] ?? null, fn ($q, $d) => $q->whereDate('date', '>=', $d))
            ->limit(200)
            ->get()
            ->map(fn ($r) => [
                'student_ulid' => $r->student->ulid,
                'date' => $r->date->toDateString(),
                'status' => $r->status,
            ])
            ->all();
    }
}

ثلاث قواعد لا تتفاوض:

  • لا تفحص الصلاحية هنا. المنصّة لا تستدعي صنفك إلا بعد أن تكون قد أثبتت الثلاثة: أن مطوّر التطبيق الطالب مُنِح هذا الزوج، وأن هذه الجهة بالذات وافقت عليه، وأن المُنادي هو $consumerSlug (مأخوذاً من سياق التشغيل لا من شيء أرسله المُنادي). فحصك الثاني يضيف تعقيداً لا أماناً.
  • $filters مدخلات غير موثوقة. تأتي من تطبيق آخر. تحقّق منها كما تتحقّق من $request، وضَع سقفاً للعدد.
  • أرجِع قيماً صِرفة، وULIDs لا معرّفات رقمية. المُنادي قاعدة كود أخرى قد تكون منشورة بإصدار مختلف؛ تمرير نموذج Eloquent يربط مخطّطَي المنتجين معاً فيصير أيّ تغيير اسم عمود لاحقاً كسراً لتطبيق شريك آخر.

يعمل الصنف داخل سياقك أنت: مخطّطك، وصلاحياتك المعتمَدة، والجهة الحاليّة. فاحصر استعلامك بالجهة الحاليّة كما تفعل في صفحاتك تماماً.

المدمجة فقط. القارئ صنف PHP يعيش في كود تشحنه داخل okta-web. التطبيقات الخارجية تُعلِن provides وتظهر على شاشة الموافقة، لكن لا قناة قراءة لها بعد.

لا يظهر إلا المنشور. التطبيقات الأخرى ترى provides من آخر إصدار منشور لك فقط. ما دام في مسوّدة فهو غير موجود بالنسبة لهم — ومقصود: الرابط التزام بين منتجين يعملان، وبناؤه على مسوّدة يعني أن ما تعتمد عليه قد يتغيّر أو يختفي قبل أن يراه أحد خارج فريقك.

وثّق ما تشاركه

label وdescription جملتان للمدرسة. المطوّر الذي يبني على عرضك يحتاج ما لا يتّسع له البيان: شكل الصفوف التي يُرجعها القارئ، والمرشِّحات التي يفهمها، والحمولة التي يقبلها الكاتب وما يُرجعه، ومثال نداء. لهذا وثائق البيانات المشتركة: صفحة Markdown لكل تطبيق، تكتبها من محرّر التطبيق («وثّق ما تشاركه» في بطاقة «ما يشاركه هذا التطبيق») أو من مساعد MCP:

set_shared_docs {
  "slug": "attendance",
  "body": "## records (read)\n\nكل صفّ: `student_ulid`, `date` (YYYY-MM-DD), `status` (present|absent|late).\n\nالمرشِّحات: `from` تاريخ البداية. الحدّ ٢٠٠ صفّ.\n\n```php\napp(ReadFromApp::class)('attendance', 'records', ['from' => '2026-09-01']);\n```\n\n## records (write)\n\nالحمولة: `student_ulid`, `date`, `status`. يُرجع `{id, status}`."
}

من يقرؤها: فريقك، ومطوّرو التطبيقات التي وافقتَ على طلب ربطها بتطبيقك — بعد الموافقة لا قبلها. طلب معلّق لا يقرأ شيئاً، وطلب مسحوب لم يعد يقرأ. ليست للمدارس ولا عامّة، فاكتب فيها تفاصيل الحقول التي لا تليق ببطاقة موافقة. وهي خارج البيان: تعديلها يصل القارئ فوراً بلا إصدار جديد.

2. إن كنت المحتاج: أرسل الطلب

/dashboard/app-links → «طلب وصول جديد». تختار تطبيقك، ثم التطبيق صاحب البيانات، ثم من قائمته المنشورة ما تحتاجه، ثم تكتب السبب.

السبب ليس إجراءً شكلياً: هو كلّ ما يقرأه المطوّر الآخر قبل أن يقرّر. اكتب فيه ما ستعرضه للمستخدم تحديداً («نعرض حصص الغياب داخل جدول الطالب، بلا تخزين») لا وصفاً عامّاً («للتكامل»).

3. الردّ: قبول كلّي أو جزئي أو رفض

مطوّر التطبيق صاحب البيانات يرى الطلب في «طلبات واردة»، ويستطيع:

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

الموافقة لا تحتاج تعليلاً، والرفض يحتاجه. وبعد الموافقة يبقى للمطوّر أن يسحبها (بسبب أيضاً) — وحينها لا يعود تطبيقك يستطيع إعلان ذلك الوصول في إصداراته القادمة.

بعد الموافقة تُفتح لك وثائق التطبيق المزوّد: زرّ «الوثائق» على الرابط في صفحة الروابط، أو get_shared_docs {"slug": "attendance"} من المساعد. اقرأها قبل أن تكتب سطراً من ReadFromApp أو WriteToApp؛ البيان لا يحمل إلا اسماً وجملة لكل عرض.

4. أعلِن الاعتماد في إصدارك

بعد الموافقة يظهر السطر في محرّر التطبيق تحت «بيانات يحتاجها هذا التطبيق». لا تكتبه يدوياً — القائمة مبنيّة من الموافقات السارية، فما لا يظهر فيها لا يمكن إعلانه أصلاً. تختار لكل سطر أمرين:

"cross_module_access": [
  {
    "module": "attendance",
    "resource": "records",
    "access": "read",
    "required": true,
    "reason": "لعرض حصص الغياب داخل جدول الطالب."
  }
]
  • required — هذا هو الحقل الذي تشعر به المدرسة:

    • true → المتجر يطلب منها تثبيت التطبيقين معاً، وتطبيقك لا يعمل بدونه.
    • false → بقيّة التطبيق تعمل، وتتعطّل هذه الميزة وحدها.

    اقلبه بالخطأ وستجد مدرسة عاجزة عن استخدام ما دفعت ثمنه. اجعله true فقط إذا كان التطبيق بلا هذه البيانات بلا معنى.

  • reason — يظهر حرفياً في صفحة المتجر بجانب زر الموافقة. اكتبه للمدرسة.

ما يحدث عند التثبيت — وعند كل تحديث

okta-web يقرأ cross_module_access من بيانك، ويُنشئ لكل سطر صفّاً في module_cross_access_grants غير ممنوح (is_granted = false). ثم تعرض صفحة التطبيق البطاقة على الجهة، وهي التي تمنح أو ترفض كل سطر على حدة. لا بيانات تتحرّك قبل ذلك.

والصفوف تُنشأ عند كل تحديث أيضاً، لا عند أول تثبيت فقط. إن أضفت الاعتماد في إصدار لاحق (مثلاً 2.3.17)، فكل جهة كانت قد ثبّتت تطبيقك قبله تحصل على الصفوف الناقصة لحظة نشر الإصدار، وتظهر البطاقة لديها بمفاتيحها — بلا إعادة تثبيت. الصفوف القائمة لا تُمَسّ: ما منحته الجهة يبقى ممنوحاً، وما رفضته يبقى مرفوضاً.

موافقة المطوّر ليست موافقة الجهة. موافقته تعني «يحقّ لهذا التطبيق أن يطلب» فقط. كل مدرسة تُسأل بعدها على حدة، ولها أن ترفض.

ومن في المدرسة يجيب؟ من تحمل أدواره صلاحية «اعتماد مشاركة البيانات بين التطبيقات» (app_store.cross_access.approve) — تُمنح لدور مدير الجهة افتراضاً، وللمدرسة أن تمنحها من تشاء. فمن يفتح صفحة تطبيقك بلا هذه الصلاحية يرى الحالة ولا يستطيع تغييرها. إن أبلغتك مدرسة أن الميزة لا تعمل وأن «لا أحد يستطيع الموافقة»، فهذا هو السؤال الأول: من عندهم يحملها.

ما تفحصه بوّابة الإصدار — وما لا تفحصه

المنصّة تفحص إعلان cross_module_access في ثلاث لحظات، ولا تفحص الشيء نفسه في كلٍّ منها:

اللحظة ما يُفحص لماذا
الحفظ في المحرّر كل المدخلات، ضدّ كتالوج المزوّد المنشور (وعلى بيئة التجربة: ما ثبّته تطبيقك الشقيق هناك) هنا تستطيع أن تفعل شيئاً — تحذف سطراً، أو تطلب موافقة جديدة
التقديم للمراجعة والنشر الجديد فقط: المدخلات غير الموجودة في إعلان إصدارك المنشور حالياً، بالمفتاح (module, resource, access) ما شُحن فُحص يوم شُحن؛ إعادة فحصه ضدّ كتالوج المزوّد الحالي كانت تعطّل إصدارك بسبب عثرة عند غيرك
إعادة النشر لا شيء إعادة النشر تدفع ما شُحن أصلاً ولا تضيف سطراً

القصّة التي فرضت القاعدة: تطبيقٌ يعلن قراءة attendance_status من «حضور»، نُشر ووافقت عليه المدارس والقراءة تعمل. ثم نشر «حضور» إصداراً سقطت منه provides سهواً — فصارت إعادة نشر التطبيق الأول تُرفض بـ«"حضور" لا ينشر okta_hdor.attendance_status.read في آخر إصدار منشور له»، بينما الإصدار المثبَّت عند المدارس ينشره فعلاً. التطبيق عوقب على تغيّر لم يُحدثه، والعقوبة تعطّل خطّ نشره كلّه — إصلاحٌ لا علاقة له بالمشاركة لا يستطيع الخروج.

فلماذا القاعدة آمنة: المدرسة لا تحتاج البوّابة لتحميها من سطر شُحن قبلُ. وقت التشغيل يرفض القراءة أصلاً إن لم يعد المزوّد ينشر المورد أو لم توافق المدرسة (ReadFromApp)، ويُسقط العرضَ المعطوب من الكتالوج. البوّابة عند النشر تحمي من سطر جديد يُشحن بلا موافقة مطوّر أو بلا عرض منشور — وهذا ما ما زالت تفعله بالضبط.

ما يعنيه لك:

  • إن رأيت «لا ينشر … في آخر إصدار منشور له» عند الحفظ على سطر كان يعمل، فالمزوّد غيّر كتالوجه. راسل مطوّره؛ إصدارك المنشور لا يتأثّر، ونشرك التالي لا يُعطَّل بهذا السطر.
  • الرسالة نفسها عند النشر تعني سطراً جديداً في هذا الإصدار: المزوّد لا ينشر ما تطلبه، أو لم يوافق بعد. هذا رفضٌ صحيح، ولا يُلتفّ عليه.
  • write بجانب read حيّ سطرٌ جديد — صفّا موافقة لا درجتان على سلّم.

الكتابة في بيانات تطبيق آخر

القناة كانت قناة قراءة بشكلها، والدليل كان يقول إن write يُعلَن ويُعرَض للمدرسة ولا يُفرَض. لم يعد الأمر كذلك: للكتابة بابها الخاص، والكلمة على بطاقة الموافقة هي التي تقرّر أيّ باب يُفتح.

عند صاحب البيانات — عرض access: write يسمّي writer:

{
  "resource": "records",
  "access": "write",
  "label": "تسجيل حضور من تطبيق آخر",
  "description": "يسمح لتطبيق آخر بتسجيل حضور طالب أو غيابه لهذا اليوم.",
  "writer": "Modules\\Attendance\\Services\\PartnerApi\\RecordsWriter"
}
namespace Modules\Attendance\Services\PartnerApi;

use App\Services\PartnerApi\Contracts\CrossAppWriter;
use Illuminate\Support\Facades\Validator;
use Modules\Attendance\Models\AttendanceRecord;

final class RecordsWriter implements CrossAppWriter
{
    public function write(string $resource, array $payload, string $consumerSlug): array
    {
        $data = Validator::validate($payload, [
            'student_ulid' => ['required', 'string'],
            'date' => ['required', 'date'],
            'status' => ['required', 'in:present,absent,late'],
        ]);

        $record = AttendanceRecord::query()->updateOrCreate(
            ['student_ulid' => $data['student_ulid'], 'date' => $data['date']],
            ['status' => $data['status'], 'source' => $consumerSlug],
        );

        return ['id' => $record->ulid, 'status' => $record->status];
    }
}

القواعد الثلاث نفسها التي للقارئ، وواحدة فوقها: $payload مدخلات من تطبيق آخر — تحقّق منها كما تتحقّق من $request، وارفض المعطوب برمي استثناء لا بالصمت؛ كاتبٌ يعود بلا خطأ عن شيء لم يُخزَّن يجعل التطبيق الآخر يظنّ أنه كتب.

عند المحتاج — نداء واحد، بنفس شكل القراءة:

use App\Services\PartnerApi\CrossApp\WriteToApp;

$written = app(WriteToApp::class)('attendance', 'records', [
    'student_ulid' => '01J…',
    'date' => '2026-09-13',
    'status' => 'absent',
]);
// ['id' => '01K…', 'status' => 'absent']

ما يفحصه النداء هو ما تفحصه القراءة، بفارق واحد يجب أن تعرفه: موافقة المدرسة على الكتابة موافقة أخرى. read وwrite صفّان منفصلان في module_cross_access_grants وسطران منفصلان على البطاقة، وokta-web يقارن عمود access الآن: موافقة read لا تفتح WriteToApp، وموافقة write لا تفتح ReadFromApp. تطبيقٌ يحتاج الاثنين يُعلن سطرين في cross_module_access ويُسأل عنهما المدرسة كلاً على حدة.

على بيئة التجربة: تطبيقاتك تقرأ بعضها بلا موافقة

على جهة بيئة التجربة، تطبيقان لك أنت يقرآن بعضهما مباشرةً: لا اعتماد من مطوّر آخر (لا يوجد)، ولا موافقة من الجهة (هي جهتك)، ولا نشر على الإنتاج. ما يُحكَم به هو ما ثبّته على التجربة: ثبّت التطبيق المزوّد هناك بإصدار يعلن المورد في provides ويسمّي له قارئاً، ثم نادِ ReadFromApp من التطبيق الآخر فتصلك البيانات. بطاقة الموافقة في متجر التجربة تعرض السطر بشارة «مُنحت تلقائياً» بدل مفتاح.

وفي محرّر الإصدار، تظهر موارد تطبيقاتك الأخرى المثبَّتة على التجربة قابلةً للإعلان حتى لو لم تُنشر بعد، بشارة «على بيئة التجربة فقط». هذا يكفي للحفظ وللتجربة، ولا يكفي للمراجعة: إرسال الإصدار للمراجعة يعود إلى القاعدة الأصلية — التطبيق الآخر يجب أن يكون قد نشر ما تعتمد عليه، لأن مدرسة على الإنتاج لا تُسأل الموافقة على مورد لم يُشحن إليها.

القاعدة لا تمرّ على جهة إنتاج أبداً، ولو كان التطبيقان لك: بيانات المدرسة بيانات المدرسة، والصفّ الممنوح هو الإذن الوحيد هناك.

مثال كامل: كتابة بين تطبيقين لك على التجربة، بلا نشر

الحالة: تطبيق «الحضور» يملك سجلّات الحضور، وتطبيق «البوّابة» يريد أن يسجّل غياب طالب عندما تمرّ بطاقته. كلاهما لك، ولم يُنشر أيّ منهما بعد. الخطوات من مساعد MCP (أو من المحرّر — الشاشتان تكتبان الأعمدة نفسها):

1. المزوّد يعلن عرض كتابة بكاتب — على إصدار الحضور القابل للتعديل:

set_shared_data {
  "slug": "attendance",
  "shares": [
    {
      "resource": "records",
      "access": "write",
      "label": "تسجيل حضور من تطبيق آخر",
      "description": "يسمح لتطبيق آخر بتسجيل حضور طالب أو غيابه لهذا اليوم.",
      "writer": "Modules\\Attendance\\Services\\PartnerApi\\RecordsWriter"
    }
  ]
}

ثم يكتب الصنف RecordsWriter كما في قسم «الكتابة في بيانات تطبيق آخر» أعلاه، ويدفعه إلى المستودع.

2. المزوّد يُثبَّت على التجربة — install_on_sandbox {"slug": "attendance"} (أو زرّ التثبيت في تبويب التجربة). هذا هو ما يُحكَم به: الإصدار المثبَّت على التجربة، لا آخر إصدار منشور.

3. المستهلك يعلن الاعتماد — يظهر العرض الآن في get_app_dependencies {"slug": "gate"} تحت «المتاح للإعلان» بعلامة «(على التجربة فقط)»:

set_app_dependencies {
  "slug": "gate",
  "dependencies": [
    {
      "module": "attendance",
      "resource": "records",
      "access": "write",
      "required": false,
      "reason": "لتسجيل غياب الطالب تلقائياً عند مرور بطاقته من البوّابة."
    }
  ]
}

يُقبل الحفظ لأن الاثنين لك والمزوّد مثبَّت على التجربة. (لو كان «الحضور» لمطوّر آخر لرُفض السطر حتى يوافق ذلك المطوّر وينشر العرض.)

4. المستهلك يُثبَّت على التجربة ويكتب — install_on_sandbox {"slug": "gate"}، ثم من كود البوّابة:

use App\Services\PartnerApi\CrossApp\WriteToApp;

$written = app(WriteToApp::class)('attendance', 'records', [
    'student_ulid' => $student->ulid,
    'date' => today()->toDateString(),
    'status' => 'absent',
]);

لا صفّ موافقة يُنتظر: على جهة التجربة، والتطبيقان لمطوّر واحد، يمرّ النداء مباشرةً إلى RecordsWriter في سياق الحضور. بطاقة الموافقة في متجر التجربة تعرض سطر الكتابة بشارة «مُنحت تلقائياً».

5. قبل المراجعة — انشر «الحضور» أولاً (فيصير عرض الكتابة منشوراً)، ثم أرسل «البوّابة» للمراجعة. بالترتيب المعكوس تُرفض البوّابة بـ«لا ينشر»، وهو الرفض الصحيح: مدرسةٌ على الإنتاج ستُسأل عن سطر كتابة، ولا بدّ أن يكون خلفه عرضٌ منشور.

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

قراءة البيانات فعلياً

استدعاء واحد، من داخل تطبيقك:

use App\Services\PartnerApi\CrossApp\ReadFromApp;

$records = app(ReadFromApp::class)('attendance', 'records', ['from' => '2026-08-01']);
// [['student_ulid' => '01J…', 'date' => '2026-08-16', 'status' => 'absent'], …]

ثلاثة وسائط: التطبيق صاحب البيانات، والمورد كما أعلنه هو في provides، ومرشِّحات اختيارية تمرّ كما هي إلى قارئه. تُرجِع ما أرجعه القارئ — مصفوفات وقيماً صِرفة.

لا تُمرِّر هويّتك. لا $tenantId ولا معرّف (slug) تطبيقك: كلاهما مقروء من سياق التشغيل الذي أنت فيه أصلاً. لو كان معرّف المُنادي وسيطاً لأمكن لأي تطبيق أن يدّعي أنه غيره ويستعير روابطه.

ما يفحصه النداء قبل أن يُرجِع شيئاً

  1. المُنادي هو سياق التشغيل الحالي، لا شيء أرسلته.
  2. الجهة وافقت على هذا السطر بعينه — (الطالب، المزوّد، المورد) — وهي البطاقة التي رأتها في المتجر. موافقة المطوّر وحدها لا تكفي.
  3. المزوّد ما يزال يعلن المورد وسمّى له قارئاً في بيان الإصدار المُثبَّت، لا في السطر الذي حصلت عليه يوم الموافقة. مَن سحب المورد يتوقّف عن الإجابة عليه.

ثم تدخل المنصّة سياق المزوّد وتستدعي قارئه هناك، فيقرأ بياناته باعتماداته هو، وتستعيد سياقك بعد الرجوع. أنت لا تلمس مخطّطه ولا تعرف عنه شيئاً — وهذا مقصود: لكل تثبيت مخطّط Postgres خاص خلف دور مُقيَّد، فالقراءة المباشرة ليست ممنوعة فحسب، بل مستحيلة تقنياً.

الفشل استثناء لا مصفوفة فارغة

سطر لم تمنحه الجهة يرمي RuntimeException برسالة تقول أيّ الأبواب أُغلق، ولا يعيد [] — لأن «لا يوجد ما تقرأه» و«لست مسموحاً لك» حالتان مختلفتان تماماً، وخلطهما يجعلك تشحن لوحة فارغة بدل خطأ قابل للإصلاح.

وهذا يعني أنك تتوقّع الاستثناء وتتعامل معه كحالة طبيعية على سطر required=false:

try {
    $records = app(ReadFromApp::class)('attendance', 'records');
} catch (\RuntimeException) {
    $records = null; // اعرض الجدول بلا عمود الغياب.
}

على سطر required=true هو ما يحدث بين التثبيت والموافقة فقط، وبعدها لا يتكرّر.

لماذا لا CrossModuleAccessService::hasAccess()؟ كان هذا الدليل يوجّهك إليه، وكان خطأً من وجهين: فاحص السياسة يرفض استيراده (internal-service-import — العقد هو App\Services\PartnerApi\* وحده)، وهو يعيد true/false على أي حال، فيُخبرك أنك مسموح لك بالقراءة ولا يعطيك بها سبيلاً. ReadFromApp يفحص الشيء نفسه ثم يُرجِع البيانات.

حدود يجب أن تعرفها

  • write مفروض. okta-web يقارن عمود access: موافقة read تفتح القراءة وحدها، وموافقة write تفتح الكتابة وحدها. تطبيقٌ كان يعتمد على أن صفّ قراءة يمرّ في فحص كتابة (وهو ما كان يقوله هذا الدليل) يحتاج الآن عرض write بكاتب عند المزوّد وسطر write في إعلانه وموافقة المدرسة عليه.
  • السحب لا يرجع بالزمن. إن سحب المطوّر موافقته، تتوقّف إصداراتك القادمة عن إعلان الوصول، لكن الجهات التي منحته أصلاً تبقى موافقتها سارية حتى تُحدَّث المنصة.
  • تطبيقاك أنت لا يحتاجان طلباً. تطبيقان لجهة واحدة لا شيء بينهما للتفاوض — تُعلِن الاعتماد مباشرةً. على بيئة التجربة يكفي أن يكون التطبيق الآخر مثبَّتاً هناك بالمورد؛ وعند المراجعة يبقى ملزَماً بأن يكون قد نشر ما تُسمّيه في provides.

عرض الدليل كاملاً في صفحة واحدة · ↧ Markdown خام