Magento 2 - Retriving frontend layout blocks from adminhtml

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.


I bumped into this issue as well (trying to render a frontend block in the backend), with a resulting Required parameter 'theme_dir' was not passed error in my page. After diving into this, I found out that the emulateAreaCode approach should be working (and is working fine with non-theming related stuff). But with theming, the layout is already instantiated for the backend. Additionally, the configuration is also attached to the layout, meaning that the theme_dir variable I was missing was actually not needed because we are in the backend.

The solution seemed simple: Reset the configuration somehow so that the theme_dir (a numeric ID referencing the right database row, for instance with the Luma theme in it) was added to the layout.

The layout is initialized through the theme. The theme is initialized through a design. And the design has a method setDesignTheme() which could be used.

See my code sample below: $this->design is an instance of \Magento\Framework\View\DesignInterface. THe other variables are quite common and should speak for themselves.

$store = $this->storeRepository->getById($this->storeId);
$layoutFactory = $this->layoutFactory;

$themeId = $this->scopeConfig- >getValue(DesignInterface::XML_PATH_THEME_ID, 'store', $store);
$this->design->setDesignTheme($themeId);

$alertGrid = $this->appState->emulateAreaCode(
    Area::AREA_FRONTEND,
    function () use ($store, $layoutFactory) {
        $layout = $layoutFactory->create();
        $block = $layout->createBlock(MyBlock::class);
        return $block->toHtml();
    }
);

Perhaps this helps you guys as well.