Magento 2 – Retrieving Frontend Layout Blocks from Adminhtml

adminhtmlarealayoutmagento2scope

Problem

I need to gain access to the children blocks from the parent block alias of customer_account_navigation from the frontend layout to be used in the adminhtml system configuration.

Current Attempt

As the code is being run in the admin area (adminhtml) I assume emulation is required in order to retrieve anything from the frontend, I have used the $appState->emulateAreaCode method which has a call back and the initialised the ObjectManager as frontend.

class AccountLinks implements \Magento\Framework\Option\ArrayInterface
{
    protected $_options = [];

    /**
     * @var \Magento\Framework\ObjectManagerInterface
     */
    protected $_objectManager;

    /**
     * @var \Magento\Framework\ObjectManager\ConfigLoaderInterface
     */
    protected $_configLoader;

    public function __construct(
        \Magento\Framework\ObjectManagerInterface $objectManager,
        \Magento\Framework\ObjectManager\ConfigLoaderInterface $configLoader
    )
    {
        $this->_objectManager = $objectManager;
        $this->_configLoader = $configLoader;
    }

    /**
     * Retrieve my account menu links.
     *
     * @return array
     */
    public function toOptionArray()
    {
        if (!$this->_options) {
            /* @var $appState \Magento\Framework\App\State */
            $appState = $this->_objectManager->get('Magento\Framework\App\State');

            $appState->emulateAreaCode('frontend', [$this, 'emulateDesignCallback']);
        }

        return $this->_options;
    }

    public function emulateDesignCallback()
    {
        $this->_objectManager->configure($this->_configLoader->load('frontend'));

        /* @var $layout \Magento\Framework\View\Layout */
        $layout = $this->_objectManager->create('\Magento\Framework\View\LayoutInterface');

        $layout->getUpdate()->addHandle('default');
        $layout->getUpdate()->addHandle('customer_account_index');

        $layout->getUpdate()->load();
        $layout->generateXml();
        $layout->generateElements();

        $blocks = $layout->getChildBlocks('customer_account_navigation');

        return $this;
    }
}

Debugging

I can see 2 frontend blocks which seems to imply that using addHandle is not working correctly to generate the correct blocks.

I am also unsure if I am loading the layout after setting the handles correctly.

The _configScope is incorrectly saying it's current scope is adminhtml even with the emulation, I wonder if this is causing the problem but I can't work out how to set it

Debugging

Any ideas? Thanks.

Best Answer

I have a solution, whether it is a good one I am not sure about but it does work. Disadvantages is that you have to have the Magento/blank theme installed and uses xpath so if Magento change their layout declaration it might break, both quite unlikely scenarios.

Here is my final solution:

class AccountLinks implements \Magento\Framework\Option\ArrayInterface
{
    /**
     * @var array
     */
    protected $_options = [];

    /**
     * @var \Magento\Framework\View\Layout\ProcessorFactory
     */
    protected $_layoutProcessorFactory;

    /**
     * @var \Magento\Theme\Model\ResourceModel\Theme\CollectionFactory
     */
    protected $_themesFactory;

    /**
     * @param \Magento\Framework\View\Layout\ProcessorFactory $_layoutProcessorFactory
     * @param \Magento\Theme\Model\ResourceModel\Theme\CollectionFactory $_themesFactory
     */
    public function __construct(
        \Magento\Framework\View\Layout\ProcessorFactory $_layoutProcessorFactory,
        \Magento\Theme\Model\ResourceModel\Theme\CollectionFactory $_themesFactory
    ) {
        $this->_layoutProcessorFactory = $_layoutProcessorFactory;
        $this->_themesFactory = $_themesFactory;
    }

    /**
     * Build options array
     *
     * @return array
     */
    public function toOptionArray()
    {
        if (!$this->_options) {

            foreach ($this->getLinksFromLayout() as $link) {
                /* @var $link \Magento\Framework\View\Layout\Element */
                $name = $link->asCanonicalArray();

                $this->_options[] = [
                    'value' => $name,
                    'label' => $name
                ];
            }
        }

        return $this->_options;
    }

    /**
     * Retrieve my account menu links.
     *
     * @return \SimpleXMLElement[]
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getLinksFromLayout()
    {
        $themeCollection = $this->_themesFactory->create();
        $theme = $themeCollection->getItemByColumnValue('code', 'Magento/blank');

        /* @var $layoutProcessor \Magento\Framework\View\Model\Layout\Merge */
        $layoutProcessor = $this->_layoutProcessorFactory->create(['theme' => $theme]);

        $layoutProcessor->addHandle('customer_account_index');
        $layoutProcessor->load();

        $layoutProcessorXML = $layoutProcessor->asSimplexml();

        $result = $layoutProcessorXML->xpath('//body/referenceBlock[@name="customer_account_navigation"]/block/@name');

        return $result;
    }

}

It doesn't use appState emulation, I would be interested in seeing an example that involves using it to access layout as I have bypassed a ton of abstraction in order to get a working solution but I tried many things and couldn't get it to register the frontend handle and return the child blocks.

Related Topic