تخصيص حقل Subform في جوملا لبناء دليل متحدثين احترافي

تخصيص حقل Subform في جوملا لبناء دليل متحدثين احترافي

عامل موقع لمؤتمر أو كورس بجوملا، وحطيت المتحدثين في حقل Subform، وفي الآخر طلعلك قائمة نقط كل سطر فيها اسم ومنصب ونبذة ملزوقين في بعض؟ الحل مش إضافة ولا ووردبريس للمحتوى المؤرخ مثل التدوينات والأخبار والشروحات. تُرتّب زمنيًا وتُنظَّم بالتصنيفات والوسوم وتظهر في الأرشيف وخلاصات RSS، بخلاف الصفحات الثابتة.">Page Builder، الحل ملفين PHP صغيرين. تعالى نمشيها خطوة خطوة، وفي الآخر هنقولك تعمل نفس الحكاية إزاي في ووردبريس.

المشكلة: Subform يعرض كل شيء كسطر واحد

حقل Subform في جوملا (Joomla) هو الأداة الطبيعية لأي قائمة متكررة: مجموعة حقول صغيرة يكررها المحرر مرة لكل متحدث. لكن العرض الافتراضي يحوّل كل صف إلى عنصر قائمة واحد، والحقول فيه مفصولة بفواصل:

• Laura Gordon, Joomla Volunteer, [bio text runs on forever]

• Olivier Buisard, Joomla Volunteer, [bio text runs on forever]

• Robin Clapp, Joomla Volunteer, [bio text runs on forever]

يعمل، لكن لا أحد يريده في صفحة متحدثين. الحل هو تجاوز العرض (القالب في Elementor تصميم محفوظ يُعاد استخدامه، مثل رأس الموقع أو التذييل أو صفحة المقالة أو صفحة المنتج. تصممه مرة وتحدد شروط ظهوره، فيُطبَّق على كل…">Template Override): ملفان يعترضان طريقة عرض الحقل ويسلّمانه تخطيطاً حقيقياً.

الخطوة 0: تجهيز الحقول

قبل أي تجاوز، أنشئ مجموعة حقول باسم Speakers فيها أربعة حقول مخصصة (Custom Fields). الاسم البرمجي لكل حقل مهم، لأن الكود سيشير إليه مباشرة:

الحقلالنوعالاسم البرمجي
الاسمTextspeaker-name
المنصبTextspeaker-position
الصورةMediaspeaker-image
النبذةEditorspeaker-bio

اجمع الحقول الأربعة داخل حقل Subform قابل للتكرار اسمه Speakers، واربطه بتصنيف مقالات المتحدثين. الآن يضيف المحرر أي عدد من المتحدثين بزر “+” الأخضر.

تنبيه: في المقال الأصلي ظهر اسم حقل النبذة في الجدول بحرف كبير Speaker-bio، بينما الكود يبحث عن speaker-bio. تأكد أن الاسم البرمجي عندك مطابق حرفياً لما في الكود، وإلا ستظهر البطاقات بلا نبذة دون أي رسالة خطأ.

الخطوة 1: إنشاء ملفات التجاوز

جوملا لا يسمح بتعديل ملفات النواة مباشرة، ولا يجب أن تفعل. أنشئ نسخة تجاوز من مدير القوالب:

  1. من الإضافات والقوالب والمستخدمين والإعدادات، وما يظهر فيها يختلف حسب دور المستخدم.">لوحة التحكم ادخل System ← Site Templates ← Cassiopeia ← Details and Files.
  2. افتح تبويب Create Overrides.
  3. تحت Plugins اختر fields ثم subform. سيُنشئ جوملا الملف /templates/cassiopeia/html/plg_fields_subform/subform.php.
  4. ارجع إلى Create Overrides، وتحت Layouts اختر com_fields ← field. سيُنشئ الملف /templates/cassiopeia/html/layouts/com_fields/field/render.php.

تعديل subform.php مباشرة سيغيّر كل حقول Subform في الموقع، وهذا ما لا تريده إن كان عندك مثلاً حقل “الجلسات” بتصميم مختلف. لذلك:

  1. أنشئ ملفاً جديداً speakers.php داخل /templates/cassiopeia/html/layouts/com_fields/field/.
  2. انسخ إليه محتوى subform.php الافتراضي.
  3. احذف subform.php الأصلي من مجلد التجاوز، فتركه فخ ينتظرك لاحقاً.
  4. أنشئ ملفاً ثانياً في المجلد نفسه: speakers_speaker.php.

ارجع إلى حقل Speakers، وافتح تبويب Options، وستجد speakers ظاهراً في قائمة Layout بجانب الافتراضي. اختره واحفظ. لن يتغيّر شيء في الواجهة بعد، لكن التوصيلات جاهزة.

الخطوة 2: الملف الأول يجهّز البيانات

فكّر في الملفين كقطعتين منفصلتين: speakers.php يجمع البيانات ويرتّبها، وspeakers_speaker.php يعرض بطاقة متحدث واحد. كل قطعة لها وظيفة واحدة، ويمكنك تغيير إحداها دون الأخرى.

ابدأ بتحديث الاستيرادات أعلى speakers.php. الملف الافتراضي يستورد FieldsHelper فقط، وإضافة Text وLayoutHelper تتيح ترجمة عنوان الحقل وتسليم العرض لملف منفصل:

defined(‘_JEXEC’) or die;

defined('_JEXEC') or die;

use JoomlaCMSLanguageText;
use JoomlaCMSLayoutLayoutHelper;
use JoomlaComponentFieldsAdministratorHelperFieldsHelper;

if (!array_key_exists('field', $displayData)) {
    return;
}

$field      = $displayData['field'];
$label      = Text::_($field->label);
$showLabel  = $field->params->get('showlabel');
$suffix     = trim($field->params->get('suffix', ''));
$labelClass = $field->params->get('label_render_class');
$valueClass = $field->params->get('value_render_class');

$value = $field->value;

if ($value == '') {
    return;
}

$result = '';

foreach ($field->subform_rows as $subform_row) {

    // Extract subfields by name
    $fields = [];
    foreach ($subform_row as $subfield) {
        $fields[$subfield->fieldname] = trim(
            FieldsHelper::render(
                'com_content_article',
                'field.' . trim($subfield->params->get('layout', 'render')),
                [
                    'field' => $subfield,
                ]
            )
        );
    }

    // Skip row if all fields are empty
    if (empty(array_filter($fields))) {
        continue;
    }

    // Extract speaker data by field name
    $image    = $fields['speaker-image'] ?? '';
    $name     = $fields['speaker-name'] ?? '';
    $position = $fields['speaker-position'] ?? '';
    $bio      = $fields['speaker-bio'] ?? '';

    // Render the layout by calling the speakers_speaker.php file
    $result .= LayoutHelper::render(
        'com_fields.field.speakers_speaker' . $suffix,
        [
            'image'    => $image,
            'name'     => $name,
            'position' => $position,
            'bio'      => $bio,
        ]
    );
}
?>

<?php if (trim($result) != '') : ?>
    <?php if ($showLabel == 1) : ?>
        <h2 class="field-label my-3 <?php echo $labelClass; ?>">
            <?php echo htmlentities($label, ENT_QUOTES | ENT_IGNORE, 'UTF-8'); ?>
        </h2>
    <?php endif; ?>
    <div class="speakers-container <?php echo $valueClass; ?>">
        <?php echo $result; ?>
    </div>
<?php endif; ?>

use Joomla\CMS\Language\Text;

use Joomla\CMS\Layout\LayoutHelper;

use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;

ثم جهّز متغيرات الحقل: العنوان، وهل يظهر، وأصناف CSS التي يحددها المدير، ولاحقة (Suffix) تسمح لاحقاً بتخطيطات بديلة للبطاقة:

if (!array_key_exists(‘field’, $displayData)) {

    return;

}

$field = $displayData[’field’];

$label = Text::_($field->label);

$showLabel = $field->params->get(‘showlabel’);

$suffix = trim($field->params->get(‘suffix’, ”));

$labelClass = $field->params->get(‘label_render_class’);

$valueClass = $field->params->get(‘value_render_class’);

$value = $field->value;

if ($value == ”) {

    return;

}

الآن الجزء الأهم. الكود الافتراضي يمر على كل صف ثم على كل حقل فرعي ويدمجها في عنصر <li> واحد. احذف تلك الحلقة واستبدلها بهذه، التي تعامل كل صف كسجل منظم:

$result = ”;

foreach ($field->subform_rows as $subform_row) {

    // Extract subfields by name

    $fields = [];

    foreach ($subform_row as $subfield) {

        $fields[$subfield->fieldname] = trim(

            FieldsHelper::render(

                ‘com_content_article’,

                ‘field.’ . trim($subfield->params->get(‘layout’, ‘render’)),

                [

                    ‘field’ => $subfield,

                ]

            )

        );

    }

    // Skip row if all fields are empty

    if (empty(array_filter($fields))) {

        continue;

    }

    // Extract speaker data by field name

    $image = $fields[’speaker-image’] ?? ”;

    $name = $fields[’speaker-name’] ?? ”;

    $position = $fields[’speaker-position’] ?? ”;

    $bio = $fields[’speaker-bio’] ?? ”;

    // Render the layout by calling the speakers_speaker.php file

    $result .= LayoutHelper::render(

        ‘com_fields.field.speakers_speaker’ . $suffix,

        [

            ‘image’ => $image,

            ‘name’ => $name,

            ‘position’ => $position,

            ‘bio’ => $bio,

        ]

    );

}

?>

<?php if (trim($result) != ”) : ?>

    <?php if ($showLabel == 1) : ?>

        <h2 class=”field-label my-3 <?php echo $labelClass; ?>”>

            <?php echo htmlentities($label, ENT_QUOTES | ENT_IGNORE, ‘UTF-8’); ?>

        </h2>

    <?php endif; ?>

    <div class=”speakers-container <?php echo $valueClass; ?>”>

        <?php echo $result; ?>

    </div>

<?php endif; ?>

قراران تصميميان يستحقان الانتباه:

  • الصفوف الفارغة تُتخطى بصمت، فلا تظهر بطاقات متحدثين فارغة في الصفحة.
  • هذا الملف لا يبني HTML للمتحدث إطلاقاً. يسلّم المتغيرات الأربعة إلى speakers_speaker.php عبر LayoutHelper::render(). تريد تصميماً مختلفاً للبطاقة لاحقاً؟ تعدّل الملف الثاني فقط.

الخطوة 3: الملف الثاني يرسم البطاقة

يستقبل speakers_speaker.php القيم الأربع عبر $displayData ويحوّلها إلى بطاقة منسقة بأصناف Bootstrap المدمجة في Cassiopeia:

<?php

defined('_JEXEC') or die;

use JoomlaCMSFactory;

/** @var JoomlaCMSWebAssetWebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

$wa->addInlineStyle('
    .speaker-image img {
        width: 400px;
        height: auto;
        border-radius: 4px;
    }
    .speaker-divider:last-child {
        display: none;
    }
');

$image    = $displayData['image']    ?? '';
$name     = $displayData['name']     ?? '';
$position = $displayData['position'] ?? '';
$bio      = $displayData['bio']      ?? '';
?>

<div class="speaker-row d-flex flex-wrap">
    <div class="speaker-layout d-flex gap-4 align-items-start mb-3">
        <?php if ($image) : ?>
            <figure class="speaker-image m-0">
                <?php echo $image; ?>
                <?php if ($name) : ?>
                    <figcaption class="speaker-image-caption text-center small mt-1"><?php echo $name ?></figcaption>
                <?php endif; ?>
            </figure>
        <?php endif; ?>
        <div class="speaker-details d-flex flex-column gap-1">
            <?php if ($name) : ?>
                <div class="speaker-name fw-bold fs-5"><?php echo $name ?></div>
            <?php endif; ?>
            <?php if ($position) : ?>
                <div class="speaker-position fst-italic text-secondary mb-2"><?php echo $position ?></div>
            <?php endif; ?>
            <?php if ($bio) : ?>
                <div class="speaker-bio"><?php echo $bio ?></div>
            <?php endif; ?>
        </div>
    </div>
</div>

<hr class="speaker-divider">
<div class="speakers-container">
   [Speaker Card 1: headshot, name, title, bio]
   [Speaker Card 2: headshot, name, title, bio]
   [Speaker Card 3: headshot, name, title, bio]
</div>

<?php

defined(‘_JEXEC’) or die;

use Joomla\CMS\Factory;

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */

$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

$wa->addInlineStyle(‘

    .speaker-image img {

        width: 400px;

        height: auto;

        border-radius: 4px;

    }

    .speaker-divider:last-child {

        display: none;

    }

‘);

$image = $displayData[’image’] ?? ”;

$name = $displayData[’name’] ?? ”;

$position = $displayData[’position’] ?? ”;

$bio = $displayData[’bio’] ?? ”;

?>

<div class=”speaker-row d-flex flex-wrap”>

    <div class=”speaker-layout d-flex gap-4 align-items-start mb-3″>

        <?php if ($image) : ?>

            <figure class=”speaker-image m-0″>

                <?php echo $image; ?>

                <?php if ($name) : ?>

                    <figcaption class=”speaker-image-caption text-center small mt-1″><?php echo $name ?></figcaption>

                <?php endif; ?>

            </figure>

        <?php endif; ?>

        <div class=”speaker-details d-flex flex-column gap-1″>

            <?php if ($name) : ?>

                <div class=”speaker-name fw-bold fs-5″><?php echo $name ?></div>

            <?php endif; ?>

            <?php if ($position) : ?>

                <div class=”speaker-position fst-italic text-secondary mb-2″><?php echo $position ?></div>

            <?php endif; ?>

            <?php if ($bio) : ?>

                <div class=”speaker-bio”><?php echo $bio ?></div>

            <?php endif; ?>

        </div>

    </div>

</div>

<hr class=”speaker-divider”>

الملف يضيف CSS مضمّناً صغيراً يحدد عرض الصورة، ويخفي الخط الفاصل بعد آخر بطاقة حتى لا تنتهي القائمة بخط يتيم.

ملاحظة: على موقع عربي بقالب RTL، أصناف Bootstrap مثل d-flex وgap-4 تنعكس تلقائياً فتظهر الصورة يميناً والنص يساراً، وهذا المطلوب غالباً. لكن الصورة بعرض ثابت 400px ستضغط النص على الموبايل، ففكّر في إضافة max-width: 100% داخل addInlineStyle أو استخدام flex-wrap على speaker-layout كذلك.

النتيجة

قبل التجاوز، كان الحقل ينتج هذا فقط:

<ul>

  <li>Name1, Position1, Bio1</li>

  <li>Name2, Position2, Bio2</li>

  <li>Name3, Position3, Bio3</li>

</ul>

بعده، نفس المحتوى، بنفس زر “+” عند المحرر، يُعرض هكذا:

<div class=”speakers-container”>

   [Speaker Card 1: headshot, name, title, bio]

   [Speaker Card 2: headshot, name, title, bio]

   [Speaker Card 3: headshot, name, title, bio]

</div>

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

الدرس الأعمق أن التجاوز في جوملا لا يجب أن يكون إعادة كتابة شاملة للنواة. ملف تخطيط واحد باسم واضح ووظيفة محددة يكفي لتحويل محتوى منظم إلى تصميم حقيقي.

في Elementor + JetEngine: نفس الفكرة بلا PHP

في JetEngine: المقابل المباشر لحقل Subform هو حقل التكرار (Repeater) داخل صندوق الحقول (Meta Box). أنشئ Repeater فيه أربعة حقول فرعية (name, position, image, bio) واربطه بنوع المحتوى “الفعاليات” مثلاً.

ثم للعرض عندك طريقتان:

  1. ودجت Dynamic Repeater من JetEngine داخل Elementor، وتكتب فيه تخطيط العنصر مرة واحدة بالحقول الأربعة.
  2. أو مصدر استعلام من الـ Repeater في الـ ومنشئ الاستعلامات مع Elementor.">JetEngine تبني الاستعلامات بصريًا بمنشئ…">Query Builder (منشئ الاستعلامات)، ثم قالب قائمة (Listing) تصمّم فيه بطاقة المتحدث بصرياً بالسحب والإفلات، وتعرضه بـ Listing Grid.

الطريقة الثانية هي نفس فلسفة الملفين في جوملا: الاستعلام يجهّز البيانات، والـ Listing يرسم البطاقة. تغيّر تصميم البطاقة من مكان واحد، وتعيد استخدامها في أي صفحة، قطعة قطعة.

الخلاصة

بدل قائمة نقاط باهتة، ملفا PHP في مجلد التجاوز يحوّلان حقل Subform إلى دليل متحدثين حقيقي، دون المساس بباقي الحقول أو بطريقة عمل المحرر. افصل البيانات عن العرض، وسمِّ التخطيط باسم واضح، واختره من إعدادات الحقل.

ولو أنت على ووردبريس وعايز تبني نفس دليل المتحدثين بـ JetEngine من غير كود، ابدأ من هنا احصل على JetEngine، ولو محتاج حد يبنيه معاك احجز جلسة من هنا احجز استشارة مجانية.

مرجع المقال (بالإنجليزية): Beyond the Bullet List: Overriding a Joomla Subform to Build a Real Speaker Directory
المقال ده مش ترجمة حرفية: متكيّف للقارئ العربي ومضاف عليه سياق ووردبريس وElementor وJetEngine.

الأسئلة الشائعة

ما هو حقل Subform في جوملا؟

حقل مخصص قابل للتكرار يجمع عدة حقول فرعية، مثل الاسم والمنصب والصورة والنبذة، ويسمح للمحرر بإضافة صف جديد بالضغط على زر “+” لكل عنصر.

ما هو الـ Template Override في جوملا؟

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

هل تعديل subform.php يؤثر على كل حقول الموقع؟

نعم، لذلك الأفضل إنشاء ملف تخطيط باسم جديد مثل speakers.php واختياره من إعدادات الحقل نفسه، فيبقى التغيير محصوراً في هذا الحقل.

ما المقابل في ووردبريس؟

في ووردبريس مع JetEngine تستخدم حقل Repeater داخل صندوق الحقول، ثم تعرضه عبر قالب Listing في Elementor، دون كتابة PHP.

اترك تعليقاً