مسیریابی (Routing)
- محمد علایی
- منتشر شده در
- زمان خواندن 5 دقیقه
مقدمه
در زمینه یک برنامه وب، مسیریابی به معنای تجزیه و تحلیل یک URL ورودی است تا مشخص شود کدام بخشهای برنامه لازم است برای رسیدگی به درخواست فعال شوند. گاهی این کار نسبتاً ساده است، اما در یک سایت جوملا، پشتیبانی از URLهای سازگار با موتورهای جستجو (SEF) این کار را بهمراتب پیچیدهتر میکند. مسئولیت مسیریاب سایت جوملا (Joomla Site Router) تبدیل بین URLهای SEF و فرمت URL داخلی (که بخشهای مختلف برنامه را شناسایی میکند) است.
وقتی کاربران از صفحات سایت شما بازدید میکنند، شما میخواهید URLهایی داشته باشید که به طور طبیعی نشاندهنده محتوای نمایش داده شده باشند، مانند:
mysite.org/articles/latest-news
اما از نظر داخلی، جوملا از فرمت URI استفاده میکند که منابع درگیر را مشخص میکند، مثلاً:
mysite.org?option=com_content&view=article&id=5
وظیفه مسیریاب جوملا تبدیل بین این دو فرمت است.
- عملکرد تجزیه مسیریاب (router parse) URLهای خارجی (SEF) را به URIهای داخلی تبدیل میکند.
- عملکرد ایجاد مسیریاب (router build) URIهای داخلی را به URLهای SEF تبدیل میکند.
این بخش از مستندات به مسیریاب سایت جوملا میپردازد و توضیح میدهد چگونه کار میکند و چگونه میتوانید آن را در برنامههای خود استفاده کنید.
اگر در حال توسعه یک کامپوننت جوملا هستید و میخواهید از URLهای SEF استفاده کنید، یا قصد دارید قوانین مسیریابی کامپوننتهای هسته جوملا را تغییر دهید، لازم است درک خوبی از عملکرد مسیریاب سایت داشته باشید.
همچنین ممکن است مشاهده دو ویدئو زیر که به تجزیه یک URL و ساخت یک URL میپردازند برای شما مفید باشد.
parsing a URL و building a URL.
تجزیه یک URL سازگار با موتور جستجو (Parsing an SEF URL)
فرض کنیم URL زیر را داریم:
http://localhost/joom/index.php/en/mountain/21-mont-blanc
وقتی این URL تجزیه شود، انتظار داریم که یک آرایه از پارامترهای کوئری (مانند option، view، layout، lang و غیره) حاصل شود که جوملا با استفاده از آنها میداند کدام کامپوننت را فراخوانی کند (پارامتر option)، و بقیه پارامترها جزئیات لازم برای ساخت خروجی HTML (یا json یا xml و غیره) را به کامپوننت میدهند.
مراحل تجزیه
- `http://localhost/joom` مسیر پایه (base) جوملا است و از مسیر نهایی حذف میشود، چون برای همه URLها مشترک است و در فرآیند مسیریابی نقشی ندارد.
- `index.php` نقطه ورود برنامه است (ممکن است بسته به تنظیمات سایت وجود نداشته باشد) و آن هم از مسیر حذف میشود.
همانطور که مشخص است ۳ بخش باقی مانده داریم:
/en/mountain/21-mont-blanc
جوملا به صورت داخلی دو آرایه برای ذخیره این بخشها و پارامترهای کوئری استفاده میکند:
$segments = array('en', 'mountain', '21-mont-blanc'); $queryVars = array();
فیلتر زبان (Language Filter Plugin)
این افزونه هنگام اجرا، یک «قانون» (rule) به مسیریاب اضافه میکند که میگوید وقتی مسیریاب شروع به پردازش بخشها (segments) میکند، تابع `parseRule()` من فراخوانی شود.
تابع `parseRule()` کد زبان `'en'` را شناسایی میکند (که در سایت ها معمولا معادل `'en-GB'` است)، آن را از آرایه `$segments` حذف کرده و در پارامترهای کوئری اضافه میکند:
$segments = array('mountain', '21-mont-blanc');
$queryVars = array('lang' => 'en-GB');
جستجو در منوها
1. مسیریاب سپس اولین عضو آرایه `$segments` (یعنی `'mountain'`) را با مقدار Alias (نام مستعار) عناصر منوی سطح اول در منوهای سایت مطابقت میدهد.
2. اگر چیزی پیدا نکرد، خطای ۴۰۴ (صفحه یافت نشد) صادر میکند.
3. اگر یافت، بررسی میکند که آیا آن منو شامل زیرمنو (submenu) هست یا خیر؛ سپس تلاش میکند بخش بعدی یعنی (`21-mont-blanc`) را با زیرمنوها تطبیق دهد و همین طور ادامه میدهد تا پایینترین منو (یا زیرمنو) پیدا شود.
فرض کنیم یک منو با `alias 'mountain'` پیدا شده ولی زیرمنویی ندارد و id این منو ۶ است. حالا `'mountain'` از `$segments` حذف میشود و شناسه منو به `$queryVars` افزوده میشود:
$segments = array('21-mont-blanc');
$queryVars = array('lang' => 'en-GB', 'Itemid' => '6');
اضافه کردن سایر پارامترهای منو
در بخش مدیریت اگر به فیلد Link آیتمهای منو نگاه کنیم، میبینیم که معمولاً مواردی مثل این وجود دارد:
index.php?option=com_content&view=article&id=10
پس مقادیر دیگری هم به `$queryVars` اضافه میشود:
$queryVars = array('lang' => 'en-GB',
'Itemid' => '6',
'option' => 'com_content',
'view' => 'article',
'id' => '10'
);
اهمیت شناسایی آیتم منو (Menuitem)
به عنوان نکتهای جانبی، شناسایی آیتم منو (menuitem) به دلایل متعددی اهمیت دارد. این شناسایی نه تنها کامپوننت مورد استفاده را معین میکند (مثلاً `option=com_content`)، بلکه بسیاری از ویژگیهای قالببندی صفحه وب را نیز تعیین میکند، از جمله ماژولهایی که روی آن صفحه نمایش داده میشوند و قالب (template) که روی آن صفحه اعمال میشود. البته این موضوع در صورتی صدق میکند که پارامتری مانند `?template=anothertemplate` در URL قرار نگرفته باشد.
وقتی این آیتم منو پیدا شد، مسیریاب (router) آن را با استفاده از تابع `setActive()` به عنوان آیتم منوی «فعال» تعیین میکند. این باعث میشود که دیگر بخشها و کدهای برنامه بتوانند به راحتی با استفاده از متد `getActive()` (همانطور که در بخشهای «Menus and Menuitems» توضیح داده شده) به این آیتم منو دسترسی پیدا کنند.
در این مرحله، مسیریاب سایت (Site Router) تا همین جا پیش رفته است. اکنون تصمیم دارد بقیه بخشهای (segments) باقیمانده را به مسیریاب کامپوننت (component router) بسپارد تا تجزیه شوند. وقتی شما یک کامپوننت مینویسید که از URLهای SEF استفاده میکند، تقریباً همیشه باید یک مسیریاب کامپوننت فراهم کنید که شامل پیادهسازی تابع `parse()` باشد:
public function parse(&$segments)
مسیریاب بخشها را به صورت ارجاعی (by reference) به تابع parse میفرستد و انتظار دارد که این تابع بخشهای ارجاع شده را تحلیل کند تا متغیرهای کوئری را شناسایی کند، سپس:
- بخشهایی که استفاده شدهاند را از آرایهی `$segments` حذف کند (اگر پس از این مرحله هنوز بخشهایی باقی مانده باشد، مسیریاب باعث تولید خطای HTTP 404 «صفحه پیدا نشد» میشود)، و
- آرایه متغیرهای کوئری شناسایی شده را بازگرداند.
مثال
فرض کنیم سایت ما کوهها را نمایش میدهد و آیتم منویی با `Itemid=6` به مقالهای درباره اورست (با شناسه مقاله `id=10`) لینک شده است، اما مقاله مان شناسهای برابر با ۲۱ دارد. در این حالت، تابع parse با آرایهای مانند زیر فراخوانی میشود:
$segments = array('21-mont-blanc');
کد parse باید چیزی شبیه به این داشته باشد:
unset($segments[0]);
$vars = array('view' => 'article', 'id' => '21-mont-blanc');
return $vars;
مشکلی ندارد که شناسه را به شکل `21-mont-blanc ` برگردانیم، چرا که مقدار ارسالی از طریق فیلتر عدد صحیح (int filter) عبور میکند که هر چیزی بعد از `21` را نادیده میگیرد. همچنین بهتر است صریحاً مقدار `'view'` (و دیگر متغیرها مثل `'layout'`) را برگردانیم و به ادغام این مقادیر توسط مسیریاب از آیتم منو اعتماد نکنیم.
در پایان
بعد از این مرحله، تجزیه URL کامل شده است و در نهایت به شکل زیر خواهیم داشت:
$segments = array();
$queryVars = array(
'lang' => 'en-GB',
'Itemid' => '6',
'option' => 'com_content',
'view' => 'article',
'id' => '21'
);
جوملا اکنون همه اطلاعات لازم برای پاسخ به درخواست را دارد:
- فایلهای زبان INI مورد استفاده بر اساس `'en-GB'` انتخاب میشوند.
- از طریق آیتم منو `Itemid` مشخصات قالببندی صفحه مانند قالب(تمپلیت) و ماژولهای مورد استفاده بر صفحه تعیین میشوند.
- کامپوننت مورد استفاده `com_content` است و همچنین نما (`view`) و شناسه مقاله برای نمایش مشخص است.
ساخت یک URL سازگار با موتور جستجو (Building an SEF URL)
فرآیند ساخت یک URL SEF معمولاً با فراخوانی مانند زیر آغاز میشود:
use Joomla\CMS\Router\Route;
…
$url = Route::_('index.php?option=com_content&view=article&id=21&catid=50&lang=en-GB');
در اینجا پارامترهای کوئری به صورت رشته در تابع Route::_() ارائه میشوند و وظیفه مسیریاب (router) است که URL سازگار با موتور جستجو را تولید کند، که ممکن است چیزی شبیه به این شود:
http://localhost/joom/index.php/en/mountains/50-alps/21-mont-blanc
مراحل اصلی ساخت URL
دو مرحله اصلی برای تولید این URL وجود دارد:
1. مرحله پیشپردازش (Preprocess): تعیین آیتم منو (menuitem) که URL بر اساس آن ساخته شود.
2. مرحله ساخت (Build): ساخت آرایه بخشها (segments) برای URL.
و در نهایت یک مرحله مرتبسازی و ساماندهی (tidy-up).
مرحله پیشپردازش (Preprocess Stage)
مرحله پیشپردازش بر عهده مسیریاب کامپوننت است. مسیریاب سایت جوملا با استفاده از پارامتر `option` در فراخوانی `Route::_() `، کامپوننت مربوطه را شناسایی میکند (مثلاً در اینجا `com_content`) و سپس متد `preprocess` مسیریاب کامپوننت را فراخوانی میکند:
public function preprocess($query)
پارامترهای کوئری به صورت آرایه انجمنی (associative array) به این تابع داده میشوند، مثلاً:
$query = array('view' => 'article', 'id' => '21', ...);
وظیفه این تابع این است که آیتم منوی مناسب را شناسایی کرده و در آرایه `$query` قرار دهد، مثلاً:
$query['Itemid'] = 13; return $query;
کامپوننتهای اصلی جوملا معمولاً مستقیماً متد `preprocess()` را نداشته اما با استفاده از پیکربندیهای `RouterView` این کار را انجام میدهند؛ این موضوع در بخش بعدی مستندات توضیح داده شده است. در این موارد، مسیریاب سایت خود وظیفه `preprocess()` را انجام میدهد.
مسیریاب سایت آیتمهای منو مرتبط با کامپوننت را مییابد (شامل آیتمهای مخفی هم می باشد ولی آیتمهای منتشرنشده را شامل نمی شود) و یکی را برای ساخت URL SEF انتخاب میکند.
چگونگی انتخاب آیتم منو
منطق انتخاب آیتم منو در نسخههای مختلف جوملا تغییراتی داشته است، ولی به طور کلی بر اساس قواعد `RouterView` است که اولویت دارند.
توجه کنید میتوانید آیتم منو (Itemid) را به صورت مستقیم در فراخوانی Route مشخص کنید:
$url = Route::_('index.php?option=com_content&view=article&id=21&Itemid=6');
اما مسیریاب این مقدار را ممکن است نادیده گرفته و آیتم منو دیگری را برای ساخت URL انتخاب کند.
چنانچه از عملکرد پیشفرض رضایت نداشتید، میتوانید با نوشتن پلاگین، منطق انتخاب آیتم منو را به دلخواه خود تغییر دهید (مطابق با روش سیستم پلاگینهای قواعد مسیریابی).
ساخت بخش ابتدایی URL
آیتم منوی انتخابشده بخش ابتدایی URL SEF را بر اساس فیلد alias آیتم منو فراهم میکند. اگر آیتم منو زیرمنو باشد، alias های آیتمهای والد نیز به شکل مسیر (route) به ابتدای بخشها اضافه میشوند.
فرض کنیم آیتم منوی انتخابی یک منو سطح اول است که لیستی از دستهبندی مقالات را نمایش میدهد و alias آن `'mountains'` است. در این حالت آرایه بخشها به شکل زیر خواهد بود:
$segments = array('mountains');
اگر هیچ آیتم منویی در سایت وجود نداشته باشد که به گزینه (option) مشخصشده در فراخوانی `Route::_() ` مرتبط باشد، چه اتفاقی میافتد؟
در این صورت، مسیریاب URL سازگار با موتور جستجو (SEF URL) را بر اساس آیتم منوی صفحه اصلی (home menuitem) سایت ایجاد میکند — که در سایتهای چندزبانه بر اساس زبان مربوطه انتخاب میشود. سپس بخشهایی را به URL اضافه میکند که نشاندهندهی کامپوننت و پارامترهای مرتبط میباشد.
برای مثال، اگر کامپوننت `com_contact` باشد، ممکن است بخشهای URL به شکل زیر باشد:
/en/component/contact/contact/me?Itemid=1
چرا این وضعیت بد است؟
این وضعیت نشانه یک مشکل است و باید از آن به هر قیمتی اجتناب شود! زیرا لینک نهایی (اگر اصلاً کار کند) به جای استفاده از قالببندی و ماژولهای مخصوص آن کامپوننت، قالببندی صفحه اصلی و ماژولهای صفحه اصلی را به کار میبرد که معمولاً نتیجهی مطلوبی نیست.
چگونه میتوان از این مشکل جلوگیری کرد؟
این وضعیت به آسانی با تعریف یک آیتم منوی پنهان (hidden menuitem) که به آن کامپوننت مرتبط باشد، قابل رفع است. آیتم منوی پنهان، در منوهای قابل مشاهدهی سایت نشان داده نمیشود ولی مسیریاب آن را میشناسد و URLها را به درستی میسازد.
اما توسعهدهندگان افزونه همیشه کنترل کاملی روی ساختار منوهای مدیران سایت ندارند، بنابراین نمیتوان این مشکل را به طور کامل به آنها واگذار کرد.
مرحله ساخت (Build Stage)
مرحله ساخت عمدتاً بر عهده مسیریاب کامپوننت (component router) است و مسیریاب سایت (Site Router) تلاش میکند متد `build` مسیریاب کامپوننت را فراخوانی کند:
public function build(&$query)
پارامترهای کوئری به صورت آرایهی انجمنی (associative array) به این تابع داده میشوند و در این مرحله باید مقدار `Itemid` منو نیز مشخص شده باشد، مثلا:
$query = array('Itemid' => '13', 'view' => 'article', 'id' => '21', 'catid' => '50', ...);
وظیفه متد `build` در مسیریاب کامپوننت این است که بر اساس پارامترهای کوئری دریافتی، آرایهای از بخشها (segments) برای URL سازگار با موتور جستجو تولید کند. این متد باید آرایه بخشها را بازگرداند و هر پارامتر کوئری که در ساخت بخشها استفاده شده را از آرایه پارامترها حذف کند، مانند:
$segments = array('50-alps', '21-mont-blanc');
unset($query['view']);
unset($query['id']);
unset($query['catid']);
return $segments;
اگر پارامترهای کوئری به درستی حذف (unset) نشوند، آنها به صورت کوئری در انتهای آدرس URL اضافه میشوند، مثلاً:
http://localhost/joom/index.php/en/mountains/50-alps/21-mont-blanc?view=article&id=21
آزادی عمل در ساخت بخشها
مسیریاب کامپوننت آزادی دارد که تصمیم بگیرد چه بخشهایی (segments) را در URL قرار دهد، به شرطی که بتواند این بخشها را هنگام کلیک کاربر تجزیه و پردازش کند.
به طور سنتی جوملا بخشهایی به صورت `id:alias` آیتم تعریف میکند، ولی میتوان با فعال کردن گزینه تنظیمات کلی `Global Configuration: Articles / Integration / Routing Remove IDs from URLs` (برای `com_content` و مشابه آن برای Contactها)، بخششناسه (id) را حذف کرد و فقط alias را گذاشت.
مزیت داشتن `id:alias` این است که هنگام تجزیه URL در درخواست ورودی، نیازی به جستجوی پایگاه داده نیست چون شناسه عددی موجود است. اگر فقط از alias استفاده کنید، مطمئن شوید که فیلد alias شما در دیتابیس ایندکس شده باشد.
نکته درباره آیتمها و دستهبندیها در URL
- اگر آیتم منو به یک آیتم تکی اشاره داشته باشد (مثلاً یک مقاله یا یک تماس تکی)، میتواند فقط یک بخش در URL باشد که `id:alias` یا فقط `alias` آن آیتم باشد.
- اگر آیتم منو به فهرست دستهای از آیتمها اشاره کند، باید بخشهای دستهبندی را هم به صورت `id:alias` قرار دهد. معمولاً بخشهای دستهبندی والد تا ریشه نیز اضافه میشوند.
این مسیر دستهبندیها را میتوان با استفاده از متد `getPath()` کلاس `CategoryNode` به آسانی بدست آورد (این متد در بخش «متدهای دریافت گره مجموعه » توضیح داده شده است).
نکته مهم: اگر شناسهها در بخشها حذف شده باشند، باید توجه داشت که alias دستهها نباید در سطح مشابه در ساختار دستهبندی تکراری باشند، چون uniqueness در سطح کل سایت لازم نیست بلکه فقط در هر سطح (سطح دستههای والد مشترک) الزامآور است. بنابراین مسیر کامل دستهبندی لازم است.
مثال مشخص
فرض کنیم آیتم منو به یک فهرست دستهبندی اشاره دارد و بخشها به شرح زیر تولید شوند:
- بخش اول: ` 50-alps` که id:alias دسته بالایی است (که فرض میکنیم در سطح اول درخت دستهبندی است)،
- بخش دوم: ` 21-mont-blanc` که id:alias آیتم مورد نظر است.
اگر بخش `mountains` که در مرحله پیشپردازش بدست آمد را هم اضافه کنیم، سه بخش نهایی URL به شکل زیر خواهد بود:
/mountains/50-alps/21-mont-blanc
بخش پایانی URL
تا این مرحله، تمامی مراحل دشوار تولید URL سازگار با موتور جستجو (SEF URL) انجام شده است.
کار باقیمانده فقط اضافه کردن بخش زبان به URL است (که در سایتهای چندزبانه توسط افزونه Language Filter انجام میشود) و همچنین دامنه سایت و در صورت نیاز نقطه ورود که معمولاً فایل index.php می باشد، اضافه می شود.
RouterView
نوشتن توابع `preprocess`، `parse` و `build` برای روتر کامپوننت میتواند کار پیچیده و زمانبری باشد. اگر کامپوننت شما به شکل تقریبی مشابه `com_content` است که آیتمهای آن در گروههای دستهبندیشده قرار دارند، استفاده از پیکربندیهای RouterView میتواند بسیار کار شما را ساده کند.
اما اگر حس کنید این روش به درستی کار نمیکند، به دلیل دشواری دیباگ کردن بهتر است از نوشتن مستقیم توابع `preprocess`، `parse` و `build` استفاده کنید.
توصیه شخصی
پیشنهاد میشود اول روش RouterView را امتحان کنید و ببینید تا چه حد برای کامپوننتتان قابل استفاده است. احتمالاً کمی نیاز به آزمون و خطا دارد. اگر بعد از چند روز نتوانستید آن را به خوبی پیاده کنید، بهتر است به روش کلاسیک و معمول یعنی نوشتن توابع استاندارد بازگردید.
نحوۀ تعریف پیکربندی RouterView
سادهترین روش برای ساخت پیکربندی RouterView دیدن یک مثال عملی است، مثلاً کامپوننت com_content در مسیر:
components/com_content/src/Service/Router.php
برای هر ویوی کامپوننت شما — برای com_content اینها پوشههای درون `components/com_content/tmpl` هستند — باید یک نمونه از `RouterViewConfiguration` بسازید، مثلا:
$this->registerView(new RouterViewConfiguration('featured'));
اگر ویو شما پارامتر کوئری پیچیدهای ندارد، همین یک خط کافی است.
مثال پیشرفتهتر
برای ویوی مقاله (article) در com_content (شامل مسیر `components/com_content/tmpl/article`):
$article = new RouterViewConfiguration('article');
$article->setKey('id')->setParent($category, 'catid');
$this->registerView($article);
- ویوی `article` برای مقالات مختلف قابل استفاده است، کلید اصلی (key) آن پارامتر کوئری `id` است.
- ویو همچنین به یک دستهبندی مرتبط است که با `catid` شناسایی میشود.
- مسیر درختی دستهبندی با استفاده از `setParent` و قابلیت `setNestable` باعث میشود که چند بخش دستهبندی (category segments) که مسیر دستهها را تشکیل میدهند به URL اضافه شوند.
- علاوه بر آن، ویوی `category` میتواند چند Layout داشته باشد مانند `blog` که برای نوعهایی از آیتمهای منو است (مثلاً منوهای با نوع بلاگ دستهبندی).
این پیکربندی باعث میشود روتر بفهمد چه پارامترهایی انتظار میروند و هر پارامتر چه معنایی دارد.
قواعد (Rules)
در خطوط بعدی کد، قواعدی که قرار است در روتر استفاده شوند ثبت میشوند:
$this->attachRule(new MenuRules($this));
$this->attachRule(new StandardRules($this));
$this->attachRule(new NomenuRules($this));
- MenuRules: مربوط به کد پیشپردازش (`preprocess`) است که برای انتخاب آیتم منو هنگام ساخت URL SEF به کار میرود.
- StandardRules: شامل توابع `build()` و `parse()` برای زمانی است که آیتم منو پیدا شده است.
- NomenuRules: شامل توابع `build()` و `parse()` زمانی که آیتم منو پیدا نشده است — همان شرایط خاصی که URLها شبیه این میشوند:
/en/component/contact/contact/me?Itemid=1
جدول ارتباط قواعد با توابع روتر:
|
نوع قاعده |
preprocess |
build |
parse |
|
MenuRules |
✓ |
|
|
|
StandardRules |
|
✓ |
✓ |
|
NomenuRules |
|
✓ |
✓ |
انعطافپذیری
یک مزیت بزرگ ساختار جوملا این است که اگر از رفتار پیشفرض یکی از این قواعد راضی نبودید، میتوانید کد خود را نوشته و جایگزین نسخه جوملایی کنید (مثلاً برای کامپوننت com_content)، مانند کاری که توسط پلاگینهای سیستم انجام میشود.
بخشها و متغیرهای پرسوجو
جنبه سوم پیکربندیهای RouterView، روتر را قادر میسازد تا بین بخشهای URL SEF و شناسه مرتبط با پارامترهای پرسوجو تبدیل انجام دهد. بنابراین برای هر پیکربندی RouterView (یعنی برای هر نما) باید 2 تابع فراخوانی بنویسید:
get<Viewname>Segment - یک شناسه ارسال میکند و از شما میخواهد که بخش یا بخشهای مرتبط را برگردانید.
get<Viewname>Id - یک بخش ارسال میکند و از شما میخواهد که شناسه مرتبط را برگردانید.
علاوه بر این، یک پارامتر $query نیز به شما ارسال میشود که نشان دهنده وضعیت فعلی درخواست در حال ساخت یا تجزیه است.
دسترسی به کلاس Router کامپوننت
اگر با مفاهیم تزریق وابستگی (Dependency Injection) و کلاسهای اکستنشن در جوملا آشنا نیستید، شاید مفید باشد ابتدا بخش «کلاسهای اکستنشن و Dispatcher» و «تزریق وابستگی» را مطالعه کنید تا راحتتر این بخش را درک کنید. از جوملا ۴، روش ترجیحی برای دسترسی به کلاس router کامپوننت استفاده از کلاس RouterFactory است که به صورت تزریقی (injected) به کامپوننت داده میشود. این یعنی در فایل `services/provider.php` کامپوننت (مثلاً در مسیر `administrator/components/com_content/services/provider.php`) ما داریم:
<?php
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
...
public function register(Container $container)
{
...
$container->registerServiceProvider(new RouterFactory('\Joomla\Component\Content'));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component = new ContentComponent(,,,);
...
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
return $component;
}
);
}
?>
در اینجا، `RouterFactory` به کلاسی در مسیر و با فضای نام زیر اشاره دارد
مسیر: `libraries/src/Extension/Service/Provider/RouterFactory.php`
فضای نام: `Joomla\CMS\Extension\Service\Provider\RouterFactory`
$container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content'));
در واقع نمونهای از این کلاس ایجاد میکند و فضای نام مورد استفاده را به آن میدهد و تابع `register()` را روی آن نمونه فراخوانی میکند:
<?php
public function register(Container $container)
{
$container->set( RouterFactoryInterface::class,
function (Container $container){
$categoryFactory = null;
if ($container->has(CategoryFactoryInterface::class)) {
$categoryFactory = $container->get(CategoryFactoryInterface::class);
}
return new \Joomla\CMS\Component\Router\RouterFactory(
$this->namespace,
$categoryFactory,
$container->get(DatabaseInterface::class)
);
}
);
}
?>
این باعث میشود که یک ورودی جدید در کانتینر تزریق وابستگی (DIC) با کلید `RouterFactoryInterface::class` ثبت شود (این مقدار رشتهای است که نام کامل کلاس را نشان میدهد).
زمانی که کامپوننت `com_content` نمونهسازی میشود، جوملا آن را از DIC دریافت میکند و سپس خطوط زیر اجرا میشوند:
کلاس `ContentComponent` اکستنشن کامپوننت `com_content` است و فایل آن در مسیر زیر قرار دارد که نمونهسازی میشود.
function (Container $container)
{
$component = new ContentComponent(,,,));
...
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
return $component;
}
`administrator/components/com_content/src/Extension/ContentComponent.php`
اگر کد در آن فایل را نگاه کنید میبینید:
// خارج از تعریف کلاس
use Joomla\CMS\Component\Router\RouterServiceTrait;
class ContentComponent extends MVCComponent implements
...
// داخل تعریف کلاس
use RouterServiceTrait;
(عبارت `use` در PHP معانی مختلفی دارد، بسته به اینکه داخل یا خارج کلاس استفاده شود.) این `RouterServiceTrait` که در مسیر `libraries/src/Component/Router/RouterServiceTrait.php` قرار دارد، دو تابع کلیدی دارد که با این عبارت `use` به متدهای کلاس اکستنشن `com_content` تبدیل میشوند:
- تابع `setRouterFactory` که دقیقاً در همین خط فراخوانی شده است:
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
که این متد نمونه `RouterFactoryInterface::class` را از DIC میگیرد.
با توجه به کد داخل `RouterFactory.php` و `register()`، این نمونه در واقع یک نمونه واقعی از کلاس `\Joomla\CMS\Component\Router\RouterFactory` است که در اینجا درون کلاس اکستنشن `com_content` به صورت محلی در متغیر `$this->routerFactory` ذخیره میشود.
- تابع `createRouter` که هنگامی فراخوانی میشود که جوملا بخواهد router کامپوننت `com_content` را بگیرد. طبق کد داخل `RouterServiceTrait` این تابع نمونه `routerFactory` را گرفته و متد `createRouter` را روی آن فراخوانی میکند:
return $this->routerFactory->createRouter($application, $menu);
(در اینجا دقت کنید که چند متد `createRouter` در کلاسهای مختلف وجود دارد!)
در نهایت این باعث میشود متد `createRouter` کلاس اصلی `RouterFactory` اجرا شود که کد آن در مسیر `libraries/src/Component/Router/RouterFactory.php` قرار دارد:
public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface
{
$className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router';
if (!class_exists($className)) {
throw new \RuntimeException('No router available for this application.');
}
return new $className($application, $menu, $this->categoryFactory, $this->db);
}
این نام کلاس از ترکیب موارد زیر ساخته میشود:
- فضای نام ذخیرهشده (برای com_content این مقدار `\\Joomla\\Component\\Content` است که در فراخوانی `registerServiceProvider` داخل فایل `services/provider.php` کامپوننت com_content ارسال شده است)
- نتیجه فراخوانی `$application->getName()` که وقتی در بخش جلویی سایت جوملا اجرا میشوید، مقدار `"site"` را برمیگرداند.
نام کلاس نهایی برای com_content، به صورت زیر خواهد بود:
`\Joomla\Component\Content\Site\Service\Router`
و طبق قواعد نامگذاری PSR-4، این کلاس در مسیر
`components/com_content/src/Service/Router.php`
قرار دارد.
شما باید این الگو را هرگاه برای کامپوننت خودتان یک router تعریف میکنید، دنبال کنید.
انتخاب Router کامپوننت شما
همانطور که پیشتر گفته شد، جوملا انتظار دارد که router کامپوننت شما در کلاس
\<فضای نام شما>\Site\Service\Router
و در فایل
`components/com_yourcomponent/src/Service/Router.php`
قرار داشته باشد و این کلاس باید اینترفیس
`Joomla\CMS\Component\Router\RouterInterface`
را پیادهسازی کند.
در این مرحله شما حق انتخاب دارید که از روتر سنتی با توابع `preprocess()`, `build` و `parse()` استفاده کنید، یا گزینه RouterView را به کار ببرید.
اگر از روتر سنتی استفاده میکنید، باید کد اسکلتی زیر را تکمیل نمایید:
use Joomla\CMS\Component\Router\RouterInterface;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\Database\DatabaseInterface;
class Router implements RouterInterface
{
public function __construct($application, $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) {}
public function build(&$query) {}
public function parse(&$segments) {}
public function preprocess($query) {}
}
اما اگر روش RouterView را انتخاب کنید، کلاس شما باید از کلاس `Joomla\CMS\Component\Router\RouterView` ارثبری کند که خود به صورت پیشفرض اینترفیس `RouterInterface` را پیادهسازی کرده است. پیکربندیهای RouterView مطابق توضیحات در بخش «تنظیمات RouterView» ارائه میشود.
استفاده از Router سایت در بخش مدیریت (Back-end) جوملا
ممکن است مواردی پیش بیاید که بخواهید در پنل مدیریت، نتیجه ساخت یک URL SEF که در بخش جلویی سایت (Front-end) قرار دارد را نمایش دهید. برای این منظور جوملا متد `link()` را در کلاس `Joomla\CMS\Router\Route`
ارائه داده است. این متد مشابه تابع `Route::_()` عمل میکند، با این تفاوت که یک پارامتر اضافی به نام `client` دارد که هنگام مقداردهی آن به `site`، URL SEF مربوط به سایت ساخته میشود. میتوانید تفاوت این متدها را در مستندات API مشاهده کنید.
برای اینکه این قابلیت در کامپوننت شما به درستی کار کند، باید برخی تغییرات انجام دهید.
اگر از روتر سنتی با توابع `preprocess`، `build` و `parse` استفاده میکنید، احتمالاً تنها تغییر مهمی که باید انجام دهید نحوه دریافت مجموعه منوهای سایت است. هنگام اجرای برنامه سایت (Site application) میتوانید به این صورت عمل کنید:
$sitemenu = $app->getMenu();
اما اگر در بخش مدیریت (administrator) اجرا میکنید، باید به طور مشخص منوهای سایت را بارگذاری کنید:
use Joomla\CMS\Factory;
$app = Factory::getApplication();
$sitemenu = $app->getMenu('site');
$sitemenu->load();
که این موضوع در بخش «منوها و آیتمهای منو» تشریح شده است. نیاز است فقط یک بار بارگذاری انجام شود.
اگر روتر کامپوننت شما از RouterView استفاده میکند، باید اطمینان حاصل کنید که منوهای سایت قبل از فراخوانی `link()` از قبل بارگذاری شدهاند. کد SiteRouter به درستی منوهای سایت را جستجو میکند حتی اگر در بخش مدیریت (backend) اجرا شود، ولی خودش عملیات بارگذاری (`load()`) را انجام نمیدهد. ممکن است در این حالت روتینگ به شکل مورد انتظار عمل نکند. علت دقیق این موضوع مشخص نیست، اما احتمالاً به این دلیل است که الگوریتم روتینگ وضعیت صفحه ی سایت جاری را (یعنی منوی فعال) لحاظ میکند که طبیعتاً وقتی از بخش مدیریت این عملیات اجرا شود، این منوی فعال تنظیم نشده است.