1

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: Categories tree in the product form Is this acheivable? And how? please.

1 Answer 1

1

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>

enter image description here

4
  • 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?
    – AboElnouR
    Commented 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. Commented Aug 16, 2022 at 12:10
  • My form doesn't build using XML, so how can I overwrite it using XML?
    – AboElnouR
    Commented 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. Commented Aug 16, 2022 at 12:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.