Form generation example
August 23, 2025 · AI Conversations
<?php
defined('C5_EXECUTE') or die('Access Denied.');
use ConcreteCoreViewView;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreUrlUrl;
$app = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form = $app->make('helper/form');
$token = $app->make('token');
$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');
/** Generate unique slug */
function generateUniqueSlug()
{
return 'feed-' . bin2hex(random_bytes(4));
}
$generatedUrl = generateUniqueSlug();
// Build options arrays for selects
$localeOptions = [];
foreach ($locales as $locale) {
$localeOptions[$locale->getLocale()] = $locale->getLanguageText() . ' (' . $locale->getLocale() . ')';
}
$productOptions = [];
foreach ($products as $product) {
$productOptions[$product['pID']] = $product['pName'];
}
$productGroupOptions = [];
foreach ($product_groups as $group) {
$productGroupOptions[$group['gID']] = $group['groupName'];
}
$productTypeOptions = [];
foreach ($product_types as $type) {
$productTypeOptions[$type['ptID']] = $type['ptName'];
}
$manufacturerOptions = [];
foreach ($manufacturers as $m) {
$manufacturerOptions[$m['mID']] = $m['mName'];
}
// Derived URLs
$feedSlug = ltrim($generatedUrl, '/');
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed/'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
?>
<form method="post" action="<?= h($view->action('submit')) ?>" class="ccm-dashboard-form">
<?= $token->output('submit_feed') ?>
<?= $form->hidden('feedSlug', $feedSlug) ?>
<?= $form->hidden('url', $fullFeedUrl) ?>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Basic Information') ?></h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<div class="form-group">
<?= $form->label('name', t('Feed Name')) ?> <span class="text-danger">*</span>
<?= $form->text('name', '', ['class' => 'form-control ccm-input-text', 'required' => 'required', 'placeholder' => t('Human-readable feed title')]) ?>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<?= $form->label('url_display', t('Feed URL')) ?>
<div class="form-control-plaintext">
<?= h($baseFeedUrl) ?><strong>/<?= h($feedSlug) ?></strong>
</div>
<small class="form-text text-muted">
<?= t('This is the URL where your feed will be accessible.') ?>
</small>
</div>
</div>
</div>
<div class="form-group mt-3">
<?= $form->label('settings[language]', t('Language')) ?>
<?= $form->select('settings[language]', $localeOptions, null, ['class' => 'form-control form-select']) ?>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Products & Options') ?></h5>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div class="row g-3">
<div class="col">
<h3 class="h6 mb-2"><?= t('Products') ?></h3>
<?= $form->label('settings[pName][]', t('Product Name')) ?>
<?= $form->select('settings[pName][]', $productOptions, [], [
'class' => 'form-control form-select',
'size' => 5,
'multiple' => 'multiple'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
<div class="col">
<h3 class="h6 mb-2"><?= t('Options') ?></h3>
<?= $form->label('settings[records_per_page]', t('Max number of products to display')) ?>
<?= $form->number('settings[records_per_page]', '', [
'class' => 'form-control ccm-input-text',
'placeholder' => t('Leave blank or 0 to list all matching products'),
'min' => 0
]) ?>
</div>
</div>
</div>
<div class="form-group mb-0">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[sort_order]', t('Sort Order')) ?>
<?= $form->select('settings[sort_order]', [
'alphabetical' => t('Alphabetical'),
'price' => t('Price'),
'date' => t('Date'),
], 'alphabetical', ['class' => 'form-control form-select']) ?>
</div>
<div class="col">
<?= $form->label('settings[product_groups][]', t('Product Groups')) ?>
<?= $form->select('settings[product_groups][]', $productGroupOptions, [], [
'class' => 'form-control form-select',
'size' => 5,
'multiple' => 'multiple'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Filtering') ?></h5>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[product_types][]', t('Product Types')) ?>
<?= $form->select('settings[product_types][]', $productTypeOptions, [], [
'class' => 'form-control form-select',
'size' => 5,
'multiple' => 'multiple'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
<div class="col">
<?= $form->label('settings[brand][]', t('Brand/Manufacturer')) ?>
<?= $form->select('settings[brand][]', $manufacturerOptions, [], [
'class' => 'form-control form-select',
'size' => 5,
'multiple' => 'multiple'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
</div>
</div>
<div class="form-group mb-3">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pSku]', t('Product SKU')) ?>
<?= $form->text('settings[pSku]', '', ['class' => 'form-control ccm-input-text']) ?>
</div>
<div class="col">
<label class="form-label d-block"><?= t('Attributes') ?></label>
<div class="form-check">
<?= $form->checkbox('settings[pFeatured]', 1, false, ['class' => 'form-check-input', 'id' => 'pFeatured']) ?>
<label class="form-check-label" for="pFeatured"><?= t('Include Featured Only') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[onsale]', 1, false, ['class' => 'form-check-input', 'id' => 'onsale']) ?>
<label class="form-check-label" for="onsale"><?= t('Include OnSale Only') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[outofstock]', 1, false, ['class' => 'form-check-input', 'id' => 'outofstock']) ?>
<label class="form-check-label" for="outofstock"><?= t('Include Out Of Stock Products') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[inactive]', 1, false, ['class' => 'form-check-input', 'id' => 'inactive']) ?>
<label class="form-check-label" for="inactive"><?= t('Include Inactive Products') ?></label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Product Attributes') ?></h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pTaxable]', t('Taxable')) ?>
<?= $form->select('settings[pTaxable]', ['0' => t('No'), '1' => t('Yes')], '0', ['class' => 'form-control form-select']) ?>
</div>
<div class="col">
<?= $form->label('settings[pShippable]', t('Shippable')) ?>
<?= $form->select('settings[pShippable]', ['0' => t('No'), '1' => t('Yes')], '0', ['class' => 'form-control form-select']) ?>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Price Range') ?></h5>
</div>
<div class="card-body">
<div class="form-group mb-3">
<?= $form->label('settings[price_range]', t('Price Range')) ?>
<div class="price-input d-flex align-items-center gap-2">
<div class="field">
<span><?= t('Min') ?></span>
<?= $form->text('settings[price_range][min]', $minPrice, ['class' => 'input-min form-control']) ?>
</div>
<div class="separator">-</div>
<div class="field">
<span><?= t('Max') ?></span>
<?= $form->text('settings[price_range][max]', $maxPrice, ['class' => 'input-max form-control']) ?>
</div>
</div>
<div class="slider mt-3">
<div class="progress"></div>
</div>
<div class="range-input mt-2">
<input type="range" class="range-min" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$minPrice ?>" step="0.1">
<input type="range" class="range-max" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$maxPrice ?>" step="0.1">
</div>
</div>
<h3 class="h6"><?= t('Automation') ?></h3>
<div class="form-group mb-3 form-check">
<?= $form->checkbox('enable_cron', 1, false, ['class' => 'form-check-input', 'id' => 'enable_cron']) ?>
<label class="form-check-label" for="enable_cron"><?= t('Enable Cron Run') ?></label>
</div>
<div class="ccm-dashboard-form-actions-wrapper">
<div class="ccm-dashboard-form-actions">
<a href="<?= h(Url::to('/dashboard/shopping_feed')) ?>" class="btn btn-secondary float-start"><?= t('Cancel') ?></a>
<button class="float-end btn btn-primary" type="submit"><?= t('Save') ?></button>
</div>
</div>
</div>
</div>
</form>
### Notes
– Multiple selects: Concrete’s `$form->select()` supports `multiple` by passing `['multiple' => 'multiple']` and naming with `[]`.
– CSRF: I used `$token->output('submit_feed')`; check in your controller with `$this->token->validate('submit_feed')`.
– URLs: `Url::to('/google-merchant-feed/')` is preferred in v9, but `$this->url()` also works in a view.
– Range sliders aren’t part of the helper; they’re left as native “ so your existing JS keeps working.
Did you mean to call e.g. "__toString" or "toArray"?
### Quick fix
Replace your import:
use ConcreteCoreUrlUrl;
with:
use ConcreteCoreSupportFacadeUrl; // the facade
Then this works:
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
### Alternatives (also valid)
– In views/controllers you can simply use the view helper you already used elsewhere:
$baseFeedUrl = rtrim($this->url('/google-merchant-feed'), '/');
– Or resolve via the URL resolver:
use ConcreteCoreUrlResolverManagerResolverManagerInterface;
$resolver = $app->make(ResolverManagerInterface::class);
$baseFeedUrl = rtrim((string) $resolver->resolve(['/google-merchant-feed']), '/');
But the smallest change is: use the facade `ConcreteCoreSupportFacadeUrl`.
In Concrete’s `$form->select()` helper, if you want it to render as a **multi-select listbox** (not just a dropdown with multi-select enabled), you need to:
– Pass the `multiple` attribute,
– Give it a `size` greater than 1.
Otherwise Concrete will render it like a normal select (single-line dropdown), even if you pass `multiple`.
—
### Example fix for **Product Name**:
<?= $form->select(
'settings[pName][]',
$productOptions,
[], // default selected
[
'class' => 'form-control',
'multiple' => 'multiple',
'size' => 5 // 👈 ensures it shows as a multi-row listbox
]
) ?>
### For all other multi-selects (Product Groups, Types, Brand, etc.)
Do the same:
<?= $form->select(
'settings[product_groups][]',
$productGroupOptions,
[],
['class' => 'form-control', 'multiple' => 'multiple', 'size' => 5]
) ?>
—
🔑 **Key point:**
`multiple` just enables multi-selection.
`size` tells the browser to show it as a listbox with that many visible rows.
—
Do you want me to go through your whole form and rewrite all the multi-selects with the proper `size` so they render as listboxes instead of dropdowns?
### Likely causes
1) **Class conflict:** `form-control` + `form-select` together can force a single-line height.
2) **JS enhancement:** a plugin (Select2/Choices/etc.) in `sfm/admin` may be converting your “ into a dropdown.
### Fix 1 — Use only `form-select` and boolean multiple
<?= $form->select(
'settings[pName][]',
$productOptions,
[],
[
'id' => 'pNameList',
'class' => 'form-select', // ← remove 'form-control'
'multiple' => true, // ← boolean is fine
'size' => 8 // ← >1 forces listbox
]
) ?>
### Fix 2 — If your JS enhances selects, skip this one
In your `sfm/admin` JS, exclude this field from Select2/Choices init. For example:
// If you do something like: $('.form-select').select2();
$('.form-select').not('#pNameList').select2(); // exclude the multi listbox
### Fix 3 — CSS safety (if theme still forces height)
/* Dashboard-safe override */
.form-select[multiple], .form-select[size] { height: auto !important; }
### Quick sanity check
View source should show:
<select name="settings[pName][]" id="pNameList" class="form-select" multiple size="8">
...
</select>
If that’s what you see but it still renders as a dropdown, it’s **definitely** a JS plugin—exclude this ID from the enhancement and it will display as a proper multi-row listbox.
### 1) Prove the helper is fine (no classes, no JS)
Render a **plain** multi select. If this shows as a listbox, the issue is CSS/JS.
<?= $form->select(
'settings[pName][]',
$productOptions,
[],
[
'id' => 'pNameList',
'multiple' => true,
'size' => 8,
// no classes on purpose
]
) ?>
– In DevTools → Elements, you should see:
<select id="pNameList" multiple size="8" name="settings[pName][]">…</select>
– If you see a “ or similar wrapping, a JS plugin is hijacking it.
—
### 2) If a JS plugin is hijacking it (Select2/Choices/etc.)
**Option A: Destroy it after init**
<script>
document.addEventListener('DOMContentLoaded', function () {
if (window.jQuery && jQuery.fn.select2) {
try { jQuery('#pNameList').select2('destroy'); } catch(e){}
}
if (window.Choices) {
try {
// If your code created Choices on .form-select, try to find/destroy by ref
// Fallback: remove data attributes that Choices adds
const el = document.getElementById('pNameList');
el.removeAttribute('data-choice');
el.classList.remove('choices__input');
// Or just re-clone to strip enhancements:
el.replaceWith(el.cloneNode(true));
} catch(e){}
}
});
</script>
**Option B: Exclude it during enhancement**
In your `sfm/admin` init code, change:
// BEFORE (example)
$('.form-select').select2();
// AFTER
$('.form-select').not('#pNameList,[multiple],[size]').select2(); // skip multi/size selects
// or
$('.form-select').not('#pNameList').select2();
—
### 3) If it’s CSS forcing dropdown height
Some themes set a fixed height that breaks listboxes.
Add this tiny CSS override (in `sfm/admin.css` or the view):
/* Ensure multi-selects render as listboxes */
#pNameList[multiple],
#pNameList[size],
.form-select[multiple],
.form-select[size] {
height: auto !important;
min-height: 12rem; /* shows ~8 options comfortably */
overflow: auto;
}
—
### 4) Final recommended markup (once excluded from JS)
<?= $form->select(
'settings[pName][]',
$productOptions,
[],
[
'id' => 'pNameList',
'class' => 'form-select', // you can keep Bootstrap styling
'multiple' => true,
'size' => 8
]
) ?>
—
### Quick checklist
– [ ] View Source shows “ (good).
– [ ] No `.select2`/`.choices` wrapper around it (if yes → exclude/destroy).
– [ ] Computed CSS for `#pNameList` shows `height: auto` (if not → add override).
If you paste your select-init snippet from `sfm/admin.js`, I’ll tailor the exact exclusion/destroy line for your setup.
<?php
defined('C5_EXECUTE') or die('Access Denied.');
use ConcreteCoreViewView;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreSupportFacadeUrl;
$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');
$app = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form = $app->make('helper/form');
$token = $app->make('token');
$settings = is_array($feed['settings']) ? $feed['settings'] : json_decode((string)$feed['settings'], true) ?: [];
$selectedMin = $settings['price_range']['min'] ?? $minPrice;
$selectedMax = $settings['price_range']['max'] ?? $maxPrice;
$pNameSelected = $settings['pName'] ?? [];
$productGroupsSelected= $settings['product_groups'] ?? [];
$productTypesSelected = $settings['product_types'] ?? [];
$brandSelected = $settings['brand'] ?? [];
$pFeatured = !empty($settings['pFeatured']);
$onSale = !empty($settings['onsale']);
$outOfStock = !empty($settings['outofstock']);
$inactive = !empty($settings['inactive']);
$pTaxable = (string)($settings['pTaxable'] ?? '0');
$pShippable = (string)($settings['pShippable'] ?? '0');
$pLocale = (string)($settings['language'] ?? '');
$enableCron = !empty($feed['enableCron']);
// Build options
$localeOptions = [];
foreach ($locales as $locale) {
$localeOptions[$locale->getLocale()] = $locale->getLanguageText() . ' (' . $locale->getLocale() . ')';
}
$productOptions = [];
foreach ($products as $product) {
$productOptions[$product['pID']] = $product['pName'];
}
$productGroupOptions = [];
foreach ($product_groups as $group) {
$productGroupOptions[$group['gID']] = $group['groupName'];
}
$productTypeOptions = [];
foreach ($product_types as $pt) {
$productTypeOptions[$pt['ptID']] = $pt['ptName'];
}
$manufacturerOptions = [];
foreach ($manufacturers as $m) {
$manufacturerOptions[$m['mID']] = $m['mName'];
}
// Feed URL (display only)
$feedSlug = ltrim($feed['feedSlug'] ?? '', '/');
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
?>
<form method="post" action="<?= h($view->action('submit', $feed['id'])) ?>" class="ccm-dashboard-form">
<?= $token->output('update_feed') ?>
<?= $form->hidden('feedSlug', $feedSlug) ?>
<?= $form->hidden('url', $fullFeedUrl) ?>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Basic Information') ?></h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<div class="form-group">
<?= $form->label('name', t('Feed Name')) ?> <span class="text-danger">*</span>
<?= $form->text('name', $feed['name'] ?? '', [
'class' => 'form-control ccm-input-text',
'required' => 'required',
'placeholder' => t('Human-readable feed title'),
]) ?>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<?= $form->label('url_display', t('Feed URL')) ?>
<div class="form-control-plaintext">
<?= h($baseFeedUrl) ?><strong>/<?= h($feedSlug) ?></strong>
</div>
<small class="form-text text-muted">
<?= t('This is the URL where your feed will be accessible.') ?>
</small>
</div>
</div>
</div>
<div class="form-group mt-3">
<?= $form->label('settings[language]', t('Language')) ?>
<?= $form->select('settings[language]', $localeOptions, $pLocale, ['class' => 'form-select']) ?>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Products & Options') ?></h5>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pName]', t('Product Name')) ?>
<?= $form->selectMultiple(
'settings[pName]',
$productOptions,
$pNameSelected,
['class' => 'form-select', 'size' => 8, 'id' => 'pNameList']
) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
<div class="col">
<?= $form->label('settings[records_per_page]', t('Max number of products to display')) ?>
<?= $form->number('settings[records_per_page]', $settings['records_per_page'] ?? '', [
'class' => 'form-control ccm-input-text',
'placeholder' => t('Leave blank or 0 to list all matching products'),
'min' => 0,
]) ?>
</div>
</div>
</div>
<div class="form-group mb-0">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[sort_order]', t('Sort Order')) ?>
<?= $form->select('settings[sort_order]', [
'alphabetical' => t('Alphabetical'),
'price' => t('Price'),
'date' => t('Date'),
], $settings['sort_order'] ?? 'alphabetical', ['class' => 'form-select']) ?>
</div>
<div class="col">
<?= $form->label('settings[product_groups]', t('Product Groups')) ?>
<?= $form->selectMultiple(
'settings[product_groups]',
$productGroupOptions,
$productGroupsSelected,
['class' => 'form-select', 'size' => 8, 'id' => 'product_groups']
) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Filtering') ?></h5>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[product_types]', t('Product Types')) ?>
<?= $form->selectMultiple(
'settings[product_types]',
$productTypeOptions,
$productTypesSelected,
['class' => 'form-select', 'size' => 8, 'id' => 'product_types']
) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
<div class="col">
<?= $form->label('settings[brand]', t('Brand/Manufacturer')) ?>
<?= $form->selectMultiple(
'settings[brand]',
$manufacturerOptions,
$brandSelected,
['class' => 'form-select', 'size' => 8, 'id' => 'brand']
) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
</div>
</div>
<div class="form-group mb-3">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pSku]', t('Product SKU')) ?>
<?= $form->text('settings[pSku]', $settings['pSku'] ?? '', [
'class' => 'form-control ccm-input-text',
'placeholder' => t('Filter by SKU'),
]) ?>
</div>
<div class="col">
<label class="form-label d-block"><?= t('Attributes') ?></label>
<div class="form-check">
<?= $form->checkbox('settings[pFeatured]', 1, $pFeatured, ['class' => 'form-check-input', 'id' => 'pFeatured']) ?>
<label class="form-check-label" for="pFeatured"><?= t('Include Featured Only') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[onsale]', 1, $onSale, ['class' => 'form-check-input', 'id' => 'onsale']) ?>
<label class="form-check-label" for="onsale"><?= t('Include OnSale Only') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[outofstock]', 1, $outOfStock, ['class' => 'form-check-input', 'id' => 'outofstock']) ?>
<label class="form-check-label" for="outofstock"><?= t('Include Out Of Stock Products') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[inactive]', 1, $inactive, ['class' => 'form-check-input', 'id' => 'inactive']) ?>
<label class="form-check-label" for="inactive"><?= t('Include Inactive Products') ?></label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Product Attributes') ?></h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pTaxable]', t('Taxable')) ?>
<?= $form->select('settings[pTaxable]', ['0' => t('No'), '1' => t('Yes')], $pTaxable, ['class' => 'form-select', 'id' => 'pTaxable']) ?>
</div>
<div class="col">
<?= $form->label('settings[pShippable]', t('Shippable')) ?>
<?= $form->select('settings[pShippable]', ['0' => t('No'), '1' => t('Yes')], $pShippable, ['class' => 'form-select', 'id' => 'pShippable']) ?>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Price Range') ?></h5>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div class="price-input d-flex align-items-center gap-2">
<div class="field">
<span><?= t('Min') ?></span>
<?= $form->text('settings[price_range][min]', $selectedMin, ['class' => 'input-min form-control']) ?>
</div>
<div class="separator">-</div>
<div class="field">
<span><?= t('Max') ?></span>
<?= $form->text('settings[price_range][max]', $selectedMax, ['class' => 'input-max form-control']) ?>
</div>
</div>
<div class="slider mt-3"><div class="progress"></div></div>
<div class="range-input mt-2">
<input type="range" class="range-min" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$minPrice ?>" step="0.1">
<input type="range" class="range-max" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$maxPrice ?>" step="0.1">
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><?= t('Automation') ?></h5>
</div>
<div class="card-body">
<div class="form-check">
<?= $form->checkbox('enable_cron', 1, $enableCron, ['class' => 'form-check-input', 'id' => 'enable_cron']) ?>
<label class="form-check-label" for="enable_cron"><?= t('Enable Cron Run') ?></label>
</div>
</div>
</div>
<div class="ccm-dashboard-form-actions-wrapper">
<div class="ccm-dashboard-form-actions">
<a href="<?= h($view->action('')) ?>" class="btn btn-secondary float-start"><?= t('Cancel') ?></a>
<button class="float-end btn btn-primary" type="submit"><?= t('Update') ?></button>
</div>
</div>
</form>
### Notes
– **`selectMultiple`**: Use names **without** `[]` (e.g. `'settings[pName]'`). The helper outputs `multiple` and `name="…[]"` for you.
– Added `size => 8` so it renders as a **listbox**.
– If your `sfm/admin` JS enhances `.form-select` with Select2/Choices, **exclude** these IDs (`#pNameList`, `#product_groups`, `#product_types`, `#brand`) from enhancement so they stay as listboxes.
$settings = is_array($feed['settings']) ? $feed['settings'] : json_decode((string)$feed['settings'], true) ?: [];
Fix it by either adding parentheses **or** (cleaner) switching to `??`. Here’s a safe, drop‑in replacement for your setup block:
<?php
defined('C5_EXECUTE') or die('Access Denied.');
use ConcreteCoreViewView;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreSupportFacadeUrl;
$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');
$app = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form = $app->make('helper/form');
$token = $app->make('token');
// ✅ avoid nested ternaries
$rawSettings = $feed['settings'] ?? '[]';
$settings = is_array($rawSettings)
? $rawSettings
: (json_decode((string)$rawSettings, true) ?? []);
$selectedMin = $settings['price_range']['min'] ?? $minPrice;
$selectedMax = $settings['price_range']['max'] ?? $maxPrice;
$pNameSelected = $settings['pName'] ?? [];
$productGroupsSelected = $settings['product_groups'] ?? [];
$productTypesSelected = $settings['product_types'] ?? [];
$brandSelected = $settings['brand'] ?? [];
$pFeatured = !empty($settings['pFeatured']);
$onSale = !empty($settings['onsale']);
$outOfStock = !empty($settings['outofstock']);
$inactive = !empty($settings['inactive']);
$pTaxable = (string)($settings['pTaxable'] ?? '0');
$pShippable = (string)($settings['pShippable'] ?? '0');
$pLocale = (string)($settings['language'] ?? '');
$enableCron = !empty($feed['enableCron']);
$feedSlug = ltrim($feed['feedSlug'] ?? '', '/');
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
If you really want to keep the one‑liner, parenthesize it:
$settings = is_array($feed['settings'] ?? null)
? $feed['settings']
: (json_decode((string)($feed['settings'] ?? ''), true) ?: []);
Scan for any other patterns like `a ? b : c ?: d` and either add parentheses or replace the final `?: d` with `?? d`.
Below are three drop‑in snippets:
—
# 1) Element file (common form)
Create this at:
`packages/shopping_feed_manager/elements/shopping_feed/form.php`
<?php
defined('C5_EXECUTE') or die('Access Denied.');
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreSupportFacadeUrl;
/** expected vars:
* $locales, $products, $product_groups, $product_types, $manufacturers
* $minPrice, $maxPrice
* $feed (assoc array for edit) OR null for add
* $actionUrl (string)
* $tokenName (string) e.g. 'submit_feed' or 'update_feed'
*/
$app = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form = $app->make('helper/form');
$token = $app->make('token');
$isEdit = !empty($feed);
$settingsRaw = $isEdit ? ($feed['settings'] ?? '[]') : '[]';
$settings = is_array($settingsRaw) ? $settingsRaw : (json_decode((string)$settingsRaw, true) ?? []);
$selectedMin = $settings['price_range']['min'] ?? $minPrice;
$selectedMax = $settings['price_range']['max'] ?? $maxPrice;
$pNameSelected = $settings['pName'] ?? [];
$productGroupsSelected = $settings['product_groups'] ?? [];
$productTypesSelected = $settings['product_types'] ?? [];
$brandSelected = $settings['brand'] ?? [];
$pFeatured = !empty($settings['pFeatured']);
$onSale = !empty($settings['onsale']);
$outOfStock = !empty($settings['outofstock']);
$inactive = !empty($settings['inactive']);
$pTaxable = (string)($settings['pTaxable'] ?? '0');
$pShippable = (string)($settings['pShippable'] ?? '0');
$pLocale = (string)($settings['language'] ?? '');
$enableCron = $isEdit ? !empty($feed['enableCron']) : false;
// Options builders
$localeOptions = [];
foreach ($locales as $locale) {
$localeOptions[$locale->getLocale()] = $locale->getLanguageText() . ' (' . $locale->getLocale() . ')';
}
$productOptions = [];
foreach ($products as $p) { $productOptions[$p['pID']] = $p['pName']; }
$productGroupOptions = [];
foreach ($product_groups as $g) { $productGroupOptions[$g['gID']] = $g['groupName']; }
$productTypeOptions = [];
foreach ($product_types as $pt) { $productTypeOptions[$pt['ptID']] = $pt['ptName']; }
$manufacturerOptions = [];
foreach ($manufacturers as $m) { $manufacturerOptions[$m['mID']] = $m['mName']; }
// Feed URL / slug
if ($isEdit) {
$feedSlug = ltrim((string)($feed['feedSlug'] ?? ''), '/');
$feedName = (string)($feed['name'] ?? '');
} else {
$feedSlug = 'feed-' . bin2hex(random_bytes(4));
$feedName = '';
}
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
?>
<form method="post" action="<?= h($actionUrl) ?>" class="ccm-dashboard-form">
<?= $token->output($tokenName) ?>
<?= $form->hidden('feedSlug', $feedSlug) ?>
<?= $form->hidden('url', $fullFeedUrl) ?>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><?= t('Basic Information') ?></h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<?= $form->label('name', t('Feed Name')) ?> <span class="text-danger">*</span>
<?= $form->text('name', $feedName, [
'class' => 'form-control ccm-input-text',
'required' => 'required',
'placeholder' => t('Human-readable feed title'),
]) ?>
</div>
<div class="col-md-6">
<?= $form->label('url_display', t('Feed URL')) ?>
<div class="form-control-plaintext">
<?= h($baseFeedUrl) ?><strong>/<?= h($feedSlug) ?></strong>
</div>
<small class="form-text text-muted"><?= t('This is the URL where your feed will be accessible.') ?></small>
</div>
</div>
<div class="form-group mt-3">
<?= $form->label('settings[language]', t('Language')) ?>
<?= $form->select('settings[language]', $localeOptions, $pLocale, ['class' => 'form-select']) ?>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><?= t('Products & Options') ?></h5></div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col">
<?= $form->label('settings[pName]', t('Product Name')) ?>
<?= $form->selectMultiple('settings[pName]', $productOptions, $pNameSelected, [
'class' => 'form-select', 'size' => 8, 'id' => 'pNameList'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
<div class="col">
<?= $form->label('settings[records_per_page]', t('Max number of products to display')) ?>
<?= $form->number('settings[records_per_page]', $settings['records_per_page'] ?? '', [
'class' => 'form-control ccm-input-text',
'placeholder' => t('Leave blank or 0 to list all matching products'),
'min' => 0
]) ?>
</div>
</div>
<div class="row g-3">
<div class="col">
<?= $form->label('settings[sort_order]', t('Sort Order')) ?>
<?= $form->select('settings[sort_order]', [
'alphabetical' => t('Alphabetical'),
'price' => t('Price'),
'date' => t('Date'),
], $settings['sort_order'] ?? 'alphabetical', ['class' => 'form-select']) ?>
</div>
<div class="col">
<?= $form->label('settings[product_groups]', t('Product Groups')) ?>
<?= $form->selectMultiple('settings[product_groups]', $productGroupOptions, $productGroupsSelected, [
'class' => 'form-select', 'size' => 8, 'id' => 'product_groups'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><?= t('Filtering') ?></h5></div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col">
<?= $form->label('settings[product_types]', t('Product Types')) ?>
<?= $form->selectMultiple('settings[product_types]', $productTypeOptions, $productTypesSelected, [
'class' => 'form-select', 'size' => 8, 'id' => 'product_types'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
<div class="col">
<?= $form->label('settings[brand]', t('Brand/Manufacturer')) ?>
<?= $form->selectMultiple('settings[brand]', $manufacturerOptions, $brandSelected, [
'class' => 'form-select', 'size' => 8, 'id' => 'brand'
]) ?>
<small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
</div>
</div>
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pSku]', t('Product SKU')) ?>
<?= $form->text('settings[pSku]', $settings['pSku'] ?? '', ['class' => 'form-control ccm-input-text']) ?>
</div>
<div class="col">
<label class="form-label d-block"><?= t('Attributes') ?></label>
<div class="form-check">
<?= $form->checkbox('settings[pFeatured]', 1, $pFeatured, ['class' => 'form-check-input', 'id' => 'pFeatured']) ?>
<label class="form-check-label" for="pFeatured"><?= t('Include Featured Only') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[onsale]', 1, $onSale, ['class' => 'form-check-input', 'id' => 'onsale']) ?>
<label class="form-check-label" for="onsale"><?= t('Include OnSale Only') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[outofstock]', 1, $outOfStock, ['class' => 'form-check-input', 'id' => 'outofstock']) ?>
<label class="form-check-label" for="outofstock"><?= t('Include Out Of Stock Products') ?></label>
</div>
<div class="form-check">
<?= $form->checkbox('settings[inactive]', 1, $inactive, ['class' => 'form-check-input', 'id' => 'inactive']) ?>
<label class="form-check-label" for="inactive"><?= t('Include Inactive Products') ?></label>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><?= t('Product Attributes') ?></h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col">
<?= $form->label('settings[pTaxable]', t('Taxable')) ?>
<?= $form->select('settings[pTaxable]', ['0' => t('No'), '1' => t('Yes')], $pTaxable, ['class' => 'form-select', 'id' => 'pTaxable']) ?>
</div>
<div class="col">
<?= $form->label('settings[pShippable]', t('Shippable')) ?>
<?= $form->select('settings[pShippable]', ['0' => t('No'), '1' => t('Yes')], $pShippable, ['class' => 'form-select', 'id' => 'pShippable']) ?>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><?= t('Price Range') ?></h5></div>
<div class="card-body">
<div class="form-group mb-3">
<div class="price-input d-flex align-items-center gap-2">
<div class="field">
<span><?= t('Min') ?></span>
<?= $form->text('settings[price_range][min]', $selectedMin, ['class' => 'input-min form-control']) ?>
</div>
<div class="separator">-</div>
<div class="field">
<span><?= t('Max') ?></span>
<?= $form->text('settings[price_range][max]', $selectedMax, ['class' => 'input-max form-control']) ?>
</div>
</div>
<div class="slider mt-3"><div class="progress"></div></div>
<div class="range-input mt-2">
<input type="range" class="range-min" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$minPrice ?>" step="0.1">
<input type="range" class="range-max" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$maxPrice ?>" step="0.1">
</div>
</div>
<div class="form-check">
<?= $form->checkbox('enable_cron', 1, $enableCron, ['class' => 'form-check-input', 'id' => 'enable_cron']) ?>
<label class="form-check-label" for="enable_cron"><?= t('Enable Cron Run') ?></label>
</div>
<div class="ccm-dashboard-form-actions-wrapper mt-3">
<div class="ccm-dashboard-form-actions">
<a href="<?= h(Url::to('/dashboard/shopping_feed')) ?>" class="btn btn-secondary float-start"><?= t('Cancel') ?></a>
<button class="float-end btn btn-primary" type="submit"><?= $isEdit ? t('Update') : t('Save') ?></button>
</div>
</div>
</div>
</div>
</form>
—
# 2) `add.php` (calls the element)
`packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/add.php`
<?php
defined('C5_EXECUTE') or die('Access Denied.');
use ConcreteCoreViewView;
$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');
View::element(
'shopping_feed/form',
[
'locales' => $locales,
'products' => $products,
'product_groups' => $product_groups,
'product_types' => $product_types,
'manufacturers' => $manufacturers,
'minPrice' => $minPrice,
'maxPrice' => $maxPrice,
'feed' => null, // add mode
'actionUrl' => $view->action('submit'),
'tokenName' => 'submit_feed',
],
'shopping_feed_manager'
);
—
# 3) `edit.php` (calls the element)
`packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/edit.php`
<?php
defined('C5_EXECUTE') or die('Access Denied.');
use ConcreteCoreViewView;
$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');
View::element(
'shopping_feed/form',
[
'locales' => $locales,
'products' => $products,
'product_groups' => $product_groups,
'product_types' => $product_types,
'manufacturers' => $manufacturers,
'minPrice' => $minPrice,
'maxPrice' => $maxPrice,
'feed' => $feed, // edit mode data from controller
'actionUrl' => $view->action('submit', $feed['id']),
'tokenName' => 'update_feed',
],
'shopping_feed_manager'
);
—
## Controller token checks (quick heads‑up)
In your single page controller, validate the token names accordingly:
if (!$this->token->validate('submit_feed')) { /* handle error */ }
// or
if (!$this->token->validate('update_feed')) { /* handle error */ }
—
This keeps **one source of truth** for the form, works for both add & edit, and preserves your multi‑select listboxes (`selectMultiple` + `size`).
1) wrong element path/handle,
2) not actually calling the element,
3) missing vars (fatal error hidden by cache),
4) asset/URL import error stopping the view.
Here’s a bullet‑proof setup.
# 1) Put the element here
packages/shopping_feed_manager/elements/shopping_feed/form.php
(Exactly this path. `shopping_feed_manager` must match your **package folder name** / `$pkgHandle`.)
# 2) In `add.php`, call the element like this
Using the `$this->element()` helper is the simplest in single page views:
<?php
defined('C5_EXECUTE') or die('Access Denied.');
/** @var ConcreteCoreViewView $this */
$this->requireAsset('javascript', 'sfm/admin');
$this->requireAsset('css', 'sfm/admin');
$this->element(
'shopping_feed/form',
[
'locales' => $locales,
'products' => $products,
'product_groups' => $product_groups,
'product_types' => $product_types,
'manufacturers' => $manufacturers,
'minPrice' => $minPrice,
'maxPrice' => $maxPrice,
'feed' => null, // add mode
'actionUrl' => $this->action('submit'),
'tokenName' => 'submit_feed',
],
'shopping_feed_manager' // ← your package handle
);
If you prefer `View::element`, this also works:
use ConcreteCoreViewView;
View::element('shopping_feed/form', [...], 'shopping_feed_manager');
# 3) In `edit.php`, same pattern
<?php
defined('C5_EXECUTE') or die('Access Denied.');
$this->requireAsset('javascript', 'sfm/admin');
$this->requireAsset('css', 'sfm/admin');
$this->element(
'shopping_feed/form',
[
'locales' => $locales,
'products' => $products,
'product_groups' => $product_groups,
'product_types' => $product_types,
'manufacturers' => $manufacturers,
'minPrice' => $minPrice,
'maxPrice' => $maxPrice,
'feed' => $feed, // from controller
'actionUrl' => $this->action('submit', $feed['id']),
'tokenName' => 'update_feed',
],
'shopping_feed_manager'
);
# 4) Common gotchas checklist
– **Handle/path:** The 3rd param **must** be your package handle string (`shopping_feed_manager`). The element path must be `/elements/shopping_feed/form.php`.
– **Imports in the element:** In `form.php` use `use ConcreteCoreSupportFacadeUrl;` (the **facade**, not `ConcreteCoreUrlUrl`).
– **Variables exist:** Ensure your controller sets `$locales, $products, $product_groups, $product_types, $manufacturers, $minPrice, $maxPrice` for both add and edit actions.
– **Errors hidden by cache:** Clear cache: Dashboard → System & Settings → Optimization → **Clear Cache**. Check logs at `application/files/logs/` if nothing shows.
– **Minimal sanity test:** Add a temporary `echo 'FORM ELEMENT LOADED';` at the top of `form.php`. If you don’t see it, the path/handle is wrong.
If you paste your current `add()` controller method (where you set the variables), I’ll align it so the element definitely gets everything it needs.
– the element **path/handle** is wrong,
– the **element is throwing a fatal** before output,
– the **variables aren’t set** in the controller,
– or the **call** to render the element is wrong.
Here’s a tight checklist + minimal test code so we can see exactly where it fails.
# 1) Verify file locations (exact)
– Single page view:
packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/add.php
– Common element:
packages/shopping_feed_manager/elements/shopping_feed/form.php
– Package handle in package controller:
protected $pkgHandle = 'shopping_feed_manager';
# 2) Minimal smoke test (element actually gets called)
Temporarily put this at the **very top** of `packages/shopping_feed_manager/elements/shopping_feed/form.php`:
<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<?php echo '<div style="padding:8px;background:#fffae6;border:1px solid #f0c36d;margin-bottom:8px;">FORM ELEMENT LOADED</div>'; ?>
If you **don’t** see “FORM ELEMENT LOADED”, the element isn’t being found/called → go to step 3.
If you **do** see it, the element is loading, and a **fatal later** is killing the rest → go to step 4.
# 3) Call the element in add.php the “safe” way
Use the `$this->element()` form inside a single page **and** pass the package handle (3rd param):
<?php
defined('C5_EXECUTE') or die('Access Denied.');
/** @var ConcreteCoreViewView $this */
$this->requireAsset('javascript', 'sfm/admin');
$this->requireAsset('css', 'sfm/admin');
/* Sanity: show we're in add.php */
echo '<div style="padding:6px;background:#eef;">ADD VIEW LOADED</div>';
$this->element(
'shopping_feed/form',
[
'locales' => $locales ?? [],
'products' => $products ?? [],
'product_groups' => $product_groups ?? [],
'product_types' => $product_types ?? [],
'manufacturers' => $manufacturers ?? [],
'minPrice' => $minPrice ?? 0,
'maxPrice' => $maxPrice ?? 0,
'feed' => null,
'actionUrl' => $this->action('submit'),
'tokenName' => 'submit_feed',
],
'shopping_feed_manager' // <-- must match package handle exactly
);
If still nothing shows from the element, **try without the package handle** by temporarily copying the element to `application/elements/shopping_feed/form.php`, then call:
$this->element('shopping_feed/form', [/* same vars */]);
If it renders from `application/…` but not from `packages/…`, your 3rd param handle or element path under the package is wrong.
# 4) Make sure the controller sets variables
Your single page controller must set the vars that the element expects.
// File: packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboard;
use ConcreteCorePageControllerDashboardPageController;
class ShoppingFeed extends DashboardPageController
{
public function add()
{
// TODO: fetch real data; placeholders here
$this->set('locales', $this->getLocales());
$this->set('products', $this->getProducts());
$this->set('product_groups', $this->getProductGroups());
$this->set('product_types', $this->getProductTypes());
$this->set('manufacturers', $this->getManufacturers());
$this->set('minPrice', 0);
$this->set('maxPrice', 1000);
}
// … helper methods …
}
If these aren’t set, PHP notices/fatals inside the element can short‑circuit output.
# 5) Show PHP errors to catch fatals
Turn on error display temporarily (locally):
– Dashboard → System & Settings → Debug → **Display Errors** → On
or in `application/config/generated_overrides/concrete.php`:
'debug' => [
'display_errors' => true,
],
Also check logs: `application/files/logs/` after hitting the page.
# 6) Element guard + early error surfacing
Wrap the element internals with **existence checks** so missing vars don’t fatal:
<?php
// In the element form.php, right after the "FORM ELEMENT LOADED" test echo, add:
$required = ['locales','products','product_groups','product_types','manufacturers','minPrice','maxPrice','actionUrl','tokenName'];
$missing = array_filter($required, fn($k) => !isset($$k));
if ($missing) {
echo '<pre style="color:#b00;background:#fee;padding:8px;">Missing vars: '.implode(', ', $missing).'</pre>';
return;
}
If you see “Missing vars…”, then your controller isn’t setting them for `add()`.
# 7) Common gotchas
– **Wrong Url import in element**: use the **facade**
use ConcreteCoreSupportFacadeUrl;
not `ConcreteCoreUrlUrl`.
– **Cache**: Clear cache: Dashboard → System & Settings → Optimization → **Clear Cache**.
– **Case/typo in folders**: `elements/shopping_feed/form.php` (not `shopping-feed` or `shoppingfeed`).
– **Calling `$view = View::getInstance()` everywhere**: not needed for `$this->element()`; can skip to reduce risk.
—
If you try the smoke echos and the missing‑vars guard, you’ll see exactly which step is failing. Send me:
– what you see on the page (the two test boxes / missing vars),
– and any log/error message you get,
and I’ll pinpoint the exact line to fix.