مروری بر دسته بندی ها

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

در ابتدا مفید است که در مورد نحوه ذخیره و ارتباط دسته‌بندی‌ها در پایگاه داده فکر کنید. نمودار زیر به عنوان مثال دسته‌بندی‌های مرتبط با کامپوننت com_contact را نشان می‌دهد. مستطیل‌های آبی مربوط به جداول پایگاه داده و مستطیل‌های زرد مربوط به کلاس‌های جوملا هستند. نوارهای قرمز نمایانگر رکوردهایی در جدول Categories هستند.

Categories

جدول Categories در جوملا به «بخش‌هایی» (partition) تقسیم شده است، که هر بخش مخصوص یک کامپوننت است که از دسته‌بندی‌ها استفاده می‌کند. (در اینجا منظور از «بخش» مفهومی کلی است و به معنای فنی مثل «پارتیشن‌بندی پایگاه داده» نیست).

در هر بخش، رکوردهای دسته‌بندی به صورت ساختار درختی نگهداری می‌شوند، اما همیشه به یک کامپوننت خاص مرتبط‌اند؛ به عنوان مثال، یک دسته‌بندی از com_content نمی‌تواند بالادستی (parent) یک دسته‌بندی از com_contact باشد. این موضوع در نمودار با فلش‌هایی که به همان بخش بازمی‌گردند نشان داده شده است. ریشه درخت شامل یک گره ریشه سیستمی منفرد است که والد تمام دسته‌بندی‌های سطح بالای کامپوننت‌ها محسوب می‌شود.

اگر یک کامپوننت جوملا از دسته‌بندی‌ها پشتیبانی کند، جدول داده‌های آن کامپوننت ستونی دارد که شناسه (ID) دسته‌بندی را نگهداری می‌کند. بنابراین هر رکورد کامپوننت (حداقل آن‌هایی که با دسته‌بندی مرتبط‌اند) شناسه دسته‌بندی مربوطه را به‌صورت کلید خارجی ذخیره می‌کند. همه مشخصات دسته‌بندی در رکورد متناظر در جدول Categories نگهداری می‌شود.

در مثال com_contact، ارتباط یک به چند بین بخش جدول Categories و جدول Contact Details نشان می‌دهد که یک رکورد دسته‌بندی می‌تواند به چند رکورد com_contact مرتبط باشد.

دو کلاس مهم جوملا (که در نمودار به رنگ زرد نشان داده شده‌اند) مرتبط با دسته‌بندی‌ها و ارائه‌دهنده رابط‌های برنامه‌نویسی (API) برای دسترسی به داده‌های دسته‌بندی هستند:

  1. Categories: مربوط به بخش خاصی از جدول Categories است. هنگام ساختن نمونه‌ای از این کلاس باید مشخص کنید که می‌خواهید به کدام بخش (کامپوننت) دسترسی داشته باشید.
  2. CategoryNode: مربوط به یک رکورد دسته‌بندی خاص است. پس از داشتن شیء Categories، می‌توانید به آبجکت‌های CategoryNode داخل همان بخش دسترسی داشته باشید.

در کد خود می‌توانید به داده‌های دسته‌بندی چندین کامپوننت دسترسی داشته باشید، با ایجاد چند نمونه از کلاس Categories، هر کدام برای یک کامپوننت/بخش مجزا.

استفاده از API دسته‌بندی‌ها

دسترسی به کلاس Categories

برای دسترسی به مجموعه دسته‌بندی‌های com_content می‌توانید از کد زیر استفاده کنید:

use Joomla\CMS\Categories\Categories;

$extension = "content";  // توجه داشته باشید که پیشوند معمول "com_" را در این مقدار نمی‌آوریم.

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

(این روش از یک API منسوخ شده استفاده می‌کند که به صورت مستقیم از Dependency Injection Container عبور نمی‌کند، اما ساده‌تر است. روش جایگزین استفاده از DIC در ادامه توضیح داده شده است).

شما می‌توانید یک آرایه انجمنی (associative array) از گزینه‌ها را به عنوان پارامتر دوم به متد استاتیک getInstance() بدهید. کلیدهای این آرایه عبارت‌اند از:

  •  access: اگر مقدار آن true یا چیزی معادل آن در PHP باشد، فقط دسته‌بندی‌هایی برگردانده می‌شوند که کاربر فعلی اجازه مشاهده آن‌ها را دارد. اگر false باشد، همه دسته‌بندی‌ها برگردانده می‌شوند. مقدار پیش‌فرض true است.
  •  published: اگر مقدار آن عدد صحیح 1 باشد، فقط دسته‌بندی‌هایی که وضعیت انتشارشان 1 (منتشر شده) است برگردانده می‌شوند. در غیر این صورت همه دسته‌بندی‌ها با هر وضعیت منتشرمی‌شوند. مقدار پیش‌فرض 1 است.
  • countitems : اگر مقدار آن 1 باشد، هنگام بازگرداندن دسته‌بندی‌ها، جوملا برای هر دسته مشخص می‌کند که چند رکورد از کامپوننت مربوطه به آن دسته مرتبط هستند (به طور پیش‌فرض این تعداد شمرده نمی‌شود). برای این کار، جوملا یک JOIN در SQL با جدول کامپوننت انجام می‌دهد که فیلد category id آن‌ها با دسته‌بندی مطابقت داشته باشد و تعداد ردیف‌ها را در فیلد key می‌شمارد.
  • table : نام جدول کامپوننت مورد نظر. اگر "countitems" را استفاده کنید، این مقدار باید تعیین شود (پیش‌فرض ندارد).
  • : field نام فیلد در جدول کامپوننت که شناسه دسته‌بندی را نگهداری می‌کند(پیش‌فرض "catid")
  • key : نام فیلد کلیدی جدول کامپوننت که شمارش روی آن انجام می‌شود (پیش‌فرض " id")
  • statefield : نام فیلد وضعیت انتشار در جدول کامپوننت (پیش‌فرض "state")

اگر تنها دسته‌بندی‌های منتشر شده بازگردانده شوند، شمارش نیز فقط روی رکوردهای منتشر شده انجام می‌شود. توجه داشته باشید که این محدودیت برای "access" اعمال نمی‌شود و فیلد access در جدول کامپوننت نادیده گرفته می‌شود.

مثال:

$categories = Categories::getInstance("content", array("access" => false, "published" => 0));

در این مثال، دسته‌بندی‌هایی که برگردانده می‌شوند محدودیتی از نظر دسترسی یا وضعیت انتشار ندارند.

$categories = Categories::getInstance("helloworld", array("countitems" => 1, "table" => "helloworld", "statefield" => "published"));

گره‌های دسته‌بندی کامپوننت "helloworld" تعداد رکوردهای مرتبط در جدول "helloworld" را نیز شامل خواهند شد، به شرطی که وضعیت انتشار آن رکوردها در فیلدی به نام "published" ذخیره شده باشد.

استفاده از متد get() در کلاس Categories

پس از داشتن نمونه $categories می‌توانید گره‌های دسته‌بندی (CategoryNode) را به این شکل دریافت کنید:

$categoryNode = $categories->get(12); // گره دسته‌بندی با شناسه 12 را بازمی‌گرداند
  • اگر شناسه دسته‌بندی وارد نشود (get() بدون پارامتر)، گره ریشه (ROOT) دسته‌بندی‌ها که در بالاترین سطح درخت دسته‌بندی‌ها قرار دارد بازگردانده می‌شود.
  • متد get() علاوه بر خواندن رکورد مورد نظر از پایگاه داده، تمام والدین آن رکورد تا رسیدن به ریشه را می‌خواند (که برای تعیین مسیر دسته‌بندی کاربرد دارد) و همچنین تمام فرزندان آن رکورد را نیز بارگذاری می‌کند.
  • داده‌ها در حافظه کش ذخیره می‌شوند تا درخواست‌های بعدی برای گره‌های فرزندان بدون اجرای مجدد کوئری به پایگاه داده پاسخ داده شود.
  • اگر پارامتر دوم $forceload برابر true تنظیم شود، داده‌ها مجدداً از پایگاه داده خوانده می‌شوند و از کش استفاده نمی‌شود.
  • در سایت‌های چندزبانه، دسته‌بندی‌های بازگردانده شده محدود به زبان جاری سایت یا زبان عمومی * هستند، همچنین شمارش رکوردها در جدول کامپوننت فقط مربوط به زبان جاری یا عمومی است. جوملا فرض می‌کند نام ستون زبان در جدول "language" است و این قابل تغییر نیست.

ویژگی‌های (Properties) کلاس CategoryNode

شیء برگشتی از متد get($id) یک نمونه از کلاس CategoryNode است که اطلاعات دسته‌بندی با شناسه $id را دارد.

مثال دریافت گره ریشه و گره‌های فرزند:

use Joomla\CMS\Categories\Categories;

$categories = Categories::getInstance("content");

$rootNode = $categories->get();  // گره ریشه

$categoryNodes = $rootNode->getChildren();  // آرایه‌ای از گره‌های فرزند

 

برخی ویژگی‌های مهم و کاربردی:

ویژگی

توضیح

asset_id

شناسه رکورد مربوط به مدیریت دسترسی‌ها (ACL) در جدول asset (اگر ACL برای دسته تعریف شده باشد)

parent_id

شناسه دسته والد (از جدول دسته‌بندی‌ها، توجه: نه جدول asset)

lft, rgt

مقادیر استفاده شده در مدل Nested Set برای نمایش ساختار درختی

level

سطح دسته در درخت (ریشه=0، فرزند ریشه=1 و …)

extension

نام کامپوننت با پیشوند com_ (مثلاً com_content)

numitems

تعداد رکوردهای مرتبط با این دسته‌بندی در جدول کامپوننت

childrennumitems

مقداردهی نشده، قابل استفاده نیست

slug

رشته‌ای که معمولاً در URLهای SEF جوملا استفاده می‌شود. فرم آن به صورت id:alias است (مثلاً "3:uncategorised")

assets

مقداردهی نشده، قابل استفاده نیست

متدهای get کلاس CategoryNode

چند متد در کلاس CategoryNode وجود دارند که امکان دسترسی به دیگر گره‌های درخت دسته‌بندی را فراهم می‌کنند:

  • getChildren(boolean $recursive = false) آرایه‌ای از فرزندان مستقیم دسته‌بندی را برمی‌گرداند. اگر پارامتر $recursive برابر true باشد، تمام نوادگان (فرزندان، نوه‌ها و ...) را برمی‌گرداند.
  • getParent() گره والد (CategoryNode) این دسته‌بندی را برمی‌گرداند.
  • getSibling(boolean $right = true) گره هم‌سطح (Sibling) را برمی‌گرداند: اگر $right = true است، برادر سمت راست؛ در غیر این صورت برادر سمت چپ را برمی‌گرداند.

چند متد دیگر مقدار ویژگی‌هایی از CategoryNode را برمی‌گردانند:

  • getAuthor(boolean $modified_user = false) شیء کاربر (User) مرتبط با سازنده دسته‌بندی را برمی‌گرداند. اگر $modified_user = true باشد، کاربر آخرین ویرایشگر را بازمی‌گرداند.
  • getMetadata() شیء Registry جوملا حاوی متادیتای دسته‌بندی (ساختار JSON) را بازمی‌گرداند.
  • getParams() شیء Registry جوملا حاوی پارامترهای دسته‌بندی (ساختار JSON)  را بازمی‌گرداند.
  • getNumItems(boolean $recursive = false) تعداد رکوردهای مرتبط با این دسته‌بندی در جدول اکستنشن را بازمی‌گرداند. اگر $recursive = true باشد، تعداد رکوردهای مرتبط با این دسته و تمام فرزندانش شمارش می‌شود.
  • getPath() آرایه‌ای از slug‌ها (به شکل id:alias) را بازمی‌گرداند که مسیر دسته‌بندی از ریشه درخت تا این گره را نشان می‌دهد. مثال: اگر دسته دارای id=9 و alias برابر "dog" باشد، و والد آن id=6 و alias "mammal"، و پدربزرگ (زیر ریشه) id=3 و alias "animal",‌ خروجی به شکل زیر خواهد بود:
array(

    3 => "3:animal",

    6 => "6:mammal",

    9 => "9:dog"

)

نکته: متد hasParent() شامل ریشه نیز می‌شود، بنابراین برای همه گره‌ها به جز خود ریشه مقدار true برمی‌گرداند.

متدهای set کلاس CategoryNode

چند متد set نیز وجود دارد، اما این‌ها عمدتاً توسط سیستم دسته‌بندی جوملا برای تنظیم مقادیر بر اساس داده‌های دریافتی از پایگاه داده استفاده می‌شوند و تغییر دادن آن‌ها توسط شما به داده‌های ذخیره شده در پایگاه داده تاثیری نمی‌گذارد.
به عنوان مثال، نمی‌توانید با setParent() دسته‌ای را در پایگاه داده به والد دیگری منتقل کنید!

نمونه کد ماژول ساده

در ادامه نمونه‌ای از یک ماژول ساده جوملا برای نمایش قابلیت‌های API دسته‌بندی ارائه شده است.
اگر در ساخت و نصب ماژول تازه‌کار هستید، آموزش Creating a simple module  پیشنهاد می‌شود.

ساختار فایل‌ها

در پوشه‌ای به نام mod_sample_categories دو فایل زیر را ایجاد کنید:

mod_sample_categories.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="module" version="3.1" client="site" method="upgrade">
    <name>Categories demo</name>
    <version>1.0.1</version>
    <description>Code demonstrating use of Joomla APIs related to Categories</description>
    <files>
        <filename module="mod_sample_categories">mod_sample_categories.php</filename>
    </files>
</extension>

 

mod_sample_categories.php

 
<?php
defined('_JEXEC') or die('Restricted Access');

use Joomla\CMS\Factory;
use Joomla\CMS\Categories\Categories;
use Joomla\CMS\Categories\CategoryNode;

$app = Factory::getApplication();
$input = $app->input;
$ext = $input->get('categoryextension', "Content", "STRING");
$tab = $input->get('categorytable', "Content", "STRING");
echo "Getting {$ext} categories and using {$tab} table<br>";
echo "-------------<br>";

$categories = Categories::getInstance($ext, array("table" => "Content", "countItems" => 1, "access" => false));

$cat0 = $categories->get('root');

$cats = $cat0->getChildren(true);
foreach ($cats as $cat)
{
    echo "Category {$cat->id}, title: {$cat->title}<br>";
    $parent = $cat->getParent();
    echo "Level: {$cat->level}, parent id: {$cat->parent_id}, title: {$parent->title}<br>";
    echo "Numitems {$cat->getNumitems()}, including descendants: {$cat->getNumitems(true)}<br>";
    var_dump($cat->getPath());
    echo "-------------<br>";
}

$ext2 = $input->get('option', "", "STRING");
$catid = $input->get('catid', 0, "INT");
$view = $input->get('view', "", "STRING");
$id = $input->get('id', 0, "INT");
if ($ext2 && (strtolower(substr($ext2, 0, 4)) == "com_") && ($catid || (strtolower($view) == "category" && $id)))
{
    $ext2 = substr($ext2, 4);
    $categories2 = Categories::getInstance($ext2, array("access" => false));
    $categoryId = $catid ? $catid : $id; 
    echo "<br>Getting $ext2 category $categoryId<br>";
    $cat2 = $categories2->get($categoryId);
    if ($cat2)
    {
        echo "Category {$cat2->id}, title: {$cat2->title}<br>";
    }
}
 

 

فایل mod_sample_categories را فشرده‌سازی کنید تا mod_sample_categories.zip ایجاد شود.

در پنل مدیریت جوملا، به بخش نصب افزودنی‌ها بروید و از طریق برگه بارگذاری بسته، این فایل zip را برای نصب ماژول نمونه دسته‌ها انتخاب کنید.             

برای نمایش این ماژول، آن را ویرایش کنید (با کلیک بر روی آن در صفحه ماژول‌ها) سپس:

- وضعیت آن را منتشر (Published) کنید.

- یک موقعیت در صفحه برای نمایش آن انتخاب کنید.

- در برگه اختصاص منو، صفحات مورد نظر برای نمایش آن را مشخص کنید.

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

- شناسه و عنوان

- سطح در درخت دسته‌بندی و شناسه و عنوان والد

- تعداد مقالاتی که این دسته در آنها تنظیم شده و تعداد مقالاتی که این دسته یا هر یک از نوادگان آن را دارند.

- یک var_dump از مقدار بازگشتی category getPath().

شما می‌توانید دسته‌ها را برای دیگر کامپوننت‌ها با افزودن پارامترهای categoryextension و categorytable به URL دریافت کنید.

به عنوان مثال

categoryextension=contact&categorytable=contact_details&…

برای دریافت دسته‌های com_contact. توجه داشته باشید اگر می‌خواهید دسته‌های یک کامپوننت که یکی از کامپوننت‌های اصلی جوملا نیست را دریافت کنید، ممکن است نیاز باشد نام‌های فیلدهای کامپوننت و غیره را در گزینه‌های فراخوانی Categories::getInstance() ارائه دهید، همانطور که در بالا توضیح داده شد.

کد همچنین سعی می‌کند حدس بزند که آیا صفحه نمایش داده شده مربوط به یک دسته است یا نه، با بررسی وجود پارامتر catid در URL، یا اینکه آیا پارامتر view به 'category' تنظیم شده است. در این صورت، شناسه و عنوان دسته مربوطه را نمایش می‌دهد. واضح است که این روش ممکن است در تمام موارد به درستی کار نکند.

دریافت دسته‌ها از طریق کانتینر تزریق وابستگی

به طور کلی، روش ترجیحی برای دسترسی به کلاس دسته‌ها (Categories) از طریق کانتینر تزریق وابستگی (DI container) است و به طور خاص با استفاده از کلاس `CategoryFactory` که از طریق کانتینر DI به دست می‌آید. پس از اینکه یک `CategoryFactory` دارید، می‌توانید متد `createCategory($options)` را بر روی آن فراخوانی کنید و یک شیء از نوع `Categories` دریافت کنید؛ آرایه `$options` مجموعه‌ای از گزینه‌ها است که در بالای این صفحه توضیح داده شده است. با این حال، شیء `CategoryFactory` باید با افزونه‌ای که می‌خواهید به دسته‌های آن دسترسی پیدا کنید، نمونه‌سازی شود، زیرا متد `createCategory()` سعی می‌کند به کلاس زیر دسترسی پیدا کند تا جدول پایگاه داده افزونه را پیدا کند.

\\<namespace prefix>\\Site\\Service\\Category

یک روش برای انجام این کار به منظور دسترسی به دسته‌های `com_content`، استفاده از کد زیر در فایل `services/provider.php` شما است.

 
<?php

defined('_JEXEC') or die;

use Joomla\CMS\Extension\Service\Provider\CategoryFactory;
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\CMS\Dispatcher\ModuleDispatcherFactoryInterface;
use Joomla\CMS\Extension\ModuleInterface;
use Joomla\CMS\Helper\HelperFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Mycompany\Module\CategoriesDemo\Site\Extension\CategoriesDemoModule;
use Joomla\CMS\Categories\CategoryFactoryInterface;

return new class () implements ServiceProviderInterface {

    public function register(Container $container)
    {
        $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));
        $container->registerServiceProvider(new ModuleDispatcherFactory('\\Mycompany\\Module\\CategoriesDemo'));
        $container->registerServiceProvider(new HelperFactory('\\Mycompany\\Module\\CategoriesDemo\\Site\\Helper'));
        //$container->registerServiceProvider(new Module());
        $container->set(
            ModuleInterface::class,
            function (Container $container) {
                $module = new CategoriesDemoModule(
                    $container->get(ModuleDispatcherFactoryInterface::class),
                    $container->has(HelperFactoryInterface::class) ? $container->get(HelperFactoryInterface::class) : null
                );
                $module->setCategoryFactory($container->get(CategoryFactoryInterface::class));
                return $module;
            }
        );
    }
};
 

 

این خط:

$container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));

 

تابع `register()` در مسیر زیر اجرا خواهد شد

 `libraries/src/Extension/Service/Provider/CategoryFactory.php`

این کار یک ورودی در کانتینر تزریق وابستگی (DI container) با کلید زیر ایجاد می‌کند

 `Joomla\CMS\Categories\CategoryFactoryInterface`

و به عنوان مقدار، تابعی قرار می‌دهد که یک شیء `CategoryFactory` را با پیشوند فضای نام (namespace prefix) مربوط به `com_content` نمونه‌سازی می‌کند و باز می‌گرداند.

این خط:

$module->setCategoryFactory($container->get(CategoryFactoryInterface::class));

این ورودی را از کانتینر تزریق وابستگی (DI container) دریافت می‌کند (و در نتیجه نمونه‌ی CategoryFactory را می‌گیرد) و آن را با استفاده از متد `setCategoryFactory` در ارتباط با افزونه (Extension) ما `$module` ذخیره می‌کند.

نکته‌ی بالا این فرض را در نظر می‌گیرد که پیش‌وند فضای نام (namespace prefix) ماژول خود را در فایل manifest ماژول تنظیم کرده باشید:

<namespace path="src">Mycompany\Module\CategoriesDemo</namespace>

سپس در فایل افزونه خود (در مسیر src/Extension/CategoriesDemoModule.php ماژول شما):

 
<?php

namespace Mycompany\Module\CategoriesDemo\Site\Extension;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Categories\CategoryServiceInterface;
use Joomla\CMS\Categories\CategoryServiceTrait;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Psr\Container\ContainerInterface;
use Joomla\CMS\Extension\Module;
    
class CategoriesDemoModule extends Module implements CategoryServiceInterface, BootableExtensionInterface
{
    use CategoryServiceTrait;
    
    public static $categories;
    
    public function boot(ContainerInterface $container)
    {
        self::$categories = $this->categoryFactory->createCategory();
    }
    
    public static function getCategories()
    {
        return self::$categories;
    }
}
 

 

این همان شیء Extension است که در فایل services/provider.php ساخته شده و به عنوان $module بازگردانده می‌شود. تابع setCategoryFactory که در آنجا استفاده شده، در Trait به نام CategoryServiceTrait قرار دارد و این تابع شیء CategoryFactory را به عنوان یک متغیر محلی ذخیره می‌کند که می‌توانید با استفاده از `$this->categoryFactory` به آن دسترسی پیدا کنید. همچنین می‌توانید از کد زیر استفاده کنید:

self::$categories = $this->getCategory();

زمانی که ماژول شما اجرا شود، جوملا تابع boot() شما را فراخوانی می‌کند و این تابع یک متغیر استاتیک را تنظیم می‌کند که نمونه‌ی Categories را در خودش نگه می‌دارد. سپس می‌توانید از طریق این متغیر استاتیک به آن دسترسی پیدا کنید، مثلاً:

use Mycompany\Module\CategoriesDemo\Site\Extension\CategoriesDemoModule;
// ...
$categories = CategoriesDemoModule::$categories;

 

اما توجه داشته باشید که:

- اگر مثل کد نمونه ماژول، به صورت پویا افزونه‌ای که می‌خواهید دسته‌های آن را دریافت کنید تعیین می‌کنید، این موضوع می‌تواند مشکل‌ساز باشد، چون روی خط زیر تاثیر می‌گذارد:

$container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));

 

که هنگام بارگذاری ورودی در DI container انجام می‌شود.

- اگر بخواهید دسته‌های چندین کامپوننت مختلف را در ماژول خود دسترسی داشته باشید، این روش کار نمی‌کند. چون اگر بخواهید دو کلاس CategoryFactory برای `com_content` و `com_contact` را به این شکل ثبت کنید:

$container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact'));

 

ورودی دوم ورودی اول را بازنویسی می‌کند، چون هر دو از یک کلید در DI container استفاده می‌کنند.

به طور کلی، هنوز همه مسائل مربوط به استفاده از DI container به شکلی کامل و رضایت‌بخش حل نشده‌اند!

پیاده‌سازی دسته‌بندی‌ها در کامپوننت شما

پیاده‌سازی دسته‌بندی‌ها در یک کامپوننت سفارشی کار نسبتاً ساده‌ای است. در اینجا خلاصه‌ای از کارهایی که باید انجام دهید آمده است:

مشخص کردن ستون شناسه دسته‌بندی در جدول پایگاه داده کامپوننت

تمام داده‌های دسته‌بندی در جدول Categories نگهداری می‌شود؛ شما فقط باید یک کلید خارجی به رکورد مربوطه در جدول Categories مشخص کنید، بنابراین باید یک ستون اضافی در جدول کامپوننت خود برای نگهداری این کلید ایجاد کنید. معمولاً نام این ستون را 'catid' می‌گذارند، ولی اجباری نیست. این ستون با فیلد 'id' در جدول Categories همخوانی دارد.

اضافه کردن فیلدی برای انتخاب دسته‌بندی

هر جا که به کاربران اجازه می‌دهید رکورد جدید اضافه یا رکورد موجود را ویرایش کنند، باید فرم را گسترش دهید تا بتوان دسته‌بندی مرتبط را وارد کرد. می‌توانید چیزی شبیه کد زیر را به فایل XML مربوط به فرم افزودن یا ویرایش رکوردهای کامپوننتتان اضافه کنید:

 
<field
    name="catid"
    type="category"
    extension="com_yourcomponent"
    class="inputbox"
    default=""
    label="JCATEGORY"
    required="true"
    >
    <option value="0">JOPTION_SELECT_CATEGORY</option>
</field>

 

اگر نام فیلد را همان نام ستون جدول پایگاه داده انتخاب کنید، جوملا به صورت خودکار داده‌های فرم ارسالی را به جدول پایگاه داده نگاشت خواهد کرد.

افزودن ورودی‌ها در منوی فرعی مدیریت

شما باید یک ورودی در منوی فرعی کامپوننت خود اضافه کنید تا مدیران بتوانند داده‌های دسته‌بندی را مدیریت کنند. این کار را می‌توانید داخل تگ‌های `<submenu>` در فایل XML مانیفست کامپوننت خود و در بخش مدیریت (administrator) انجام دهید:

 
<administration>
    <submenu>
        <menu link="option=com_categories&amp;extension=com_yourcomponent">JCATEGORIES</menu>
    </submenu>
</administration>
 

مشخص کردن مجوزهای دسترسی (ACL) برای دسته‌بندی‌ها

لازم است برخی ورودی‌ها را به فایل access.xml کامپوننت خود اضافه کنید، برای مثال:

 
<?xml version="1.0" encoding="utf-8" ?>
<access component="com_yourcomponent">
    <section name="component">
            <action name="core.admin" title="JACTION_ADMIN"/>
            <action name="core.manage" title="JACTION_MANAGE"/>
            <action name="core.create" title="JACTION_CREATE"/>
            <action name="core.delete" title="JACTION_DELETE"/>
            <action name="core.edit" title="JACTION_EDIT"/>
            <action name="core.edit.state" title="JACTION_EDITSTATE"/>
    </section>
    <section name="category">
            <action name="core.create" title="JACTION_CREATE"/>
            <action name="core.delete" title="JACTION_DELETE"/>
            <action name="core.edit" title="JACTION_EDIT"/>
            <action name="core.edit.state" title="JACTION_EDITSTATE"/>
    </section>
</access>
 

خلاصه دسته‌بندی بر اساس وضعیت انتشار

در فرم دسته‌بندی‌ها در بخش مدیریت، معمولاً برای هر رکورد دسته‌بندی تعداد رکوردهای کامپوننت شما که در وضعیت‌های منتشر شده (Published)، منتشر نشده (Unpublished)، آرشیو شده (Archived) یا حذف شده (Trashed) هستند نمایش داده می‌شود. برای پیاده‌سازی این قابلیت، `com_categories` کامپوننت شما را راه‌اندازی کرده و تابع `countItems()` (که در اینترفیس `Joomla\CMS\Categories\CategoryServiceInterface` تعریف شده) را روی نمونه کلاس Extension شما فراخوانی می‌کند.

شما می‌توانید تابع `countItems()` را مستقیماً پیاده‌سازی کنید یا اگر فیلد دسته‌بندی شما نامش `catid` است، از تریت `Joomla\CMS\Categories\CategoryServiceTrait` استفاده کرده و متدهای زیر را بازنویسی کنید:

- `getTableNameForSection` — نام جدول کامپوننت خود را برگردانید،

- `getStateColumnForSection` — ستونی را که وضعیت رکورد را نگه می‌دارد (مثلاً ‘published’) برگردانید.

در این صورت `com_categories` خودش روی جدول کامپوننت شما پرس‌وجو را انجام خواهد داد.

ایجاد یک دسته‌بندی بدون دسته (Uncategorised Category)

شاید متوجه شده باشید که هنگام نصب اولیه جوملا، برای هر یک از کامپوننت‌های اصلی جوملا که از دسته‌بندی استفاده می‌کنند، یک رکورد دسته‌بندی به نام "Uncategorised" وجود دارد. همچنین فیلد `catid` در جدول‌های پایگاه داده آنها اجازه مقدار `NULL` را نمی‌دهد — یعنی هر مورد از کامپوننت باید به یک رکورد دسته‌بندی مرتبط باشد.

شما هم باید این الگو را دنبال کنید و رکورد دسته‌بندی "Uncategorised" کامپوننت خود را در اسکریپت نصب (install script) کامپوننت‌تان ایجاد کنید. در غیر این صورت ممکن است در بخش‌هایی (مثلاً ارتباطات چندزبانه) دچار مشکل شوید، چون کد کتابخانه جوملا گاهی فرض می‌کند که یک دسته مرتبط بودن اختیاری نیست.

سایر تغییرات

بسته به قابلیت‌های کامپوننت شما ممکن است بخواهید تغییرات دیگری هم اعمال کنید، مثل:

- اگر فرمی دارید که آیتم‌ها و خصوصیات کامپوننت را نمایش می‌دهد، احتمالاً لازم است آن را گسترش دهید تا دسته‌بندی را هم شامل شود.

- همچنین دسته‌بندی را باید به فیلدهای فیلتر فرم اضافه کنید.

- امکان عملیات گروهی (batch operations) مرتبط با دسته‌بندی‌ها.

 

گرفتن (دریافت) CategoryFactory

اگر قصد پشتیبانی از مسیرهای SEF (Search Engine Friendly) در کامپوننت خود را دارید، نیاز به یک router سفارشی خواهید داشت که از API دسته‌بندی‌ها برای ساختن و تحلیل بخش‌های دسته‌بندی مسیرهای SEF استفاده کند. ممکن است بخواهید از API دسته‌بندی‌ها برای دلایل دیگر هم استفاده کنید، برای مثال جهت نمایش جزئیات دسته‌بندی در نما (view) دسته‌بندی کامپوننتتان.

روش ترجیحی برای دسترسی به شیء Categories این است که آن را از طریق یک شیء CategoryFactory ساخته و این شیء CategoryFactory را از طریق کانتینر تزریق وابستگی (Dependency Injection Container یا DIC) دریافت کنید. برای این کار می‌توانید از روش زیر استفاده کنید:

فایل `services/provider.php` مثال زیر مربوط به کامپوننت `com_example` با namespace به نام `Mycompany\Component\Example` (که در فایل manifest xml تعریف شده) است. خطوط مربوط به دریافت CategoryFactory کامنت‌گذاری شده‌اند تا توضیح داده شود چه اتفاقی می‌افتد.

 
<?php

defined('_JEXEC') or die;

use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory as ComponentDispatcherFactoryServiceProvider;
use Joomla\CMS\Extension\Service\Provider\CategoryFactory as CategoryFactorServiceProvider;
use Joomla\CMS\Extension\Service\Provider\MVCFactory as MVCFactoryServiceProvider;
use Joomla\CMS\Extension\Service\Provider\RouterFactory as RouterFactoryServiceProvider;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Mycompany\Component\Example\Administrator\Extension\ExampleComponent;
use Joomla\Database\DatabaseInterface;


return new class implements ServiceProviderInterface {
    
    public function register(Container $container): void 
    {
        
        /* The line below will call register() in libraries/src/Extension/Service/Provider/CategoryFactory.php
            * That function will create an entry in our component's child DIC with:
            *   key = 'Joomla\CMS\Categories\CategoryFactoryInterface'
            *   value = a function which will 
            *      1. create an instance of CategoryFactory, instantiated with the namespace string passed in
            *      2. call setDatabase() on that CategoryFactory instance, so that it's got access to the database object
            *      3. return the CategoryFactory instance
            */
        $container->registerServiceProvider(new CategoryFactorServiceProvider('\\Mycompany\\Component\\Example'));
        $container->registerServiceProvider(new MVCFactoryServiceProvider('\\Mycompany\\Component\\Example'));
        $container->registerServiceProvider(new ComponentDispatcherFactoryServiceProvider('\\Mycompany\\Component\\Example'));
        $container->registerServiceProvider(new RouterFactoryServiceProvider('\\Mycompany\\Component\\Example'));
        $container->set(
            ComponentInterface::class,
            function (Container $container) {
                // The next line creates an instance of our com_example component Extension class
                $component = new ExampleComponent($container->get(ComponentDispatcherFactoryInterface::class));
                $component->setMVCFactory($container->get(MVCFactoryInterface::class));
                /* The line below will get from the DIC the entry with key 'Joomla\CMS\Categories\CategoryFactoryInterface'
                    * The CategoryFactory instance will be returned, and we'll save a reference to it in our component by
                    * calling setCategoryFactory(), passing it in as the parameter
                    */
                $component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
                $component->setRouterFactory($container->get(RouterFactoryInterface::class));
                $component->setDatabase($container->get(DatabaseInterface::class));

                return $component;
            }
        );
    }
};
 

و برای مطابقت با این، باید چند خط کلیدی در کلاس Extension خود داشته باشید:

 
<?php

namespace Mycompany\Component\Example\Administrator\Extension;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Categories\CategoryServiceInterface;
use Joomla\CMS\Categories\CategoryServiceTrait;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Extension\MVCComponent;
use Psr\Container\ContainerInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\Database\DatabaseAwareTrait;

    
class ExampleComponent extends MVCComponent implements 
    CategoryServiceInterface, RouterServiceInterface, BootableExtensionInterface
{
    use CategoryServiceTrait;
    use RouterServiceTrait;
    use DatabaseAwareTrait;
    
    // Use a static variable to store the Categories instance
    public static $categories;

    public function boot(ContainerInterface $container)
    {
        self::$categories = $this->categoryFactory->createCategory();
    }
 

توضیح درباره CategoryServiceTrait و کلاس Category

`CategoryServiceTrait` که استفاده شده است، شامل تابع `setCategoryFactory` است که در فایل `services/provider.php` برای ذخیره آبجکت برگشتی از `CategoryFactory` استفاده شده. این تابع، نمونه‌ی `CategoryFactory` را در یک متغیر عضو به نام `$categoryFactory` ذخیره می‌کند.

هر زمان که جوملا بخواهد کامپوننت را اجرا کند، ابتدا فایل `services/provider.php` را اجرا می‌کند (که در آن نمونه‌ای از کلاس Extension ساخته می‌شود) و سپس متد `boot()` را روی این شیء Extension فراخوانی می‌کند. کد درون `boot()` به متغیر ذخیره‌شده `CategoryFactory` دسترسی پیدا می‌کند و تابع `createCategory()` را روی آن فراخوانی می‌کند تا یک نمونه از کلاس Categories بسازد. نتیجه این کار در یک متغیر استاتیک ذخیره می‌شود که می‌توان از هر جای کامپوننت به آن دسترسی داشت:

 

$categories = ExampleComponent::$categories;

کد `createCategory()` در `CategoryFactory` از namespace‌ای که به آن داده شده استفاده می‌کند (که مثلاً در `services/provider.php` بالا تعریف شده) و تلاش می‌کند کلاس `<namespace>\Site\Service\Category` را نمونه‌سازی کند. اگر چنین کلاسی وجود نداشته باشد استثناء (Exception) رخ می دهد. بنابراین شما باید فایل زیر را در مسیر سایت کامپوننت خود در `src/Service/Category.php` قرار دهید:

 
<?php

namespace Mycompany\Component\Example\Site\Service;

use Joomla\CMS\Categories\Categories;

\defined('_JEXEC') or die;

class Category extends Categories
{

    public function __construct($options = array())
    {
        $options['table']     = '#__example';
        $options['extension'] = 'com_example';

        parent::__construct($options);
    }
}
 

با این روش، آرایه تنظیمات (options) مربوط به API دسته‌بندی تعریف می‌شود. (شما می‌توانید این options را مستقیماً هنگام فراخوانی `createCategory()` به عنوان پارامتر بدهید، ولی همچنان باید فایل کلاس `Category.php` خودتان را فراهم کنید).

فیلدهای چندگانه دسته‌بندی

می‌توانید چند نوع دسته‌بندی متفاوت را به کامپوننت خود اختصاص دهید. به عنوان مثال، اگر یک کامپوننت به نام `com_product` داشته باشید، می‌توانید:

- یک دسته‌بندی اندازه (size category) که ممکن است شامل مقادیری مثل کوچک، متوسط، بزرگ باشد، و

- یک دسته‌بندی قیمت (price category) که ممکن است شامل ارزان، متوسط، گران باشد.

برای پشتیبانی از این کار، جوملا به شما اجازه می‌دهد که بخش‌های (sections) مختلفی در قسمت کامپوننت شما در جدول Categories داشته باشید، به طوری که مقادیر فیلد `extension` در جدول Categories به صورت `"com_product.size"` یا `"com_product.price"` باشند.

پیاده‌سازی در کامپوننت شما:

واضح است که شما به دو ستون جدا در جدول پایگاه داده کامپوننت خود نیاز دارید، مثلا `catid1` و `catid2`.

تغییرات لازم دیگر:

منوی فرعی مدیریت (Administrator submenu)

باید آیتم‌های زیر را در منوی فرعی مدیریت کامپوننت خود اضافه کنید:

 
<menu link="option=com_categories&amp;extension=com_product.size">Size categories</menu>
<menu link="option=com_categories&amp;extension=com_product.price">Price categories</menu>
 

تا منوی ۲ دسته‌بندی مختلف در بخش مدیریت ایجاد شود.

فرم ویرایش در مدیریت (Administrator edit item)

در فرم ویرایش محصول (مثلا `edit product xml`) که محصول و دسته‌بندی‌های اندازه و قیمت مرتبط با آن را تعریف می‌کنید، فیلدهای مربوط به دسته‌بندی‌ها باید فرق بین نوع دسته‌بندی را مشخص کنند، مثلا:

<field name=catid1 type="category" extension="com_product.size" …/>

<field name=catid2 type="category" extension="com_product.price" …/>

و همینطور این تمایز باید در فایل‌هایی مثل `filter_products.xml` که فیلتر لیست محصولات را تعریف می‌کند، رعایت شود.

خلاصه دسته‌بندی (Category Summary)

در کلاس Extension کامپوننت خود باید تابع `countItems()` (که در تریت `Joomla\CMS\Categories\CategoryServiceTrait` قرار دارد) را پیاده‌سازی کنید. کامپوننت `com_categories` این تابع را با نام بخش دسته‌بندی (category section) فراخوانی می‌کند.

اگر از `ContentHelper::countRelations()` برای ارائه خلاصه تعداد آیتم‌ها استفاده می‌کنید، باید فیلد شناسه دسته مربوط به هر بخش را مشخص کنید، مثلا:

- `'catid1'` برای بخش `'size'`

- `'catid2'` برای بخش `'price'`

نمونه‌ای از این پیاده‌سازی در ادامه آمده است.

 
public function countItems(array $items, string $section)
{
    $config = (object) [
        'related_tbl'   => $this->getTableNameForSection($section),
        'state_col'     => $this->getStateColumnForSection($section),
        'group_col'     => $section == "size" ? 'catid1' : 'catid2',
        'relation_type' => 'category_or_group',
    ];

    ContentHelper::countRelations($items, $config);
}