تزریق وابستگی (Dependency Injection)

تزریق وابستگی در جوملا ۴ معرفی شد تا امکان تست بهتر کدهای جوملا فراهم شود.

مفهوم پایه‌ای

ایده‌ی اصلی پشت تزریق وابستگی در جوملا این است که توسعه‌دهندگان دیگر نباید مستقیماً به نمونه‌های کلاس‌های کلیدی جوملا از طریق کدهای زیر دسترسی پیدا کنند:

 
$obj = new JoomlaClass();
// or
$obj = JoomlaClass::getInstance();

 

در عوض، باید درخواست خود را به کانتینر تزریق وابستگی (Dependency Injection Container یا به اختصار DIC) بدهند:

$obj = $container(– لطفاً یک نمونه از کلاس JoomlaClass به من بده –);

یا نمونه‌ای را از طریق کلاس Application یا یک کلاس Factory که خودش از کانتینر DIC گرفته شده، دریافت کنند.

دلیل اصلی این کار، فراهم کردن امکان تست بهتر است، بدین صورت که کلاس‌ها به راحتی قابلیت «موک» (mock) شدن پیدا کنند. اگر در کد چندین بار مستقیماً از `JoomlaClass::getInstance()` استفاده شود، تست و موک کردن آن کلاس دشوار است.

اما اگر کد همیشه برای گرفتن نمونه کلاس از DIC استفاده کند، به راحتی می‌توانیم در زمان تست برای موک کردن، به جای کلاس واقعی، کلاس موک شده را وارد DIC کنیم. برای اینکه کمتر مشخص شود که دقیقا کدام کلاس خواسته شده، می‌توانیم به جای درخواست نام کلاس، از یک اینترفیس مشخص بخواهیم:

$obj = $container(– لطفاً یک نمونه از کلاسی که اینترفیس JoomlaInterface را پیاده‌سازی می‌کند به من بده –);

DIC سپس بر اساس پیکربندی، یا کلاس واقعی را برمی‌گرداند یا کلاس موک شده را. توجه داشته باشید که استفاده از نام کلاس یا نام اینترفیس هیچ تاثیری روی عملکرد ندارد؛ همانطور که می‌بینیم، کلید (key) استفاده شده برای قرار دادن و دریافت اشیاء از DIC صرفاً یک رشته (string) است. بنابراین فقط لازم است هنگام ذخیره و بازیابی از DIC، از یک رشته کلید ثابت استفاده کنیم.

 کانتینر تزریق وابستگی (DIC)

کانتینر تزریق وابستگی جوملا، که معمولاً با مخفف DIC شناخته می‌شود، در اصل یک مخزن (ریپازیتوری) از زوج‌های کلید-مقدار (key-value pairs) است که در آن:

- کلید (key) معمولاً یک رشته است که نام کامل (fully qualified) یک کلاس یا اینترفیس را نشان می‌دهد.

- مقدار (value) می‌تواند یک نمونه (instance) از کلاس مربوطه باشد، یا یک تابع (function) که یک نمونه از آن کلاس را بر می‌گرداند.

 

واقعا مهم نیست کلید چه باشد — فقط کافی است معنی‌دار باشد و شما هنگام وارد کردن (set) و گرفتن (get) یک مقدار از کانتینر، از همان کلید استفاده کنید. البته کلید باید یکتا (unique) باشد.

کانتینر تزریق وابستگی (DIC)

کار با DIC

شما موارد را با استفاده از متد `set()` در کانتینر قرار می‌دهید که باید پارامترهای زیر را بدهید:

- کلید (key):  نام کلاس یا نام اینترفیس

- مقدار (value):یک تابع که نمونه‌ای از کلاس را برمی‌گرداند (یا خودِ نمونه کلاس بدون تابع هم می‌تواند باشد)

- shared (اشتراک‌گذاری): یک مقدار بولی (true/false) که مشخص می‌کند آیا نمونه کلاس به صورت مشترک استفاده شود یا نه؛ یعنی اگر درخواست دوم برای همین نمونه شد، همان نمونه قبلی برگردانده شود یا نمونه جدید ساخته شود.

- protected (محافظت شده): یک مقدار بولی که مشخص می‌کند آیا این ورودی کانتینر محافظت شده است یا خیر. اگر محافظت شده باشد، تلاش برای دوباره `set()` کردن آن با همان کلید باعث خطا خواهد شد.

متد `share()` همان `set()` است ولی مقدار `shared` آن همیشه `true` است.

دریافت آیتم ها از کانتینر

برای گرفتن یک آیتم از کانتینر، متد `get()` را با کلید منبع مورد نظر صدا می‌زنید. عملکرد تزریق وابستگی به صورت زیر است:

- ابتدا کلید را در کانتینر جستجو می‌کند.

- اگر مقدار مربوطه از قبل یک نمونه‌ی کلاس نباشد، تابع مربوط به آن را اجرا و یک نمونه تولید می‌کند.

- اگر منبع مشترک (shared) باشد، نمونه ساخته شده را ذخیره می‌کند تا در درخواست‌های بعدی همان نمونه برگردانده شود.

- نمونه کلاس را به شما بازمی‌گرداند.

تعریف نام مستعار (Alias)

همچنین می‌توانید برای هر کلید در کانتینر یک یا چند نام مستعار تعریف کنید. این به شما اجازه می‌دهد که هنگام فراخوانی `get()` هم از کلید اصلی و هم از نام مستعار آن استفاده کنید.

مثال JConfig

در مراحل اولیه‌ی فرآیند راه‌اندازی جوملا، کانتینر تزریق وابستگی (DIC) با فراخوانی تابع `createContainer()` در فایل `library/src/Factory.php` مقداردهی اولیه می‌شود. این کار باعث می‌شود تابع `register()` در هر فایل کلاسی داخل مسیر `libraries/src/Service/Provider` فراخوانی شود. با مرور این فایل‌ها می‌توانید ببینید چه کلاس‌ها و مقادیری در زمان مقداردهی اولیه به کانتینر اضافه می‌شوند.

بیایید به فایل `library/src/Service/Provider/Config.php` نگاه کنیم که کلاس پیکربندی `JConfig` را تنظیم می‌کند. کد اجرایی درون تابع `register()` به صورت زیر است:

 
$container->alias('config', 'JConfig')
    ->share(
        'JConfig',
        function (Container $container) {
            if (!is_file(JPATH_CONFIGURATION . '/configuration.php')) {
                return new Registry();
            }

            \JLoader::register('JConfig', JPATH_CONFIGURATION . '/configuration.php');

            if (!class_exists('JConfig')) {
                throw new \RuntimeException('Configuration class does not exist.');
            }

            return new Registry(new \JConfig());
        },
        true
    );
 

 توضیح کد:

- `$container->alias('config', 'JConfig')`

  این دستور یک نام مستعار (alias) در کانتینر ایجاد می‌کند به طوری که شما بتوانید هم شیء کانتینر را به این صورت `$container->get('config')` و هم به این شکل `$container->get('JConfig')` فراخوانی کنید. توجه داشته باشید که بازگشت این تابع خود `$container` است تا بتوانید دستورهای زنجیره‌ای (chaining) بنویسید.

سپس متد `share()` با ۳ پارامتر فراخوانی شده:

1. رشته `'JConfig'` که کلید مورد نظر در کانتینر است.

2. یک تابع (Closure) که قرار است یک نمونه از کلاس مربوطه را برگرداند — در ادامه توضیح داده می‌شود.

3. مقدار `true` که مربوط به پارامتر `protected` است. یعنی اگر دوباره بخواهیم با کلید `'JConfig'` یک مقدار جدید داخل کانتینر قرار دهیم توسط کانتینر رد می‌شود و مقدار قبلی بازنویسی نمی‌شود.

توجه کنید که متد `share()` در واقع همان `set()` است با این تفاوت که پارامتر `shared` برابر `true` است، یعنی هر بار که `get('JConfig')` فراخوانی شود همان نمونه کلاس قبلی برگردانده می‌شود.

وقتی کدی در جوملا فراخوانی می‌کند:

$container->get('JConfig')

تابع دوم که به عنوان پارامتر به `share()` داده شده اجرا می‌شود.

کاری که این تابع انجام می‌دهد:

- بررسی می‌کند که فایل `configuration.php` وجود داشته باشد، اگر وجود نداشت یک شیء خالی از نوع `Registry` برمی‌گرداند.

- کلاس `JConfig` را با استفاده از `JLoader` ثبت می‌کند تا بارگذار خودکار (autoload) جوملا بتواند آن را پیدا کند.

- چک می‌کند که آیا کلاس `JConfig` وجود دارد یا خیر که این کار باعث می‌شود PHP تابع autoload جوملا را اجرا کند و فایل `configuration.php` را بارگذاری کند. اگر باز هم کلاس `JConfig` پیدا نشود، استثنایی پرتاب می‌شود.

- یک نمونه از کلاس `JConfig` ساخته می‌شود و درون یک شیء `Registry` قرار گرفته و آن برگشت داده می‌شود.

چون این ورودی کانتینر به صورت مشترک (`shared`) تنظیم شده، شیء `Registry` در کانتینر ذخیره می‌شود و برای درخواست‌های بعدی `get('JConfig')` همان شیء بازگردانده خواهد شد.

نحوه دسترسی به تنظیمات عمومی

می‌توانید تنظیمات سراسری جوملا را به شکل زیر دریافت کنید:

 
use Joomla\CMS\Factory;
$container = Factory::getContainer(); 
// یکی از این دو را می‌توانید استفاده کنید:  
$config = $container->get('JConfig');  
$config = $container->get('config');

 

این درخواست‌ها مستقیماً به DIC می‌روند و نمونه‌ی اشتراکی `JConfig` را برمی‌گردانند.

همچنین می‌توانید این مقادیر را را از طریق نمونه Application که قبلاً آنها را از DIC دریافت کرده است به دست آورید:

 
use Joomla\CMS\Factory;
$application = Factory::getApplication();
$application->getConfig();   
// or, to get a particular parameter:
$application->get($paramName, $defaultValue);
 

افزونه‌ها و کانتینرهای فرزند

هر بار که جوملا یک افزونه (extension) را راه‌اندازی می‌کند، یک کانتینر تزریق وابستگی فرزند (Child DIC) مخصوص آن افزونه ایجاد می‌کند. این کانتینر فرزند به صورت شماتیک به شکل زیر قابل تصور است:

افزونه‌ها و کانتینرهای فرزند

کانتینر فرزند یک اشاره‌گر (pointer) به کانتینر اصلی (Parent DIC) دارد و تا حد زیادی مشابه کانتینر اصلی عمل می‌کند، اما نه کاملاً:

- هر بار که متد `set()` روی کانتینر فرزند فراخوانی شود، زوج کلید-مقدار (key-value) داخل خود کانتینر فرزند قرار می‌گیرد.

- هر بار که متد `get()` روی آن فراخوانی شود، ابتدا در کانتینر فرزند دنبال منبع (resource) می‌گردد، و اگر آن را پیدا نکند، کانتینر والد (اصلی) را نیز جستجو می‌کند.

از نسخه ۴ جوملا به بعد، به توسعه‌دهندگان افزونه‌ها توصیه می‌شود که برای افزونه‌های خود از تزریق وابستگی استفاده کنند و در این راه یک فایل `services/provider.php` تعریف نمایند. بارگذاری افزونه حالا به صورت دو مرحله‌ای انجام می‌شود و این مراحل در فایل `services/provider.php` مدیریت می‌شوند:

1. کلاس افزونه (extension class) در کانتینر فرزند ثبت می‌شود.

2. کلاس افزونه از کانتینر فرزند گرفته می‌شود و نمونه‌ای از آن ساخته می‌شود.

مثال ساده برای com_example

فرض کنید افزونه‌ای دارید به نام `com_example` با فضای نام (namespace) زیر:

namespace Mycompany\Component\Example\Administrator\Extension;

فایل `services/provider.php` می‌تواند به صورت زیر باشد:

 
use Joomla\CMS\Extension\ComponentInterface;  
use Joomla\DI\Container;  
use Joomla\DI\ServiceProviderInterface;  
use Mycompany\Component\Example\Administrator\Extension\ExampleComponent;  
return new class implements ServiceProviderInterface {  
    public function register(Container $container): void   
    {  
        $container->set(  
            ComponentInterface::class,  
            function (Container $container) {  
                $component = new ExampleComponent();  
                return $component;  
            }  
        );  
    }  
};

 

در این کد:

- وقتی جوملا این فایل PHP را `require` می‌کند:

$provider = require $path;  // $path مسیر فایل services/provider.php است

- متغیر `$provider` به نمونه‌ای از این کلاس ناشناس اشاره می‌کند که اینترفیس `ServiceProviderInterface` را پیاده‌سازی می‌کند.

- این اینترفیس یعنی متد `register` را با امضای مشخص شده باید داشته باشد.

سپس زمانی که جوملا این کد را اجرا می‌کند:

 
if ($provider instanceof ServiceProviderInterface) {  
    $provider->register($container);  
}

 

- متد `register` فراخوانی می‌شود و این باعث می‌شود که یک ورودی در کانتینر فرزند افزوده شود:

  - کلید (key) برابر رشته `ComponentInterface::class` که کوتاه شده‌ی عبارت زیر است:

'Joomla\CMS\Extension\ComponentInterface'

  - مقدار (value) تابعی است که نمونه‌ی جدیدی از `ExampleComponent`، یعنی کلاس افزونه `com_example` را برمی‌گرداند.

وقتی بعداً جوملا اجرا می‌کند:

 
$extension = $container->get($type);

 

تابع تعریف شده اجرا شده و یک نمونه جدید از `ExampleComponent` برگردانده می‌شود.

نکته نهایی

می‌توانید این روند را در سورس جوملا در مسیر زیر مشاهده کنید و متوجه شوید که همین الگو برای ماژول‌ها و پلاگین‌ها نیز به کار می‌رود.

`libraries/src/Extension/ExtensionManagerTrait.php`

در این فایل همچنین خط زیر مشاهده می‌شود:

 
if ($extension instanceof BootableExtensionInterface) {  
    $extension->boot($container);  
}
 

این یعنی اگر کلاس افزونه اینترفیس `BootableExtensionInterface` را پیاده‌سازی کرده باشد، . همانطور که در «کلاس افزونه برای کامپوننت ها» توضیح داده، جوملا بعد از ساخت نمونه به صورت خودکار متد `boot()` آن را فراخوانی می‌کند.

 ثبت وابستگی‌های فرعی

آخرین نکته‌ای که در درک فایل‌های سرویس پرووایدر باید بدانیم، ثبت وابستگی‌های فرعی است. فرایند دو مرحله‌ای بارگذاری افزونه‌ها که پیش‌تر به صورت جزئی توضیح داده شد، در واقع به این صورت است:

1. کلاس افزونه (Extension) وارد دایره وابستگی‌های فرزند (child DIC) می‌شود و هر وابستگی کلاس افزونه نیز ثبت می‌گردد (به عبارت دیگر وارد دایره وابستگی فرزند می‌شود).

2. کلاس افزونه از داخل دایره وابستگی فرزند استخراج می‌شود و نمونه‌ای از کلاس ساخته می‌شود. سپس هر وابستگی فرعی کلاس افزونه نیز از داخل دایره وابستگی گرفته می‌شود، به طوری که نمونه کلاس افزونه اشاره‌گری به این اشیاء وابسته دارد تا زمان نیاز به آن‌ها دسترسی داشته باشد.

فایل services/provider.php مینیمال

بیایید نمونه خیلی ساده‌ای از فایل برای com_example با فضای نام Mycompany\Component\Example ببینیم. در اینجا فرض می‌کنیم com_example نیاز به کلاس Extension اختصاصی ندارد و می‌تواند از کلاس MVCComponent ارائه‌شده توسط جوملا به عنوان کلاس Extension استفاده کند. پس این فایل services/provider.php برای یک کامپوننت ساده تقریباً مینیمال‌ترین شکل است.

 
use Joomla\CMS\Extension\ComponentInterface;  
use Joomla\CMS\Extension\MVCComponent;  
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;  
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;  
use Joomla\DI\Container;  
use Joomla\DI\ServiceProviderInterface;  

return new class implements ServiceProviderInterface {  
    public function register(Container $container): void   
    {  
        $container->registerServiceProvider(new Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory('\\Mycompany\\Component\\Example'));  
        $container->registerServiceProvider(new Joomla\CMS\Extension\Service\Provider\MVCFactory('\\Mycompany\\Component\\Example'));  
        $container->set(  
            ComponentInterface::class,  
            function (Container $container) {  
                $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class));  
                $component->setMVCFactory($container->get(MVCFactoryInterface::class));  
                return $component;  
            }  
        );  
    }  
};
 

در اینجا چند خط اضافی در فایل services/provider.php اضافه شده که مربوط به کلاس‌هایی است که نمونه کلاس افزونه com_example به آن‌ها نیاز خواهد داشت:

- یک ComponentDispatcherFactory لازم است تا نمونه‌ای از کلاس Dispatcher بسازد و سپس جوملا متد dispatch() را روی این شیء Dispatcher فراخوانی کند، که این مرحله بعدی اجرای کامپوننت است.

- یک MVCFactory لازم است تا نمونه‌های کلاس Controller، View، Model و Table را به نمایندگی کامپوننت بسازد.

دو خط `registerServiceProvider` باعث می‌شود این دو وابستگی در دایره وابستگی فرزند بارگذاری شوند.

بعد هنگام نمونه‌سازی کامپوننت از دایره وابستگی فرزند، اشیاء این دو Factory گرفته می‌شود.

هشدار کلاس‌های تکراری!

دقت کنید که نام کلاس‌هایی در این مثال وجود دارد که قسمت انتهایی Fully Qualified Name (FQN) آن‌ها مشابه است:

 

- ComponentDispatcherFactory:

  - می‌تواند اشاره کند به `\Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory` در مسیر libraries/src/Service/Provider/ComponentDispatcherFactory.php

  - و می‌تواند اشاره کند به `\Joomla\CMS\Dispatcher\ComponentDispatcherFactory` در مسیر libraries/src/Dispatcher/ComponentDispatcherFactory.php

 

- MVCFactory:

  - می‌تواند اشاره کند به `\Joomla\CMS\Extension\Service\Provider\MVCFactory` در مسیر libraries/src/Service/Provider/MVCFactory.php

  - و می‌تواند اشاره کند به `\Joomla\CMS\MVC\Factory\MVCFactory` در مسیر libraries/src/MVC/Factory/MVCFactory.php

هر بار مورد دوم، کلاس اصلی «Factory» است، و مورد اول کلاس Service Provider است که امکان نمونه‌سازی کلاس Factory اصلی از طریق DIC را فراهم می‌کند. برای کاهش سردرگمی، در متن از FQN‌های مربوط به Service Provider ها استفاده شده است.

حل وابستگی ComponentDispatcherFactory

در مرحله دوم، وقتی جوملا متد `get(ComponentInterface::class)` را فراخوانی می‌کند، کلاس Extension (که در این جا MVCComponent است) نمونه‌سازی می‌شود و وابستگی‌هایش برطرف می‌گردند.

ابتدا به ComponentDispatcherFactory توجه کنیم:

$component = new MVCComponent(
$container->get(ComponentDispatcherFactoryInterface::class)
);

 

عبارت `$container->get(ComponentDispatcherFactoryInterface::class)` در دایره وابستگی فرزند به دنبال کلیدی به نام ComponentDispatcherFactoryInterface::class می‌گردد و تابع ثبت شده برای آن را اجرا می‌کند که در مسیر زیر تعریف شده است.

libraries/src/Extension/Service/Provider/ComponentDispatcherFactory.php

خروجی آن عبارت است از:

new \Joomla\CMS\Dispatcher\ComponentDispatcherFactory($this->namespace, $container->get(MVCFactoryInterface::class))

این شیء سپس به سازنده کلاس MVCComponent ارسال می‌شود. کلاس MVCComponent از کلاس Component ارث‌بری می‌کند و سازنده کلاس Component یک مرجع به این ComponentDispatcherFactory ذخیره می‌کند:

 
public function __construct(ComponentDispatcherFactoryInterface $dispatcherFactory)  
{  
    $this->dispatcherFactory = $dispatcherFactory;  
}
 

همچنین تابع `getDispatcher` که آن را بازیابی می‌کند و برای نمونه‌سازی Dispatcher استفاده می‌شود:

 
public function getDispatcher(CMSApplicationInterface $application): DispatcherInterface  
{  
    return $this->dispatcherFactory->createDispatcher($application);  
}
 

 خطی که ComponentDispatcherFactory ساخته می‌شود را دوباره ببینیم:

new \Joomla\CMS\Dispatcher\ComponentDispatcherFactory($this->namespace, $container->get(MVCFactoryInterface::class))

این خط همچنین یک نمونه MVCFactory از دایره وابستگی فرزند می‌گیرد، زیرا ComponentDispatcherFactory خود به MVCFactory وابسته است. دلیل این موضوع آن است که طبق مستندات Dispatcher، هر زمان که تابع dispatch فراخوانی شود، مقدار task در آدرس URL تحلیل شده تا مشخص شود کدام Controller ساخته شود. پس شیء MVCFactory که در ساختار ComponentDispatcherFactory ذخیره شده است، برای نمونه‌سازی Controller استفاده می‌شود:

 
$controller = $this->mvcFactory->createController(...);  
$controller->execute($task);
 

حل وابستگی MVCFactory

این وابستگی مشابه وابستگی ComponentDispatcherFactory ثبت و حل می‌شود.

به وسیله خط:

 
$container->registerServiceProvider(new Joomla\CMS\Extension\Service\Provider\MVCFactory('\\Mycompany\\Component\\Example'));

 

کلاس سرویس پرووایدر MVCFactory مقداردهی شده و متد register آن اجرا می‌شود. در این فایل می‌بینیم که MVCFactory وابستگی‌های متعدد دیگری دارد که همه آن‌ها از دایره وابستگی والد دریافت می‌شوند.

سپس:

 
$component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class));  
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
 

 خط اول، کلاس Extension (MVCComponent) را نمونه‌سازی می‌کند و شیء ComponentDispatcherFactory به سازنده آن می‌رود.

در خط دوم، نمونه MVCFactory از دایره وابستگی فرزند گرفته شده و از طریق متد `setMVCFactory` به شیء کامپوننت اختصاص داده می‌شود. این متد از Trait ای به نام MVCFactoryServiceTrait می‌آید که در کلاس MVCComponent استفاده شده است.

پارامتر فضای نام (Namespace)

شما متوجه شده‌اید که رشته فضای نام '\\Mycompany\\Component\\Example' به‌عنوان پارامتر به چندین کلاس داده می‌شود. دلیل این است که این اشیاء Factory مسئول ساخت نمونه‌های مختلف کلاس‌ها به نمایندگی از کامپوننت هستند:

- ComponentDispatcherFactory وظیفه نمونه‌سازی Dispatcher را دارد.

- MVCFactory وظیفه ساخت Controller، View، Model و Table را دارد.

مثلاً تابع `createDispatcher` در ComponentDispatcherFactory بررسی می‌کند که آیا برای کامپوننت com_example کلاس Dispatcher خاصی وجود دارد یا خیر:

 
$className = '\\' . trim($this->namespace, '\\') . '\\' . $name . '\\Dispatcher\\Dispatcher';

 

که `$name` می‌تواند "Site" یا "Administrator" باشد. اگر چنین کلاس خاصی وجود نداشته باشد، کلاس پیش‌فرض `Joomla\CMS\Dispatcher\ComponentDispatcher` ساخته می‌شود.

همین‌طور MVCFactory از فضای نام برای پیدا کردن نام کامل کلاس‌های Controller، View و سایر کلاس‌ها استفاده می‌کند و آن‌ها را نمونه‌سازی می‌کند.

ضرورت دایره وابستگی فرزند (Child DIC)

در طول پاسخ به یک درخواست HTTP، جوملا چندین افزونه را نمونه‌سازی می‌کند که هرکدام وابستگی‌های خود را دارند. با توجه به اینکه جوملا از کلیدهای مشترک (مثل `ComponentInterface::class` برای کامپوننت‌ها) استفاده می‌کند، لازم است دایره وابستگی فرزند داشته باشیم تا موارد مربوط به هر افزونه به صورت جداگانه مدیریت شود و با هم تداخل نکنند.

ماژول‌ها و پلاگین‌ها

فایل‌های سرویس پرووایدر برای ماژول‌ها و پلاگین‌ها به مراتب ساده‌تر از فایل‌های مربوط به کامپوننت‌ها هستند. اگر پوشه‌های ماژول‌ها و پلاگین‌های جوملا را در نمونه نصب خود نگاه کنید، چند نمونه از فایل‌های services/provider.php را خواهید یافت.

ماژول‌ها

برای مثال، در Joomla 5 برای mod_breadcrumbs داریم:

 
use Joomla\CMS\Extension\Service\Provider\Module;  

public function register(Container $container): void  
{  
    $container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Breadcrumbs'));  
    $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Breadcrumbs\\Site\\Helper'));  

    $container->registerServiceProvider(new Module());  
}
 

 این ماژول از کلاس استاندارد `Joomla\CMS\Extension\Service\Provider\Module` استفاده می‌کند تا کلاس افزونه خود را که `\Joomla\CMS\Extension\Module` است، بسازد.

این ماژول دو وابستگی دارد:

- از کلاس `ModuleDispatcherFactory` برای نمونه‌سازی Dispatcher اختصاصی خود که در `src/Dispatcher/Dispatcher.php` قرار دارد، استفاده می‌کند.

- از کلاس `HelperFactory` برای یافتن فایل Helper خود که در `src/Helper/BreadcrumbsHelper.php` قرار دارد، استفاده می‌کند.

پلاگین‌ها

برای مثال، پلاگین فیلد سفارشی رنگ در مسیر `plugins/fields/color` به شکل زیر است:

 
use Joomla\Plugin\Fields\Color\Extension\Color;  

public function register(Container $container)  
{  
    $container->set(  
        PluginInterface::class,  
        function (Container $container) {  
            $plugin     = new Color(  
                $container->get(DispatcherInterface::class),  
                (array) PluginHelper::getPlugin('fields', 'color')  
            );  
            $plugin->setApplication(Factory::getApplication());  

            return $plugin;  
        }  
    );  
}
 

کلاس Extension این پلاگین، کلاس `Color` است که با دو پارامتر به سازنده خود مقدار می‌گیرد:

- کلاس `EventDispatcher` که از دایره وابستگی والد (parent DIC) گرفته می‌شود و هنگام شروع جوملا ابتدا به داخل آن افزوده شده بود (در فایل `libraries/src/Service/Provider/Dispatcher.php`)

- شیء استاندارد `stdClass` پلاگین که جوملا به طور رایج برای نگهداری داده‌های پلاگین (مثل شناسه، نام، نوع و پارامترها) استفاده می‌کند.

این الگو یک روش استاندارد است که شما می‌توانید برای پلاگین‌های خود استفاده کنید. برای دسترسی به نمونه اپلیکیشن می‌توانید در فایل services/provider.php یا در کد معمولی پلاگین خود، از فراخوانی `Factory::getApplication` استفاده کنید.

مسائل مربوط به تزریق وابستگی

تزریق وابستگی (Dependency Injection) در جوملا 4 معرفی شد، با هدف حذف دسترسی مستقیم به بیشتر کلاس‌های کلیدی از طریق فراخوانی‌های `getInstance()`، از جمله متدهای `Factory::`، که بسیاری از این فراخوانی‌ها در جوملا 4 منسوخ (deprecated) اعلام شده‌اند. البته همه‌ی متدهای `getInstance()` منسوخ نشده‌اند؛ برای مثال `Uri::getInstance()` و `Filter\InputFilter::getInstance()` هنوز معتبر باقی مانده‌اند.

با این حال مشکلاتی وجود دارد که باید از آن‌ها آگاه باشید. نسخه‌های اولیه جوملا 5 هنوز تعداد زیادی از این فراخوانی‌های `getInstance()` را دارند!

Toolbar::getInstance()

کلاس Toolbar برای مدیریت دکمه‌ها در فرم‌های بخش مدیریت جوملا استفاده می‌شود. در مستندات API این کلاس پیشنهاد شده است که به جای:

$bar = Toolbar::getInstance('toolbar');

 

از این شکل استفاده کنیم:

$bar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar');

 

ولی متأسفانه در حال حاضر این روش کار نمی‌کند. مشکل این است که تقریبا همه کدهای دیگر جوملا برای دسترسی به کلاس Toolbar همچنان از `getInstance('toolbar')` استفاده می‌کنند، از جمله کد مربوط به ماژول `administrator/modules/mod_toolbar/mod_toolbar.php` که تولبار را رندر می‌کند. متد `getInstance` نمونه‌های Toolbar را در متغیر استاتیک محلی `$instances` نگهداری می‌کند و در فراخوانی‌های بعدی همان نمونه را برمی‌گرداند.

بنابراین اگر از روش دوم استفاده کنید، نمونه Toolbar ساخته می‌شود ولی این نمونه همان نمونه‌ای نیست که در `$instances` ذخیره شده است. در نتیجه ماژول Toolbar قادر به پیدا کردن آن نیست و تولبار روی فرم نمایش داده نخواهد شد.

Table::getInstance()

معمولا نمونه‌های Table از طریق کلاس MVCFactory و متد `getTable()` در مدل شما ساخته می‌شوند. اما در مواردی ممکن است شما بخواهید نمونه دیگری داشته باشید، مثل:

- در کد کلاس Table خود، برای بررسی اینکه آیا یک مقدار alias وارد شده قبلاً وجود دارد یا نه.

- یا بخواهید به کلاس Table دیگری در کامپوننت متفاوت (مثلاً com_categories) دسترسی پیدا کنید.

هیچ‌کدام از این دو توسط MVCFactory کامپوننت شما ممکن نیست، چون:

- MVCFactory فقط برای کامپوننت شما کلاس‌های Table می‌سازد.

- و در کد Table شما معمولاً اشاره‌ای به نمونه MVCFactory ندارید.

اما می‌توانید به صورت پیچیده‌تری این نمونه‌ها را به دست آورید، مثلاً برای کامپوننت com_content:

Factory::getApplication()->bootComponent('content')->getMVCFactory()->createTable($name, $prefix, $config);

 

شما همچنین می‌توانید با این روش کامپوننت خودتان را بوت کرده و نمونه دیگری از Table خودتان بسازید.

Categories::getInstance()

اگر در کامپوننت خود از سیستم Categories استفاده می‌کنید، ممکن است بخواهید از API مربوط به Categories در مواردی مثل روتینگ سفارشی یا مدل‌های Category در سایت استفاده کنید. روش قدیمی فراخوانی

 
$categories = Categories::getInstance(...);

 

منسوخ شده است.

شما می‌توانید یک `CategoryFactory` از دایره وابستگی فرزند (child DIC) هنگام نمونه‌سازی کلاس Extension دریافت کنید، اما متأسفانه این شئ به صورت پیش‌فرض از طریق کلاس MVCFactory به مدل شما منتقل نمی‌شود. یک راه حل این است که ارجاع `CategoryFactory` را به عنوان یک متغیر استاتیک در کلاس Extension خود ذخیره کنید، همانطور که در مستندات «پیاده‌سازی Categories در کامپوننت خود» توضیح داده شده است.