I'm trying to create an EAV attribute for the customer, and I need the input to be the product categories tree like the following image:
Is this acheivable? And how? please.
1 Answer
You can reuse some Catalog resources and you need to define custom Source Model
1. Source Model
app/code/Acme/StackExchange/Model/Source/CategoriesTree.php
<?php
declare(strict_types=1);
namespace Acme\StackExchange\Model\Source;
use Magento\Backend\Model\Auth\Session;
use Magento\Catalog\Model\Category as CategoryModel;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\Data\OptionSourceInterface;
use Magento\Framework\DB\Helper as DbHelper;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Serialize\SerializerInterface;
class CategoriesTree implements OptionSourceInterface
{
private const CATEGORY_TREE_ID = 'CATALOG_PRODUCT_CATEGORY_TREE';
protected LocatorInterface $locator;
protected CategoryCollectionFactory $categoryCollectionFactory;
protected DbHelper $dbHelper;
protected CacheInterface $cache;
protected SerializerInterface $serializer;
protected Session $session;
public function __construct(
LocatorInterface $locator,
CategoryCollectionFactory $categoryCollectionFactory,
DbHelper $dbHelper,
CacheInterface $cache,
SerializerInterface $serializer,
Session $session
) {
$this->locator = $locator;
$this->categoryCollectionFactory = $categoryCollectionFactory;
$this->dbHelper = $dbHelper;
$this->cache = $cache;
$this->serializer = $serializer;
$this->session = $session;
}
/**
* @inheritDoc
*/
public function toOptionArray()
{
$storeId = (int)$this->locator->getStore()->getId();
$cachedCategoriesTree = $this->cache->load($this->getCategoriesTreeCacheId($storeId, ''));
if (!empty($cachedCategoriesTree)) {
return $this->serializer->unserialize($cachedCategoriesTree);
}
$categoriesTree = $this->retrieveCategoriesTree(
$storeId,
$this->retrieveShownCategoriesIds($storeId, '')
);
$this->cache->save(
$this->serializer->serialize($categoriesTree),
$this->getCategoriesTreeCacheId($storeId, ''),
[
\Magento\Catalog\Model\Category::CACHE_TAG,
\Magento\Framework\App\Cache\Type\Block::CACHE_TAG,
]
);
return $categoriesTree;
}
/**
* Get cache id for categories tree.
*
* @param int $storeId
* @param string $filter
* @return string
*/
private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string
{
if ($this->session->getUser() !== null) {
return self::CATEGORY_TREE_ID
. '_' . (string)$storeId
. '_' . $this->session->getUser()->getAclRole()
. '_' . $filter;
}
return self::CATEGORY_TREE_ID
. '_' . (string)$storeId
. '_' . $filter;
}
/**
* Retrieve tree of categories with attributes.
*
* @param int $storeId
* @param array $shownCategoriesIds
* @return array|null
* @throws LocalizedException
*/
private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds): ?array
{
$collection = $this->categoryCollectionFactory->create();
$collection->addAttributeToFilter('entity_id', ['in' => array_keys($shownCategoriesIds)])
->addAttributeToSelect(['name', 'is_active', 'parent_id'])
->setStoreId($storeId);
$categoryById = [
CategoryModel::TREE_ROOT_ID => [
'value' => CategoryModel::TREE_ROOT_ID,
'optgroup' => null,
],
];
foreach ($collection as $category) {
foreach ([$category->getId(), $category->getParentId()] as $categoryId) {
if (!isset($categoryById[$categoryId])) {
$categoryById[$categoryId] = ['value' => $categoryId];
}
}
$categoryById[$category->getId()]['is_active'] = $category->getIsActive();
$categoryById[$category->getId()]['label'] = $category->getName();
$categoryById[$category->getId()]['__disableTmpl'] = true;
$categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()];
}
return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup'];
}
/**
* Retrieve filtered list of categories id.
*
* @param int $storeId
* @param string $filter
* @return array
* @throws LocalizedException
*/
private function retrieveShownCategoriesIds(int $storeId, string $filter = ''): array
{
$matchingNamesCollection = $this->categoryCollectionFactory->create();
if (!empty($filter)) {
$matchingNamesCollection->addAttributeToFilter(
'name',
['like' => $this->dbHelper->addLikeEscape($filter, ['position' => 'any'])]
);
}
$matchingNamesCollection->addAttributeToSelect('path')
->addAttributeToFilter('entity_id', ['neq' => CategoryModel::TREE_ROOT_ID])
->setStoreId($storeId);
$shownCategoriesIds = [];
foreach ($matchingNamesCollection as $category) {
foreach (explode('/', $category->getPath()) as $parentId) {
$shownCategoriesIds[$parentId] = 1;
}
}
return $shownCategoriesIds;
}
}
2. Define field in form
app/code/Acme/StackExchange/view/adminhtml/ui_component/customer_form.xml
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<fieldset name="customer">
<container name="container_category_ids" sortOrder="400">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="formElement" xsi:type="string">container</item>
<item name="componentType" xsi:type="string">container</item>
<item name="component" xsi:type="string">Magento_Ui/js/form/components/group</item>
<item name="label" translate="true" xsi:type="string">Categories</item>
<item name="required" xsi:type="boolean">false</item>
</item>
</argument>
<field name="category_ids" formElement="select" component="Magento_Catalog/js/components/new-category">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="filterOptions" xsi:type="boolean">true</item>
<item name="chipsEnabled" xsi:type="boolean">true</item>
<item name="disableLabel" xsi:type="boolean">true</item>
<item name="elementTmpl" xsi:type="string">ui/grid/filters/elements/ui-select</item>
<item name="levelsVisibility" xsi:type="string">1</item>
<item name="componentType" xsi:type="string">field</item>
<item name="config" xsi:type="array">
<item name="dataScope" xsi:type="string">category_ids</item>
<item name="sortOrder" xsi:type="string">10</item>
</item>
</item>
</argument>
<formElements>
<select>
<settings>
<options class="Acme\StackExchange\Model\Source\CategoriesTree"/>
</settings>
</select>
</formElements>
</field>
</container>
</fieldset>
</form>
-
It's a great answer that I believe should work correctly, but I'm working on a custom form that doesn't support modifiers, so I need to handle it using a custom renderer. Is it achievable? Aug 16, 2022 at 7:30
-
I've provided the code which is not required any modifiers or something else. Just follow instructions from answer. Aug 16, 2022 at 12:10
-
My form doesn't build using XML, so how can I overwrite it using XML? Aug 16, 2022 at 12:17
-
You can use my example and try to convert the logic as UI Element and use it. But I suggest to move your form logic to use UI Component Form. In question you've asked to implement this for customer form (UI Component based) and I've provided how to do this for customer form. Aug 16, 2022 at 12:22