لو بتبني مكوّن (Component) جوملا لعميل، فأكيد جه عليك اليوم اللي الموديل محتاج فيه أداة جوملا ما بتوفرهاش: خدمة أسعار صرف، أو API شحن، أو بوابة رسائل. والحل السهل إنك تكتب new جوه الموديل، وده بالظبط اللي هيتعبك بعدين.
جوملا 6.2 خلّى الطريقة النظيفة أقصر بكتير. هنا هتشوف إزاي تحقن خدمتك في الموديلات خطوة بخطوة، والفرق بين اللي كان لازم تعمله لحد 6.1 واللي بقى كفاية دلوقتي.
Joomla 6.2
إضافة خدمة لموديل جوملا في 4 خطوات
عرّف الخدمة بواجهة
ExchangeRatesInterface ثم سجّلها في الحاوية بـ share().
علّم الموديل
واجهة Aware بدالة setter واحدة تعلن ما يحتاجه الموديل.
وسّع المصنع
ShopMVCFactory يقرأ العلامة ويسلّم الخدمة للموديل.
وسّع مزوّد الخدمة
صنف صغير يرث مزوّد MVCFactory ويضيف خدمتك فقط.
لماذا لا تنشئ الخدمة داخل الموديل؟
افترض أن عندك مكوّن متجر يعرض الأسعار بعملة الزائر، والموديل يحتاج أسعار الصرف اليومية. لو كتبت new EcbExchangeRates() داخل الموديل، صار الموديل مربوطاً بمزوّد أسعار واحد، ولن تستطيع اختباره ببديل وهمي يعيد قيمة ثابتة.
حقن التبعيات (Dependency Injection) يحل ذلك: الخدمة تُنشأ في مكان آخر وتُسلَّم للموديل، والموديل يعلن فقط ما يحتاجه. في جوملا، هذا “المكان الآخر” هو الـ MVCFactory، الكائن الذي ينشئ المتحكمات والموديلات والعروض والجداول ويزوّدها بقاعدة البيانات ومصنع النماذج ومرسل الأحداث وغيرها.
المشكلة أن الـ MVCFactory القياسي يعرف قائمة جوملا فقط. خدمتك ليست عليها، ولا حتى مسجّلة في حاوية الخدمات (DI Container) الخاصة بمكوّنك.
الخطوة 1: عرّف الخدمة بواجهة وسجّلها في الحاوية
ابدأ بواجهة (Interface) تصف ما تقدمه الخدمة، دون تفاصيل التنفيذ:
interface ExchangeRatesInterface
{
public function getRate(string $from, string $to): float;
}
التنفيذ الحقيقي في مثال المقال الأصلي، EcbExchangeRates، يجلب أسعار البنك المركزي الأوروبي عبر عميل HTTP في جوملا. لو متجر عميلك في مصر أو الخليج، اكتب تنفيذاً آخر لنفس الواجهة يعتمد على مزوّد الأسعار الذي تختاره، والموديل لن يتغير.
ثم سجّل الخدمة في حاوية المكوّن داخل services/provider.php:
$container->share(
ExchangeRatesInterface::class,
fn (Container $container) => new EcbExchangeRates(HttpFactory::getHttp())
);
الدالة share() تعني: ابنِ الكائن مرة واحدة، ثم سلّم النسخة نفسها في كل طلب. المفتاح هو اسم الواجهة، والقيمة وصفة تنتج التنفيذ؛ فتبديل المزوّد تعديل في هذا السطر وحده.
الخطوة 2: علّم الموديل بواجهة Aware
الموديل يعلن حاجته بواجهة صغيرة فيها دالة setter واحدة، كأنها لافتة على الصنف تقول: “سلّمني أسعار الصرف عبر setExchangeRates()“:
interface ExchangeRatesAwareInterface
{
public function setExchangeRates(ExchangeRatesInterface $rates): void;
}
class ProductModel extends ItemModel implements ExchangeRatesAwareInterface
{
private ExchangeRatesInterface $rates;
public function setExchangeRates(ExchangeRatesInterface $rates): void
{
$this->rates = $rates;
}
public function getPriceIn(float $priceInEuro, string $currency): float
{
return $priceInEuro * $this->rates->getRate('EUR', $currency);
}
}
لاحظ أن الموديل لا يعرف إلا الواجهة. هذه هي القطعة التي تجعل الخدمة قابلة للتبديل كقطعة بازل: أي تنفيذ يطابق الواجهة يركب في المكان نفسه.
الخطوة 3: مصنع مخصص يقرأ العلامة
أنشئ ShopMVCFactory يرث MVCFactory. بعد أن ينشئ جوملا الموديل بالطريقة المعتادة، يفحص المصنع: هل يحمل الموديل علامة ExchangeRatesAwareInterface؟ إن كان نعم، يسلّمه الخدمة:
final class ShopMVCFactory extends MVCFactory
{
private ExchangeRatesInterface $rates; // filled via a setter
public function createModel($name, $prefix = '', array $config = [])
{
$model = parent::createModel($name, $prefix, $config);
if ($model instanceof ExchangeRatesAwareInterface) {
$model->setExchangeRates($this->rates);
}
return $model;
}
// createController() and createView() can do the same
}
هذه بالضبط طريقة جوملا مع أدواته: الموديل الذي يطبّق DatabaseAwareInterface تُستدعى عليه setDatabase(). وأنت تضيف الجديد فقط. وبما أن مصنعك هو الـ MVCFactory، فكل متحكم ينشئه يحصل على مصنعك أيضاً، وعندما يطلب موديلاً تُنفَّذ دالتك createModel().
في Joomla 6.2 ينصح الكاتب بأن يطبّق المصنع نفسه واجهة ExchangeRatesAwareInterface أيضاً، وأسهل طريقة هي trait باسم ExchangeRatesAwareTrait تستخدمه في المصنع والموديل معاً.
الخطوة 4: وسّع مزوّد الخدمة (جوملا 6.2)
هنا الفرق الكبير. حتى جوملا 6.1 لم يكن ممكناً توسيع مزوّد الخدمة (Service Provider) الأساسي JoomlaCMSExtensionServiceProviderMVCFactory، لأن خاصية الـ namespace خاصة (private) والوصفة دالة مجهولة داخل register(). فكنت مضطراً لنسخ الوصفة كلها:
$container->set(
MVCFactoryInterface::class,
function (Container $container) {
$factory = new ShopMVCFactory('\Acme\Component\Shop');
// Copied from the core service provider for the MVCFactory.
$factory->setFormFactory($container->get(FormFactoryInterface::class));
$factory->setDispatcher($container->get(DispatcherInterface::class));
$factory->setDatabase($container->get(DatabaseInterface::class));
$factory->setSiteRouter($container->get(SiteRouter::class));
$factory->setCacheControllerFactory($container->get(CacheControllerFactoryInterface::class));
$factory->setUserFactory($container->get(UserFactoryInterface::class));
$factory->setMailerFactory($container->get(MailerFactoryInterface::class));
// Add your custom service.
$factory->setExchangeRates($container->get(ExchangeRatesInterface::class));
return $factory;
}
);
أي أداة جديدة يضيفها جوملا لاحقاً لن تصل لمصنعك لأنك نسخت القائمة القديمة.
في جوملا 6.2، أضاف طلب الدمج رقم 47718 ثلاث دوال قابلة للتجاوز في المزوّد الأساسي:
| الدالة | وظيفتها |
|---|---|
getNamespace() | تعطيك namespace المكوّن الذي كان مغلقاً |
| — | — |
createMVCFactory() | تحدد صنف المصنع الذي يُنشأ؛ تجاوزها لتعيد مصنعك |
injectServicesIntoFactory() | تسلّم الخدمات للمصنع؛ استدعِ الأصل ثم أضف خدمتك |
فيصير المزوّد صنفاً صغيراً:
use JoomlaCMSExtensionServiceProviderMVCFactory as MVCFactoryProvider;
final class ShopMVCFactoryProvider extends MVCFactoryProvider
{
protected function createMVCFactory(): MVCFactoryInterface
{
return new ShopMVCFactory($this->getNamespace());
}
protected function injectServicesIntoFactory(MVCFactoryInterface $factory, Container $container): void
{
parent::injectServicesIntoFactory($factory, $container); // Joomla's own list
if ($factory instanceof ExchangeRatesAwareInterface) {
$factory->setExchangeRates($container->get(ExchangeRatesInterface::class));
}
}
}
وفي services/provider.php يكفي سطر واحد:
$container->registerServiceProvider(new ShopMVCFactoryProvider('\Acme\Component\Shop'));
تنبيه: لو تجاوزت createMVCFactory() ومكوّنك يقدّم Web Services API، فعليك إنشاء ApiMVCFactory أيضاً. راجع الدالة الأصلية createMVCFactory() في النواة لترى كيف تفعل ذلك.
اجعل الخدمة كسولة: lazy()
بالطريقة السابقة، تُبنى EcbExchangeRates ومعها عميل HTTP بمجرد بناء المصنع، حتى في صفحات لا تعرض أي سعر. التحميل الكسول (Lazy Loading) يؤجل ذلك:
$container->share(
ExchangeRatesInterface::class,
$container->lazy(
EcbExchangeRates::class,
fn (Container $container) => new EcbExchangeRates(HttpFactory::getHttp())
)
);
الحاوية تسلّم الآن بديلاً (lazy proxy) يشبه EcbExchangeRates تماماً، ولا يبني الكائن الحقيقي إلا عند أول استدعاء لدالة عليه. المصنع والموديل لا يتغيران.
ملاحظة: lazy() تحتاج اسم الصنف الفعلي لا الواجهة، وتعتمد على الكائنات الكسولة في PHP 8.4. على إصدارات PHP أقدم تبني الكائن فوراً بلا خطأ. راجع إصدار PHP في استضافتك؛ كثير من الاستضافات المشتركة في المنطقة ما زالت تضع إصداراً أقدم افتراضياً، ويمكنك غالباً تغييره من لوحة التحكم.
ماذا عن جوملا 6.3؟
هناك طلب دمج تجريبي (Draft) رقم 48193 يقترح AutowireFactory: الحاوية تقرأ ما يطلبه الصنف وتجلبه بنفسها، فلا تحتاج مصنعاً مخصصاً ولا مزوّداً ولا حتى واجهة Aware، ويكفي تسجيل الخدمة في الحاوية. المقترح ما زال قيد النقاش وليس مضموناً في 6.3، فلا تبنِ عليه الآن.
بصراحة: هذا الشرح مبني على مقال مجلة جوملا وصفحة طلب الدمج الرسمية، لا على مكوّن جرّبناه على جوملا 6.2. اختبر الكود على بيئة تطوير قبل نقله لموقع عميل.
في ووردبريس: لا يوجد في النواة مقابل مباشر للـ MVCFactory؛ الإضافات الكبيرة تستخدم عادة حاوية خدمات خاصة بها. لو تعمل على المنصتين، ففكرة “الواجهة + الحاوية” نفسها تنتقل معك.
اقرأ أيضًا
الأسئلة الشائعة
س: ما هو حقن التبعيات في جوملا؟
ج: طريقة تسلّم بها جوملا الأدوات للمتحكمات والموديلات بدل أن تنشئها بنفسها. أُدخلت في جوملا 4 بأسلوب حقن عبر دوال setter للحفاظ على التوافق مع الإصدارات السابقة.
س: ما الفرق بين share() و set() في حاوية جوملا؟
ج: حسب المقال الأصلي، share() تبني الكائن مرة واحدة وتسلّم النسخة نفسها في كل طلب، وهو المطلوب لخدمة مثل أسعار الصرف.
س: هل أحتاج تعديل مكوّني القديم عند الترقية إلى جوملا 6.2؟
ج: لا، التغيير متوافق مع الإصدارات السابقة حسب صاحب طلب الدمج. لكن الترقية فرصة لحذف الوصفة المنسوخة واستبدالها بالمزوّد الصغير.
س: لماذا لا أكتب new داخل الموديل وخلاص؟
ج: لأن الموديل يرتبط بتنفيذ واحد، فيصعب تبديل المزوّد أو اختباره ببديل وهمي. مع الحقن يتغير المزوّد من سطر واحد في الحاوية.
الخلاصة
الخدمة تتعرّف بواجهة، وتتسجّل في الحاوية، والموديل يعلن حاجته، والمصنع يسلّمها. في جوملا 6.2 الخطوة الأخيرة بقت صنف صغير بدل كود منسوخ من النواة. لو عندك مكوّن لعميل فيه new جوه الموديلات، ابدأ بخدمة واحدة النهارده وانقلها للحاوية.
مراجع إضافية: [6.2] Refactor MVCFactory service provider to make it easier to extend، Registering subdependencies – Joomla Manual
مرجع المقال (بالإنجليزية): Adding a service via the MVCFactory
المقال ده مش ترجمة حرفية: متكيّف للقارئ العربي ومضاف عليه سياق ووردبريس وElementor وJetEngine.



