commercetools-8.x-1.2-alpha1/modules/commercetools_content/src/Plugin/Block/CommercetoolsContentCategoriesListBlock.php
modules/commercetools_content/src/Plugin/Block/CommercetoolsContentCategoriesListBlock.php
<?php
namespace Drupal\commercetools_content\Plugin\Block;
use Drupal\commercetools_content\CommercetoolsAjaxTrait;
use Drupal\commercetools_content\Service\CommercetoolsAjaxHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\commercetools\Plugin\Block\CommercetoolsCategoriesListBlockBase;
use Drupal\commercetools_content\Form\ContentSettingsForm;
use Drupal\commercetools_content\Service\CommercetoolsContentComponents;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a Categories List block.
*
* @Block(
* id = "commercetools_content_categories_list",
* admin_label = @Translation("Categories"),
* )
*/
class CommercetoolsContentCategoriesListBlock extends CommercetoolsCategoriesListBlockBase {
use CommercetoolsAjaxTrait;
/**
* The commercetools content component service.
*
* @var \Drupal\commercetools_content\Service\CommercetoolsContentComponents
*/
protected $contentComponents;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
$instance = parent::create(...func_get_args());
$instance->contentComponents = $container->get('commercetools_content.content_components');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return parent::defaultConfiguration() + [
'use_ajax' => TRUE,
CommercetoolsAjaxHelper::COMMERCETOOLS_SYSTEM_BLOCK_FORCE_UPDATE_CONFIG => FALSE,
];
}
/**
* {@inheritdoc}
*/
public function getBlockConfigKeys(): array {
$keys = parent::getBlockConfigKeys();
$keys[] = CommercetoolsAjaxHelper::COMMERCETOOLS_SYSTEM_BLOCK_FORCE_UPDATE_CONFIG;
return $keys;
}
/**
* {@inheritdoc}
*/
public function buildSafe(): array {
$build = [];
$displayStyle = $this->configuration['display_style'];
$treeDepth = $this->configuration['max_level'];
$index = $this->configuration['product_list_index'];
$queryParams = $this->contentComponents->getRequestQueryParams();
$paramName = CommercetoolsContentComponents::getParamNameByIndex('category', $index);
$paramNameValue = $queryParams[$paramName] ?? NULL;
$categoriesTree = $this->getCategoriesTree($paramNameValue);
if (!$categoriesTree) {
return $build;
}
$queryParams = $this->cleanupDynamicQueryParamsByIndex($queryParams, $index);
if ($this->configuration['initial_level'] > 1) {
$currentLevel = $paramNameValue
? $this->findCategoryLevel($categoriesTree, $paramNameValue, 2)
: 1;
if ($this->configuration['initial_level'] > $currentLevel) {
return $build;
}
}
$maxDepthTree = $displayStyle === 'cards' ? 1 : 100;
if (!$this->configuration['display_from_current'] && $this->configuration['parent_category']) {
$categoriesTree = $this->getCategorySubtree($categoriesTree, $this->configuration['parent_category'], $maxDepthTree)['children'];
}
if (!$categoriesTree) {
return $build;
}
if ($displayStyle === 'list') {
// Prevent re-ordering by Drupal\Core\Render\Element::children().
$build['#sorted'] = TRUE;
// Add the theme wrapper for outer markup and allow theme overrides.
$build['#theme'] = 'menu__' . $this->getPluginId();
$build['#items'] = $this->buildListItems(
$categoriesTree,
$index,
$queryParams,
$treeDepth,
);
$build['#attributes']['class'][] = 'ct-categories-list-block';
}
elseif ($displayStyle === 'cards') {
if ($this->configuration['display_from_current']) {
$categoriesTree = $paramNameValue
? $this->getCategorySubtree($categoriesTree, $paramNameValue)['children']
: $categoriesTree;
}
$items = [];
foreach ($categoriesTree as $category) {
$queryParams[$paramName] = $category['id'];
$items[] = [
'title' => $category['name'],
'url' => $this->getLink()->setOption('query', $queryParams),
];
}
$build = [
'#theme' => 'commercetools_categories_cards_block',
'#items' => $items,
'#cards_columns' => $this->configuration['cards_columns'],
];
}
// Add ajax class by default.
$this->configuration['use_ajax'] = TRUE;
return $build;
}
/**
* Recursively search for $id and return its depth (0-based), or null.
*/
protected function findCategoryLevel(array $items, string $id, int $currentDepth = 1): ?int {
foreach ($items as $node) {
if ($node['id'] === $id) {
return $currentDepth;
}
if (!empty($node['children'])) {
$childDepth = $this->findCategoryLevel($node['children'], $id, $currentDepth + 1);
if ($childDepth !== NULL) {
return $childDepth;
}
}
}
return NULL;
}
/**
* Find and return a subtree rooted at $id, limited to $maxDepth levels.
*/
protected function getCategorySubtree(array $items, string $id, int $maxDepth = 1): ?array {
// A small recursive helper to trim any node's children to $d levels.
$prune = function (array $nodes, int $d) use (&$prune): array {
if ($d <= 0) {
// No deeper levels at all.
return [];
}
$out = [];
foreach ($nodes as $node) {
$node['children'] = $prune($node['children'] ?? [], $d - 1);
$out[] = $node;
}
return $out;
};
// Depth-first search.
foreach ($items as $node) {
if ($node['id'] === $id) {
// Found the target: prune its children to $maxDepth levels and return.
$node['children'] = $prune($node['children'] ?? [], $maxDepth);
return $node;
}
if (!empty($node['children'])) {
if ($sub = $this->getCategorySubtree($node['children'], $id, $maxDepth)) {
return $sub;
}
}
}
return NULL;
}
/**
* Builds the #items property for a categories tree.
*
* @param array $list
* The data structure representing the categories tree.
* @param int $index
* The component index.
* @param array $queryParams
* The current URL query parameters.
* @param int $treeDepth
* The maximum allowed categories tree depth.
* @param int $currentDepth
* The current level. Optional.
*
* @return array
* A value to use for the #items property to render a system menu.
*/
protected function buildListItems(array $list, int $index, array $queryParams, int $treeDepth, int $currentDepth = 1): array {
$items = [];
$categoryParameterName = CommercetoolsContentComponents::getParamNameByIndex('category', $index);
$baseLink = $this->getLink();
foreach ($list as $data) {
$link = clone $baseLink;
$queryParams[$categoryParameterName] = $data['id'];
$link->setOption('query', $queryParams);
$element = [
'is_active' => !empty($data['is_active']),
'in_active_trail' => !empty($data['in_active_trail']),
'attributes' => new Attribute(),
'title' => $data['name'],
'url' => $link,
'below' => [],
];
if ($data['children'] && (!empty($data['in_active_trail']) || !$treeDepth || $currentDepth < $treeDepth)) {
$element['below'] = $this->buildListItems(
$data['children'],
$index,
$queryParams,
$treeDepth,
$currentDepth + 1,
);
}
$element['is_expanded'] = !empty($element['below']);
$element['is_collapsed'] = empty($element['below']) && !empty($data['children']);
$items[$data['id']] = $element;
}
return $items;
}
/**
* Add option to force update system main content block.
*
* @param array $form
* Form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*
* @return array
* Form.
*/
public function blockForm($form, FormStateInterface $form_state): array {
$form = parent::blockForm($form, $form_state);
$form += $this->getFormElements($this->configuration);
return $form;
}
/**
* Provides a URL instance for category link.
*
* @return \Drupal\Core\Url
* The URL instance.
*/
protected function getLink(): Url {
return empty($this->configuration['target_page']) ?
Url::fromRoute('<current>') :
Url::fromUserInput($this->configuration['target_page']);
}
/**
* {@inheritdoc}
*/
public function getCacheTags(): array {
$cacheTags = parent::getCacheTags();
return Cache::mergeTags($cacheTags, [
'config:' . ContentSettingsForm::CONFIGURATION_NAME,
]);
}
/**
* Filters query parameters, removing only variants.attributes.* filters.
*
* @param array $queryParams
* The original query parameters.
* @param int $index
* The component index.
*
* @return array
* The filtered query parameters.
*/
protected function cleanupDynamicQueryParamsByIndex(array $queryParams, int $index): array {
// Remove pagination to reset to the first page when changing category.
if (isset($queryParams['page'])) {
unset($queryParams['page']);
}
// If filters exist, selectively remove only variants.attributes.* filters.
if (isset($queryParams['filters']) && is_array($queryParams['filters'])) {
foreach ($queryParams['filters'] as $filterKey => $filterValue) {
if (strpos($filterKey, 'variants.attributes.') === 0) {
unset($queryParams['filters'][$filterKey]);
}
}
// If no filters remain after removing attributes, remove the array.
if (empty($queryParams['filters'])) {
unset($queryParams['filters']);
}
}
return $queryParams;
}
}
