commercetools-8.x-1.2-alpha1/modules/commercetools_content/src/Form/CatalogFiltersForm.php
modules/commercetools_content/src/Form/CatalogFiltersForm.php
<?php
namespace Drupal\commercetools_content\Form;
use Drupal\commercetools\CommercetoolsService;
use Drupal\commercetools\CommercetoolsLocalization;
use Drupal\commercetools_content\Service\CommercetoolsContentComponents;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides catalog filters.
*
* @internal
*/
class CatalogFiltersForm extends FormBase implements BaseFormIdInterface {
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'commercetools_content_catalog_filters_form';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
// Try to fix an error with several identical forms on the same page.
// @todo Remove after https://www.drupal.org/project/drupal/issues/2821852
static $index = 0;
return $this->getBaseFormId() . $index++;
}
/**
* The Commercetools service.
*
* @var \Drupal\commercetools\CommercetoolsService
*/
protected CommercetoolsService $ct;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* The Commercetools service.
*
* @var \Drupal\commercetools\CommercetoolsLocalization
*/
protected CommercetoolsLocalization $ctLocalization;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->ct = $container->get('commercetools');
$instance->ctLocalization = $container->get('commercetools.localization');
$instance->renderer = $container->get('renderer');
return $instance;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $install_state = NULL) {
[$searchResult, $enabledFilters, $activeFilters, $appliedSorts, $productListIndex] = $form_state->getBuildInfo()['args'];
$fractionDigits = $this->ctLocalization->getFractionDigits();
$paramName = CommercetoolsContentComponents::getParamNameByIndex('filters', $productListIndex);
$form['#attributes']['data-product-list-index'] = $productListIndex;
$form['#attributes']['class'][] = 'commercetools-content-auto-submit';
$form['#attributes']['class'][] = 'commercetools-filters-form';
$form[$paramName] = [
'#type' => 'container',
'#tree' => TRUE,
];
// Build filters.
foreach ($enabledFilters as $filter) {
$path = $filter['path'];
switch ($filter['widget_type']) {
case 'facet':
case 'facet_count':
$form[$paramName][$path] = $this->buildFacetFilter($filter, $searchResult['facets'] ?? [], $activeFilters);
break;
case 'textfield':
case 'checkbox':
$form[$paramName][$path] = $this->buildSimpleFilter($filter, $activeFilters);
break;
case 'separator':
$form[$paramName][$path] = $this->buildSeparator($filter);
break;
case 'custom':
// @todo Add handling for other custom filters or rename the group
// from 'custom' to 'range' if only range filters are expected.
$form[$paramName][$path] = $this->buildRangeFilter($filter, $activeFilters, $searchResult['facets'] ?? [], $fractionDigits);
break;
}
}
// Add sort by select.
$language_fallback = $this->ctLocalization->getLanguageFallbacks();
$locale = !empty($language_fallback) ? reset($language_fallback) : NULL;
$form['sort_by'] = [
'#type' => 'select',
'#title' => $this->t('Sort by'),
'#options' => [
'name.' . $locale . ' asc' => $this->t('Name ↑'),
'name.' . $locale . ' desc' => $this->t('Name ↓'),
'price asc' => $this->t('Price ↑'),
'price desc' => $this->t('Price ↓'),
],
'#name' => 'sorts',
'#default_value' => !empty($appliedSorts) ? $appliedSorts : NULL,
'#empty_option' => $this->t('Default'),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Apply'),
];
$form['#attached']['library'][] = 'commercetools_content/ajaxify_blocks';
return $form;
}
/**
* Build faced filter.
*/
protected function buildFacetFilter(array $filter, array $facetResults = [], array $activeFilters = []) {
$label = [
'#theme' => 'commercetools_filter_label',
'#label' => $filter['label'],
];
$element = [
'#title' => $this->renderer->render($label),
'#type' => 'fieldset',
];
foreach ($facetResults as $facetResult) {
if ($facetResult['facet'] !== $filter['path']) {
continue;
}
foreach ($facetResult['value']['terms'] as $term) {
$labelRender = [
'#theme' => 'commercetools_filter_option',
'#label' => $term['label'] ?? $term['term'],
'#count' => $filter['widget_type'] === 'facet_count' ? $term['productCount'] : NULL,
'#key' => $filter['attributeKey'],
];
$element[] = [
'#type' => 'checkbox',
'#title' => $this->renderer->render($labelRender),
'#return_value' => $term['term'],
'#default_value' => in_array($term['term'], $activeFilters[$facetResult['facet']] ?? []),
'#attributes' => [
'class' => ['facet-filter'],
'data-product-attribute-key' => $filter['type'] === 'attribute' ? $filter['attributeKey'] : NULL,
],
];
}
}
return $element;
}
/**
* Build simple filter.
*/
protected function buildSimpleFilter($filter, array $activeFilters = []) {
$group_label = [
'#theme' => 'commercetools_filter_label',
'#label' => $filter['label'],
];
return [
'#title' => $this->renderer->render($group_label),
'#type' => $filter['widget_type'],
'#default_value' => $activeFilters[$filter['path']] ?? NULL,
];
}
/**
* Build custom filter.
*/
protected function buildRangeFilter($filter, array $activeFilters = [], array $facetResults = [], $fractionDigits = NULL): array {
$label = [
'#theme' => 'commercetools_filter_label',
'#label' => $filter['label'],
];
$element = [
'#title' => $this->renderer->render($label),
'#type' => 'fieldset',
];
foreach ($facetResults as $facet) {
if ($facet['facet'] === CommercetoolsService::CT_VARIANTS_PRICE_AMOUNT) {
$min = $facet['value']['ranges'][0]['min'] ?? NULL;
$max = $facet['value']['ranges'][0]['max'] ?? NULL;
}
}
// Convert stored cents back to display format.
$fromValue = isset($activeFilters[$filter['path']]['from'])
? number_format($activeFilters[$filter['path']]['from'], $fractionDigits, '.', '')
: NULL;
$toValue = isset($activeFilters[$filter['path']]['to'])
? number_format($activeFilters[$filter['path']]['to'], $fractionDigits, '.', '')
: NULL;
// Calculate step based on fractionDigits: 1/10^fractionDigits.
$step = $fractionDigits > 0 ? 1 / pow(10, $fractionDigits) : 1;
$element['from'] = [
'#type' => 'number',
'#title' => $this->t('From:'),
'#step' => $step,
'#min' => 0,
'#default_value' => $fromValue,
'#placeholder' => isset($min) ? $this->convertPrice($min, $fractionDigits, 'divide') : '',
];
$element['to'] = [
'#type' => 'number',
'#title' => $this->t('To:'),
'#step' => $step,
'#min' => 0,
'#default_value' => $toValue,
'#placeholder' => isset($max) ? $this->convertPrice($max, $fractionDigits, 'divide') : '',
];
return $element;
}
/**
* Build simple filter.
*/
protected function buildSeparator($separator) {
return [
'#type' => 'html_tag',
'#tag' => 'h3',
'#value' => $separator['label'],
];
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$query = $this->getRequest()->query->all();
// Reset page after applying new facets.
unset($query['page']);
[$searchResult, $enabledFilters, , , $productListIndex, $targetPage] = $form_state->getBuildInfo()['args'];
$fractionDigits = $this->ctLocalization->getFractionDigits();
$paramName = CommercetoolsContentComponents::getParamNameByIndex('filters', $productListIndex);
$filtersValues = $form_state->getValue($paramName);
// Convert price values to cents.
foreach ($filtersValues as $path => $value) {
if ($path === CommercetoolsService::CT_VARIANTS_PRICE_AMOUNT) {
$filtersValues[$path]['from'] = $this->convertPrice($value['from'], $fractionDigits);
$filtersValues[$path]['to'] = $this->convertPrice($value['to'], $fractionDigits);
}
}
foreach ($enabledFilters as $filter) {
$filterValue = $filtersValues[$filter['path']] ?? NULL;
$filterValue = is_array($filterValue) ? array_filter($filterValue) : $filterValue;
if (empty($filterValue)) {
unset($query[$paramName][$filter['path']]);
}
else {
$query[$paramName][$filter['path']] = $filterValue;
}
}
$query['sorts'] = $form_state->getValue('sort_by');
$targetPage ??= Url::fromRoute('<current>');
$targetPage->setOption('query', array_filter($query));
$form_state->setRedirectUrl($targetPage);
}
/**
* Convert camel case to kebab case.
*/
protected function camelToKebab(string $string): string {
return strtolower(preg_replace('/[A-Z]/', '-$0', $string));
}
/**
* Converts price values between display format and API format.
*
* @param mixed $priceValue
* The price value (can be string, float, int, or null).
* @param int $fractionDigits
* The number of fraction digits.
* @param string $operation
* Either 'multiply' (to cents) or 'divide' (from cents).
*
* @return string|null
* The converted price or '*' for null values.
*/
private function convertPrice($priceValue, int $fractionDigits = 0, string $operation = 'multiply') {
// Handle null, empty, or '*' values.
if ($priceValue === NULL || $priceValue === '' || $priceValue === '*') {
return '';
}
$priceValue = trim((string) $priceValue);
if ($priceValue === '') {
return '';
}
// Calculate the factor: 10^fractionDigits.
$factor = pow(10, $fractionDigits);
if ($operation === 'multiply') {
// Convert to cents/mills (for API).
$priceFloat = (float) $priceValue;
$result = $priceFloat * $factor;
return (string) (int) round($result);
}
elseif ($operation === 'divide') {
// Convert from cents/mills (for display).
$priceInt = (int) $priceValue;
if ($factor === 1) {
return (string) $priceInt;
}
$result = $priceInt / $factor;
return number_format($result, $fractionDigits, '.', '');
}
throw new \InvalidArgumentException("Operation must be 'multiply' or 'divide'");
}
/**
* Initialize the form state and the entity before the first form build.
*/
protected function init(FormStateInterface $form_state) {
}
}
