Magento 2.2.4 Core Bug: Saving Store-View Design Configuration Changes

Magento 2.2.4 Core Bug: Saving Store-View Design Configuration Changes

16 min read

Send to you:

Magento® released its update to 2.2.4, and a problem has been identified on Magento sites. When administrators tried to edit any design configuration parameter (Content/Design/Configuration), changes could not be saved, and administrators were shown the following error: “Something went wrong while saving this configuration: Area is already set.” We had to identify the reason for this error and fix it.

Let’s consider the entire sequential process of solving this problem.

Problem: Error When Editing Design Configuration for Store View

We checked the Magento log to examine the error, where it was recorded in the following form:

[2018-05-14 09:20:16] main.CRITICAL: Exception message: Area is already set Trace: #0 /var/www/zenzii_beta/vendor/magento/module-theme/Model/Design/Config/Validator.php(117): Magento\Email\Model\AbstractTemplate->setForcedArea('design_email_he...') #1 /var/www/zenzii_beta/vendor/magento/module-theme/Model/Design/Config/Validator.php(68): Magento\Theme\Model\Design\Config\Validator->getTemplateText('design_email_he...', Object(Magento\Theme\Model\Data\Design\Config)) #2 /var/www/zenzii_beta/vendor/magento/module-theme/Model/DesignConfigRepository.php(91): Magento\Theme\Model\Design\Config\Validator->validate(Object(Magento\Theme\Model\Data\Design\Config)) #3 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(58): Magento\Theme\Model\DesignConfigRepository->save(Object(Magento\Theme\Model\Data\Design\Config)) #4 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(138): Magento\Theme\Model\DesignConfigRepository\Interceptor->___callParent('save', Array) #5 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(153): Magento\Theme\Model\DesignConfigRepository\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Theme\Model\Data\Design\Config)) #6 /var/www/zenzii_beta/generated/code/Magento/Theme/Model/DesignConfigRepository/Interceptor.php(26): Magento\Theme\Model\DesignConfigRepository\Interceptor->___callPlugins('save', Array, Array) #7 /var/www/zenzii_beta/vendor/magento/module-theme/Controller/Adminhtml/Design/Config/Save.php(75): Magento\Theme\Model\DesignConfigRepository\Interceptor->save(Object(Magento\Theme\Model\Data\Design\Config)) #8 /var/www/zenzii_beta/vendor/magento/framework/App/Action/Action.php(107): Magento\Theme\Controller\Adminhtml\Design\Config\Save->execute() #9 /var/www/zenzii_beta/vendor/magento/module-backend/App/AbstractAction.php(229): Magento\Framework\App\Action\Action->dispatch(Object(Magento\Framework\App\Request\Http)) #10 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(58): Magento\Backend\App\AbstractAction->dispatch(Object(Magento\Framework\App\Request\Http)) #11 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(138): Magento\Theme\Controller\Adminhtml\Design\Config\Save\Interceptor->___callParent('dispatch', Array) #12 /var/www/zenzii_beta/vendor/magento/module-backend/App/Action/Plugin/Authentication.php(143): Magento\Theme\Controller\Adminhtml\Design\Config\Save\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http)) #13 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(135): Magento\Backend\App\Action\Plugin\Authentication->aroundDispatch(Object(Magento\Theme\Controller\Adminhtml\Design\Config\Save\Interceptor), Object(Closure), Object(Magento\Framework\App\Request\Http)) #14 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(153): Magento\Theme\Controller\Adminhtml\Design\Config\Save\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http)) #15 /var/www/zenzii_beta/generated/code/Magento/Theme/Controller/Adminhtml/Design/Config/Save/Interceptor.php(26): Magento\Theme\Controller\Adminhtml\Design\Config\Save\Interceptor->___callPlugins('dispatch', Array, NULL) #16 /var/www/zenzii_beta/vendor/magento/framework/App/FrontController.php(55): Magento\Theme\Controller\Adminhtml\Design\Config\Save\Interceptor->dispatch(Object(Magento\Framework\App\Request\Http)) #17 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(58): Magento\Framework\App\FrontController->dispatch(Object(Magento\Framework\App\Request\Http)) #18 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(138): Magento\Framework\App\FrontController\Interceptor->___callParent('dispatch', Array) #19 /var/www/zenzii_beta/vendor/magento/framework/Interception/Interceptor.php(153): Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http)) #20 /var/www/zenzii_beta/generated/code/Magento/Framework/App/FrontController/Interceptor.php(26): Magento\Framework\App\FrontController\Interceptor->___callPlugins('dispatch', Array, Array) #21 /var/www/zenzii_beta/vendor/magento/framework/App/Http.php(135): Magento\Framework\App\FrontController\Interceptor->dispatch(Object(Magento\Framework\App\Request\Http)) #22 /var/www/zenzii_beta/vendor/magento/framework/App/Bootstrap.php(256): Magento\Framework\App\Http->launch() #23 /var/www/zenzii_beta/index.php(39): Magento\Framework\App\Bootstrap->run(Object(Magento\Framework\App\Http\Interceptor)) #24 {main} [] []

It was found that this error occurred in the Magento\Theme\Model\Design\Config\Validator class in the getTemplateText method.

Let’s look at this method:

private function getTemplateText($templateId,
DesignConfigInterface $designConfig)
{

   //
Load template object by configured template id

   $template = $this->templateFactory->create();

   $template->emulateDesign($this->getScopeId($designConfig));

   if (is_numeric($templateId)) {

       $template->load($templateId);

   } else {

       $template->setForcedArea($templateId);

       $template->loadDefault($templateId);

   }

   $text = $template->getTemplateText();

   $template->revertDesign();

   return $text;
}

The following line causes the error:

$template->setForcedArea($templateId)

We analyzed the cause. The $template is the object of Magento\Email\Model\Template class. The setForcedArea method is specified in its parent Magento\Email\Model\AbstractTemplate and looks like as follows:

public function setForcedArea($templateId)
{

   if ($this->area) {

       throw new \LogicException(__('Area is already set'));

   }

   $this->area = $this->emailConfig->getTemplateArea($templateId);

   return $this;
}

As you can see, the method throws an exception if the class variable “area” already has a value at the time of the call. It remains for us to find out which method it was set in. Based on the code of the getTemplateText method, we know that it could be either a constructor or the emulateDesign method.

Let’s analyze the code of the latter:

/**

* Get design configuration data

*

* @return DataObject

*/
public function getDesignConfig()
{
 
   if ($this->designConfig === null) {

       if ($this->area === null) {

           $this->area = $this->design->getArea();

       }

       if ($this->store === null) {

           $this->store = $this->storeManager->getStore()->getId();

       }

       $this->designConfig = new DataObject( ['area' => $this->area, 'store' => $this->store] );

   }

   return $this->designConfig;
}
/**

* Initialize design information for template processing

*

* @param array $config

* @return $this

* @throws LocalizedException

*/
public function setDesignConfig(array $config)
{

   if (!isset($config['area']) || !isset($config['store'])) {

       throw new LocalizedException(__('Design config must have area and store.'));

   }

   $this->getDesignConfig()->setData($config);

   return $this;
}

public function emulateDesign($storeId, $area = self::DEFAULT_DESIGN_AREA)
{

   if ($storeId !== null && $storeId !== false) {

       // save current design settings

       $this->emulatedDesignConfig = clone $this->getDesignConfig();

       if ($this->getDesignConfig()->getStore() != $storeId || $this->getDesignConfig()->getArea() != $area ) {

           $this->setDesignConfig(['area' => $area, 'store' => $storeId]);

           $this->applyDesignConfig();

       }

   } else {

       $this->emulatedDesignConfig = false;

   }
}

As you can see, emulateDesign is always called with the parameter area = “frontend” and at the same time, it calls the getDesignConfig method, which sets the “area” variable, leading to an error in setForcedArea. We can only guess why Magento developers added the call of the setForcedArea method in version 2.2.4. In version 2.2.3, this method was not called in the design configuration class validator.

Solution Using the Customization of the Magento\Theme\Model\Design\Config\Validator Class

Two problems arose when trying to solve the problem with the getTemplateText method code. First, the getTemplateText method of the Magento\Theme\Model\Design\Config\Validator class is private. This means that it’s not possible to rewrite it using the “around” plugin. Furthermore, the Magento\Theme\Model\Design\Config\Validator class contains only one public method besides the constructor, and the rest are private.

Secondly, the object of the class is initialized in the Magento\Theme\Model\DesignConfigRepository class using the object manager in a private method. If it was passed as a parameter to the constructor, then the class would be replaced at the constructor level using the dependency injection. But because it cannot be redefined that way, all that’s left is to completely rewrite the class and replace it using “preference.” This is an extremely undesirable method for customizing Magento 2, but in this case, due to the above-mentioned reasons, this is the only possible method.

That’s why we redefine the Magento\Theme\Model\Design\Config\Validator class in di.xml and implement it:

<preference
for="Magento\Theme\Model\Design\Config\Validator"
type="Web4pro\All\Model\Design\Config\Validator"/>
class Validator extends \Magento\Theme\Model\Design\Config\Validator
{

   /**

    * @var string[]

    */

   protected $fields = [];

   /**

    * @var TemplateFactory

    */

   protected $templateFactory;

   /**

    * Initialize dependencies.

    *

    * @param TemplateFactory $templateFactory

    * @param string[] $fields

    */

   public function __construct(TemplateFactory $templateFactory, $fields = []) 
   {

       $this->templateFactory = $templateFactory;
 
       $this->fields = $fields;

       parent::__construct($templateFactory,$fields);

   }

   /**

    * Validate if design configuration has recursive references

    *

    * @param DesignConfigInterface $designConfig

    *

    * @throws LocalizedException 

    * @return void

    */

   public function validate(DesignConfigInterface $designConfig)

   {

       /** 
       
       * @var DesignConfigDataInterface[] $designConfigData
        */

       $designConfigData = $designConfig->getExtensionAttributes()->getDesignConfigData();

       $elements = [];

       foreach ($designConfigData as $designElement) {

           if (!in_array($designElement->getFieldConfig()['field'], $this->fields)) {

               continue;

           }

           /* Save mapping between field names and config paths */

           $elements[$designElement->getFieldConfig()['field']] = [                'config_path' => $designElement->getPath(), 'value' => $designElement->getValue() ];

       }

       foreach ($elements as $name => $data) {

           $templateId = $data['value'];

           $text = $this->getTemplateText($templateId, $designConfig);

           // Check if template body has a reference to the same config path

           if (preg_match_all(Template::CONSTRUCTION_TEMPLATE_PATTERN, $text, $constructions, PREG_SET_ORDER)) {

               foreach ($constructions as $construction) {

                   $configPath = isset($construction[2]) ? $construction[2] : '';

                   $params = $this->getParameters($configPath);

                   if (isset($params['config_path']) && $params['config_path'] == $data['config_path']) {

                       throw new LocalizedException( __( "The %templateName contains an incorrect configuration. The template has " . "a reference to itself. Either remove or change the reference.", ["templateName" => $name]

                           )

                       );

                   };

               }

           }

       }

   }

   /**

    * Returns store identifier if is store scope

    *

    * @param DesignConfigInterface $designConfig

    * @return string|bool

    */

   protected function getScopeId(DesignConfigInterface $designConfig)

   {

       if ($designConfig->getScope() == 'stores') {

           return $designConfig->getScopeId();

       }

       return false;

   }

   /**

    * Load template text in configured scope

    *

    * @param integer|string $templateId

    * @param DesignConfigInterface $designConfig

    * @return string

    */

   protected function getTemplateText($templateId, DesignConfigInterface $designConfig) 
   {

       // Load template object by configured template id

       $template = $this->templateFactory->create();

       $template->emulateDesign($this->getScopeId($designConfig));

       if (is_numeric($templateId)) {

           $template->load($templateId);

       } else {

           $template->loadDefault($templateId);

       }

       $text = $template->getTemplateText();

       $template->revertDesign();

       return $text;

   }

   /**

    * Return associative array of parameters.

    *

    * @param string $value raw parameters

    * @return array

    */

   protected function getParameters($value) 
   {

       $tokenizer = new ParameterTokenizer();

       $tokenizer->setString($value);

       $params = $tokenizer->tokenize();

       return $params;

   }
}

After that customization, it was possible to successfully save the design configuration. It was far from ideal to have to completely redefine all the methods just to remove one line that contains errors, simply because Magento made them private. In turn, our developer made the methods “protected.” If someone has to fix something in the validator class, then they will be able to inherit it from its class by creating a new class.

SUM MARY

After the appearance and elimination of errors, we learned that if Magento developers just declared these methods as "protected," we shouldn't rewrite the entire class. All we had to do is to fix the method that caused the problems. At the very least, identifying and troubleshooting in Magento lets you think outside the box and come up with clever ways to eliminate bugs. If you need help with fixing your store issues, we will be glad to provide you with Magento Support.

Posted on: July 20, 2018

4.0/5.0

Article rating (1 Reviews)

Do you find this article useful? Please, let us know your opinion and rate the post!

  • Not bad
  • Good
  • Very Good
  • Great
  • Awesome