7

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.

Raphael at Digital Pianism
70.8k37 gold badges192 silver badges357 bronze badges
asked Jun 21, 2016 at 15:26

4 Answers 4

3

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.

answered Jun 22, 2016 at 13:35
2
  • Just straight up parsing the compiled layout.xml files, interesting. You can't really render the blocks this way though, right? Commented Jun 16, 2017 at 19:02
  • Hello @jzahedieh i am gettnig some of the links not getting all links... and also iwant aurguments passed to the each account link ... how i can do this need help Commented Jan 10, 2020 at 5:41
3

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.

answered Sep 22, 2018 at 7:54
1

I'm pretty sure that instead of using the emulation system you should use the area code.

I reckon you should try adding the following to your code:

$appState->setAreaCode(\Magento\Framework\App\Area::AREA_FRONTEND);

The setAreaCode method actually changes the current scope so that might help:

public function setAreaCode($code)
{
 if (isset($this->_areaCode)) {
 throw new \Magento\Framework\Exception\LocalizedException(
 new \Magento\Framework\Phrase('Area code is already set')
 );
 }
 $this->_configScope->setCurrentScope($code);
 $this->_areaCode = $code;
}
answered Jun 21, 2016 at 15:55
1
  • I tried this initially but because the area code is already set the Exception Area code is already set is thrown which is why I went down the emulation route, the appState is somehow a singleton I think but can't quite out figure how. Commented Jun 21, 2016 at 16:03
0

everything was easier

class customerLinks
{
 /**
 * @var \Magento\Framework\App\State
 */
 protected $appState;
 /**
 * @var \Magento\Framework\View\LayoutFactory
 */
 protected $layoutFactory;
 public function __construct(
 \Magento\Framework\App\State $appState,
 \Magento\Framework\View\LayoutFactory $layoutFactory
 ) {
 $this->appState = $appState;
 $this->layoutFactory = $layoutFactory;
 }
 public function getLinks()
 {
 $links = [];
 $this->appState->emulateAreaCode(\Magento\Framework\App\Area::AREA_FRONTEND, function () use (&$links) {
 $layout = $this->layoutFactory->create();
 $layout->getUpdate()->addHandle('2columns-left');
 $layout->getUpdate()->addHandle('customer_account');
 $layout->getUpdate()->load();
 $layout->generateXml();
 $layout->generateElements();
 $blocks = $layout->getChildBlocks('customer_account_navigation');
 foreach ($blocks as $linkBlock) {
 $links[$linkBlock->getNameInLayout()] = $linkBlock->getLabel();
 }
 });
 return $links;
 }
}
answered Sep 26, 2022 at 22:59

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.