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.

Summary

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.

4.00 / 5.0
Article rating (1 Reviews)
Do you find this article useful? Please, let us know your opinion and rate the post!
Not badGoodVery GoodGreatAwesome