Magento – Magento2: How to build a custom category link widget with attributes

categorycategory-attributemagento2

I'm trying to build a category link widget similar to the core one, but that can pull in additional information from the category selected, such as the image thumbnail and maybe some custom category attributes we've created. I have created a module that constructs the widget, based on code from the core "Catalog Category Link" widget. That is working well in the Admin side (all widget options are the same as the standard one) and works for creation of a widget that appears on the page, the link works too on the front end. The module is set up with module.xml, registration.php in the usual places.

code\Vendor\CategoryWidget\etc\widget.xml

<?xml version="1.0" encoding="UTF-8"?>
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget id="tec_catlink" class="Vendor\CategoryWidget\Block\Widget\Link" placeholder_image="Magento_Catalog::images/category_widget_link.png">
        <label translate="true">Custom Category Link Widget</label>
        <description>Link to specified category to fit with grid</description>
        <parameters>
            <parameter name="id_path" xsi:type="block" visible="true" required="true" sort_order="10">
                <label translate="true">Category</label>
                <block class="Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser">
                    <data>
                        <item name="button" xsi:type="array">
                            <item name="open" xsi:type="string" translate="true">Select Category...</item>
                        </item>
                    </data>
                </block>
            </parameter>   
            <parameter name="anchor_text" xsi:type="text" visible="true">
                <label translate="true">Anchor Custom Text</label>
                <description translate="true">If empty, we'll use the category name here.</description>
            </parameter>
            <parameter name="title" xsi:type="text" visible="true">
                <label translate="true">Anchor Custom Title</label>
            </parameter>
            <parameter name="template" xsi:type="select" visible="true">
                <label translate="true">Template</label>
                <options>
                    <option name="default"
                            value="widget/category_link.phtml"
                            selected="true">
                        <label translate="true">Grid Link Template</label>
                    </option>
                </options>
            </parameter>
        </parameters>
        <containers>
            <container name="content.subcats">
                <template name="grid" value="default" />
                <template name="list" value="list" />
            </container>
        </containers>
    </widget>
</widgets>

On the front end the link appears correctly but somehow the function that retrieves the label is broken and only appears if Anchor Text has been entered (looks like $entityresource is empty). In addition I want to be able to call attributes from the category for use on the front end but it seems like I don't have the right dependencies in there or I'm not creating/calling a category object correctly that the core methods will work on (rather than recreating functions).

code\Vendor\CategoryWidget\view\frontend\templates\widget\category_link.phtml

<?php
// @codingStandardsIgnoreFile
?>

<li class="subcat-item widget block-category-link">       
    <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>>
        <?php
            $imgUrl = ???
        ?>
        <?php if ($imgUrl) : ?>

            <img width="" src="" class="section-image" alt="" />

        <?php endif; ?>

        <div class="sect-info">

            <h3 class="section-name"><?= $block->escapeHtml($block->getLabel()) ?></h3>

            <ul class="section-bullets">
               <li>Custom Attribute 1</li>
               <li>Custom Attribute 2</li>
               <li>etc</li>
            </ul>
        </div>
    </a>
</li>

I have based the Class on the core widget's Link.php, I've previously tried adding in calls to CategoryFactory and so on that I found in examples elsewhere in the hope they would give access to what I need but the most common error I get is "call to undefined method" even for GetName() or getProductCollection() which work elsewhere in the site. I also want to be able to call attributes via GetCustomAttributeName() in the .phtml. Shown here is the code in its closest state to the core widget code (although the GetLabel() is broken here as I'd like to know the steps to build it up to where I want.

How do I call a category (without using the ObjectManager which I've seen is bad) and associated attributes based on the ID which is returned from the widget data?

code\Vendor\CategoryWidget\Block\Widget\Link.php

    <?php
namespace Vendor\CategoryWidget\Block\Widget;

use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\UrlRewrite\Model\UrlFinderInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;

class Link extends \Magento\Framework\View\Element\Html\Link implements \Magento\Widget\Block\BlockInterface
{
    /**
     * Entity model name which must be used to retrieve entity specific data.
     * @var null|\Magento\Catalog\Model\ResourceModel\AbstractResource
     */
    protected $_entityResource = null;

    /**
     * Prepared href attribute
     *
     * @var string
     */
    protected $_href;

    /**
     * Prepared anchor text
     *
     * @var string
     */
    protected $_anchorText;

    /**
     * Url finder for category
     *
     * @var UrlFinderInterface
     */
    protected $urlFinder;

    /**
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param UrlFinderInterface $urlFinder
     * @param \Magento\Catalog\Model\ResourceModel\AbstractResource $entityResource
     * @param array $data
     */
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        UrlFinderInterface $urlFinder,
        \Magento\Catalog\Model\ResourceModel\AbstractResource $entityResource = null,
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->urlFinder = $urlFinder;
        $this->_entityResource = $entityResource;
    }

    /**
     * Prepare url using passed id path and return it
     * or return false if path was not found in url rewrites.
     *
     * @throws \RuntimeException
     * @return string|false
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function getHref()
    {
        if ($this->_href === null) {
            if (!$this->getData('id_path')) {
                throw new \RuntimeException('Parameter id_path is not set.');
            }
            $rewriteData = $this->parseIdPath($this->getData('id_path'));

            $href = false;
            $store = $this->hasStoreId() ? $this->_storeManager->getStore($this->getStoreId())
                : $this->_storeManager->getStore();
            $filterData = [
                UrlRewrite::ENTITY_ID => $rewriteData[1],
                UrlRewrite::ENTITY_TYPE => $rewriteData[0],
                UrlRewrite::STORE_ID => $store->getId(),
            ];
            if (!empty($rewriteData[2]) && $rewriteData[0] == ProductUrlRewriteGenerator::ENTITY_TYPE) {
                $filterData[UrlRewrite::METADATA]['category_id'] = $rewriteData[2];
            }
            $rewrite = $this->urlFinder->findOneByData($filterData);

            if ($rewrite) {
                $href = $store->getUrl('', ['_direct' => $rewrite->getRequestPath()]);

                if (strpos($href, '___store') === false) {
                    //$href .= (strpos($href, '?') === false ? '?' : '&') . '___store=' . $store->getCode();
                }
            }
            $this->_href = $href;
        }
        return $this->_href;
    }

    /**
     * Parse id_path
     *
     * @param string $idPath
     * @throws \RuntimeException
     * @return array
     */
    protected function parseIdPath($idPath)
    {
        $rewriteData = explode('/', $idPath);

        if (!isset($rewriteData[0]) || !isset($rewriteData[1])) {
            throw new \RuntimeException('Wrong id_path structure.');
        }
        return $rewriteData;
    }

    /**
     * Prepare label using passed text as parameter.
     * If anchor text was not specified get entity name from DB.
     *
     * @return string
     */
    public function getLabel()
    {
        if (!$this->_anchorText) {
            if ($this->getData('anchor_text')) {
                $this->_anchorText = $this->getData('anchor_text');
            } elseif ($this->_entityResource) {
                $idPath = explode('/', $this->_getData('id_path'));
                if (isset($idPath[1])) {
                    $id = $idPath[1];
                    if ($id) {
                        $this->_anchorText = $this->_entityResource->getAttributeRawValue(
                            $id,
                            'name',
                            $this->_storeManager->getStore()
                        );
                    }
                }
            }
        }

        return $this->_anchorText;
    }

    /**
     * Render block HTML
     * or return empty string if url can't be prepared
     *
     * @return string
     */
    protected function _toHtml()
    {
        if ($this->getHref()) {
            return parent::_toHtml();
        }
        return 'err';
    }
}

Thanks for taking the time to read and any guidance you can offer.

Best Answer

I think you should try to replace \Magento\Catalog\Model\ResourceModel\AbstractResource with \Magento\Catalog\Model\ResourceModel\Category in your constructor, then bin/magento setup:upgrade

Also, the = null in your __construct() is mainly used for compatibility reasons with older versions of the same module.

If you need to do this, you should

  1. either add a fallback method in that same class. You would not request the property directly, but call sth like getEntityResource(), which reads sth like: if $this->_entityResource is null, use ObjectManager directly as a fallback and created an object of the desired class. IIRC there are some examples of this in the core, I just can't find any at the moment.

  2. load the correct class directly via the ObjectManager inside the constructor if dependency injection fails. An example for this is Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder.

This ensures that $this->_entityResource is never null.

UPDATE

Also, why don't you simply inherit from \Magento\Catalog\Block\Widget\Link instead of the more general \Magento\Framework\View\Element\Html\Link?

Related Topic