TransWikia.com

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

Magento Asked by rorytec on January 10, 2021

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.

codeVendorCategoryWidgetetcwidget.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="VendorCategoryWidgetBlockWidgetLink" 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="MagentoCatalogBlockAdminhtmlCategoryWidgetChooser">
                    <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).

codeVendorCategoryWidgetviewfrontendtemplateswidgetcategory_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?

codeVendorCategoryWidgetBlockWidgetLink.php

    <?php
namespace VendorCategoryWidgetBlockWidget;

use MagentoCatalogUrlRewriteModelProductUrlRewriteGenerator;
use MagentoUrlRewriteModelUrlFinderInterface;
use MagentoUrlRewriteServiceV1DataUrlRewrite;

class Link extends MagentoFrameworkViewElementHtmlLink implements MagentoWidgetBlockBlockInterface
{
    /**
     * Entity model name which must be used to retrieve entity specific data.
     * @var null|MagentoCatalogModelResourceModelAbstractResource
     */
    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 MagentoFrameworkViewElementTemplateContext $context
     * @param UrlFinderInterface $urlFinder
     * @param MagentoCatalogModelResourceModelAbstractResource $entityResource
     * @param array $data
     */
    public function __construct(
        MagentoFrameworkViewElementTemplateContext $context,
        UrlFinderInterface $urlFinder,
        MagentoCatalogModelResourceModelAbstractResource $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.

One Answer

I think you should try to replace MagentoCatalogModelResourceModelAbstractResource with MagentoCatalogModelResourceModelCategory 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 MagentoCatalogModelIndexerProductFlatTableBuilder.

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

UPDATE

Also, why don't you simply inherit from MagentoCatalogBlockWidgetLink instead of the more general MagentoFrameworkViewElementHtmlLink?

Answered by simonthesorcerer on January 10, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP