Config.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Config\Model;
  7. use Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker;
  8. use Magento\Config\Model\Config\Structure\Element\Group;
  9. use Magento\Config\Model\Config\Structure\Element\Field;
  10. use Magento\Framework\App\ObjectManager;
  11. use Magento\Framework\App\ScopeInterface;
  12. use Magento\Framework\App\ScopeResolverPool;
  13. use Magento\Store\Model\ScopeInterface as StoreScopeInterface;
  14. use Magento\Store\Model\ScopeTypeNormalizer;
  15. /**
  16. * Backend config model
  17. *
  18. * Used to save configuration
  19. *
  20. * @author Magento Core Team <core@magentocommerce.com>
  21. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  22. * @api
  23. * @since 100.0.2
  24. * @method string getSection()
  25. * @method void setSection(string $section)
  26. * @method string getWebsite()
  27. * @method void setWebsite(string $website)
  28. * @method string getStore()
  29. * @method void setStore(string $store)
  30. * @method string getScope()
  31. * @method void setScope(string $scope)
  32. * @method int getScopeId()
  33. * @method void setScopeId(int $scopeId)
  34. * @method string getScopeCode()
  35. * @method void setScopeCode(string $scopeCode)
  36. */
  37. class Config extends \Magento\Framework\DataObject
  38. {
  39. /**
  40. * Config data for sections
  41. *
  42. * @var array
  43. */
  44. protected $_configData;
  45. /**
  46. * Event dispatcher
  47. *
  48. * @var \Magento\Framework\Event\ManagerInterface
  49. */
  50. protected $_eventManager;
  51. /**
  52. * System configuration structure
  53. *
  54. * @var \Magento\Config\Model\Config\Structure
  55. */
  56. protected $_configStructure;
  57. /**
  58. * Application config
  59. *
  60. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  61. */
  62. protected $_appConfig;
  63. /**
  64. * Global factory
  65. *
  66. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  67. */
  68. protected $_objectFactory;
  69. /**
  70. * TransactionFactory
  71. *
  72. * @var \Magento\Framework\DB\TransactionFactory
  73. */
  74. protected $_transactionFactory;
  75. /**
  76. * Config data loader
  77. *
  78. * @var \Magento\Config\Model\Config\Loader
  79. */
  80. protected $_configLoader;
  81. /**
  82. * Config data factory
  83. *
  84. * @var \Magento\Framework\App\Config\ValueFactory
  85. */
  86. protected $_configValueFactory;
  87. /**
  88. * @var \Magento\Store\Model\StoreManagerInterface
  89. */
  90. protected $_storeManager;
  91. /**
  92. * @var Config\Reader\Source\Deployed\SettingChecker
  93. */
  94. private $settingChecker;
  95. /**
  96. * @var ScopeResolverPool
  97. */
  98. private $scopeResolverPool;
  99. /**
  100. * @var ScopeTypeNormalizer
  101. */
  102. private $scopeTypeNormalizer;
  103. /**
  104. * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config
  105. * @param \Magento\Framework\Event\ManagerInterface $eventManager
  106. * @param \Magento\Config\Model\Config\Structure $configStructure
  107. * @param \Magento\Framework\DB\TransactionFactory $transactionFactory
  108. * @param \Magento\Config\Model\Config\Loader $configLoader
  109. * @param \Magento\Framework\App\Config\ValueFactory $configValueFactory
  110. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  111. * @param Config\Reader\Source\Deployed\SettingChecker|null $settingChecker
  112. * @param array $data
  113. * @param ScopeResolverPool|null $scopeResolverPool
  114. * @param ScopeTypeNormalizer|null $scopeTypeNormalizer
  115. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  116. */
  117. public function __construct(
  118. \Magento\Framework\App\Config\ReinitableConfigInterface $config,
  119. \Magento\Framework\Event\ManagerInterface $eventManager,
  120. \Magento\Config\Model\Config\Structure $configStructure,
  121. \Magento\Framework\DB\TransactionFactory $transactionFactory,
  122. \Magento\Config\Model\Config\Loader $configLoader,
  123. \Magento\Framework\App\Config\ValueFactory $configValueFactory,
  124. \Magento\Store\Model\StoreManagerInterface $storeManager,
  125. SettingChecker $settingChecker = null,
  126. array $data = [],
  127. ScopeResolverPool $scopeResolverPool = null,
  128. ScopeTypeNormalizer $scopeTypeNormalizer = null
  129. ) {
  130. parent::__construct($data);
  131. $this->_eventManager = $eventManager;
  132. $this->_configStructure = $configStructure;
  133. $this->_transactionFactory = $transactionFactory;
  134. $this->_appConfig = $config;
  135. $this->_configLoader = $configLoader;
  136. $this->_configValueFactory = $configValueFactory;
  137. $this->_storeManager = $storeManager;
  138. $this->settingChecker = $settingChecker
  139. ?? ObjectManager::getInstance()->get(SettingChecker::class);
  140. $this->scopeResolverPool = $scopeResolverPool
  141. ?? ObjectManager::getInstance()->get(ScopeResolverPool::class);
  142. $this->scopeTypeNormalizer = $scopeTypeNormalizer
  143. ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class);
  144. }
  145. /**
  146. * Save config section
  147. *
  148. * Require set: section, website, store and groups
  149. *
  150. * @throws \Exception
  151. * @return $this
  152. */
  153. public function save()
  154. {
  155. $this->initScope();
  156. $sectionId = $this->getSection();
  157. $groups = $this->getGroups();
  158. if (empty($groups)) {
  159. return $this;
  160. }
  161. $oldConfig = $this->_getConfig(true);
  162. /** @var \Magento\Framework\DB\Transaction $deleteTransaction */
  163. $deleteTransaction = $this->_transactionFactory->create();
  164. /** @var \Magento\Framework\DB\Transaction $saveTransaction */
  165. $saveTransaction = $this->_transactionFactory->create();
  166. $changedPaths = [];
  167. // Extends for old config data
  168. $extraOldGroups = [];
  169. foreach ($groups as $groupId => $groupData) {
  170. $this->_processGroup(
  171. $groupId,
  172. $groupData,
  173. $groups,
  174. $sectionId,
  175. $extraOldGroups,
  176. $oldConfig,
  177. $saveTransaction,
  178. $deleteTransaction
  179. );
  180. $groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups);
  181. $changedPaths = \array_merge($changedPaths, $groupChangedPaths);
  182. }
  183. try {
  184. $deleteTransaction->delete();
  185. $saveTransaction->save();
  186. // re-init configuration
  187. $this->_appConfig->reinit();
  188. // website and store codes can be used in event implementation, so set them as well
  189. $this->_eventManager->dispatch(
  190. "admin_system_config_changed_section_{$this->getSection()}",
  191. [
  192. 'website' => $this->getWebsite(),
  193. 'store' => $this->getStore(),
  194. 'changed_paths' => $changedPaths,
  195. ]
  196. );
  197. } catch (\Exception $e) {
  198. // re-init configuration
  199. $this->_appConfig->reinit();
  200. throw $e;
  201. }
  202. return $this;
  203. }
  204. /**
  205. * Map field name if they were cloned
  206. *
  207. * @param Group $group
  208. * @param string $fieldId
  209. * @return string
  210. */
  211. private function getOriginalFieldId(Group $group, string $fieldId): string
  212. {
  213. if ($group->shouldCloneFields()) {
  214. $cloneModel = $group->getCloneModel();
  215. /** @var \Magento\Config\Model\Config\Structure\Element\Field $field */
  216. foreach ($group->getChildren() as $field) {
  217. foreach ($cloneModel->getPrefixes() as $prefix) {
  218. if ($prefix['field'] . $field->getId() === $fieldId) {
  219. $fieldId = $field->getId();
  220. break(2);
  221. }
  222. }
  223. }
  224. }
  225. return $fieldId;
  226. }
  227. /**
  228. * Get field object
  229. *
  230. * @param string $sectionId
  231. * @param string $groupId
  232. * @param string $fieldId
  233. * @return Field
  234. */
  235. private function getField(string $sectionId, string $groupId, string $fieldId): Field
  236. {
  237. /** @var \Magento\Config\Model\Config\Structure\Element\Group $group */
  238. $group = $this->_configStructure->getElement($sectionId . '/' . $groupId);
  239. $fieldPath = $group->getPath() . '/' . $this->getOriginalFieldId($group, $fieldId);
  240. $field = $this->_configStructure->getElement($fieldPath);
  241. return $field;
  242. }
  243. /**
  244. * Get field path
  245. *
  246. * @param Field $field
  247. * @param string $fieldId Need for support of clone_field feature
  248. * @param array &$oldConfig Need for compatibility with _processGroup()
  249. * @param array &$extraOldGroups Need for compatibility with _processGroup()
  250. * @return string
  251. */
  252. private function getFieldPath(Field $field, string $fieldId, array &$oldConfig, array &$extraOldGroups): string
  253. {
  254. $path = $field->getGroupPath() . '/' . $fieldId;
  255. /**
  256. * Look for custom defined field path
  257. */
  258. $configPath = $field->getConfigPath();
  259. if ($configPath && strrpos($configPath, '/') > 0) {
  260. // Extend old data with specified section group
  261. $configGroupPath = substr($configPath, 0, strrpos($configPath, '/'));
  262. if (!isset($extraOldGroups[$configGroupPath])) {
  263. $oldConfig = $this->extendConfig($configGroupPath, true, $oldConfig);
  264. $extraOldGroups[$configGroupPath] = true;
  265. }
  266. $path = $configPath;
  267. }
  268. return $path;
  269. }
  270. /**
  271. * Check is config value changed
  272. *
  273. * @param array $oldConfig
  274. * @param string $path
  275. * @param array $fieldData
  276. * @return bool
  277. */
  278. private function isValueChanged(array $oldConfig, string $path, array $fieldData): bool
  279. {
  280. if (isset($oldConfig[$path]['value'])) {
  281. $result = !isset($fieldData['value']) || $oldConfig[$path]['value'] !== $fieldData['value'];
  282. } else {
  283. $result = empty($fieldData['inherit']);
  284. }
  285. return $result;
  286. }
  287. /**
  288. * Get changed paths
  289. *
  290. * @param string $sectionId
  291. * @param string $groupId
  292. * @param array $groupData
  293. * @param array &$oldConfig
  294. * @param array &$extraOldGroups
  295. * @return array
  296. */
  297. private function getChangedPaths(
  298. string $sectionId,
  299. string $groupId,
  300. array $groupData,
  301. array &$oldConfig,
  302. array &$extraOldGroups
  303. ): array {
  304. $changedPaths = [];
  305. if (isset($groupData['fields'])) {
  306. foreach ($groupData['fields'] as $fieldId => $fieldData) {
  307. $field = $this->getField($sectionId, $groupId, $fieldId);
  308. $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups);
  309. if ($this->isValueChanged($oldConfig, $path, $fieldData)) {
  310. $changedPaths[] = $path;
  311. }
  312. }
  313. }
  314. if (isset($groupData['groups'])) {
  315. $subSectionId = $sectionId . '/' . $groupId;
  316. foreach ($groupData['groups'] as $subGroupId => $subGroupData) {
  317. $subGroupChangedPaths = $this->getChangedPaths(
  318. $subSectionId,
  319. $subGroupId,
  320. $subGroupData,
  321. $oldConfig,
  322. $extraOldGroups
  323. );
  324. $changedPaths = \array_merge($changedPaths, $subGroupChangedPaths);
  325. }
  326. }
  327. return $changedPaths;
  328. }
  329. /**
  330. * Process group data
  331. *
  332. * @param string $groupId
  333. * @param array $groupData
  334. * @param array $groups
  335. * @param string $sectionPath
  336. * @param array &$extraOldGroups
  337. * @param array &$oldConfig
  338. * @param \Magento\Framework\DB\Transaction $saveTransaction
  339. * @param \Magento\Framework\DB\Transaction $deleteTransaction
  340. * @return void
  341. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  342. * @SuppressWarnings(PHPMD.NPathComplexity)
  343. */
  344. protected function _processGroup(
  345. $groupId,
  346. array $groupData,
  347. array $groups,
  348. $sectionPath,
  349. array &$extraOldGroups,
  350. array &$oldConfig,
  351. \Magento\Framework\DB\Transaction $saveTransaction,
  352. \Magento\Framework\DB\Transaction $deleteTransaction
  353. ) {
  354. $groupPath = $sectionPath . '/' . $groupId;
  355. if (isset($groupData['fields'])) {
  356. /** @var \Magento\Config\Model\Config\Structure\Element\Group $group */
  357. $group = $this->_configStructure->getElement($groupPath);
  358. // set value for group field entry by fieldname
  359. // use extra memory
  360. $fieldsetData = [];
  361. foreach ($groupData['fields'] as $fieldId => $fieldData) {
  362. $fieldsetData[$fieldId] = $fieldData['value'] ?? null;
  363. }
  364. foreach ($groupData['fields'] as $fieldId => $fieldData) {
  365. $isReadOnly = $this->settingChecker->isReadOnly(
  366. $groupPath . '/' . $fieldId,
  367. $this->getScope(),
  368. $this->getScopeCode()
  369. );
  370. if ($isReadOnly) {
  371. continue;
  372. }
  373. $field = $this->getField($sectionPath, $groupId, $fieldId);
  374. /** @var \Magento\Framework\App\Config\ValueInterface $backendModel */
  375. $backendModel = $field->hasBackendModel()
  376. ? $field->getBackendModel()
  377. : $this->_configValueFactory->create();
  378. if (!isset($fieldData['value'])) {
  379. $fieldData['value'] = null;
  380. }
  381. $data = [
  382. 'field' => $fieldId,
  383. 'groups' => $groups,
  384. 'group_id' => $group->getId(),
  385. 'scope' => $this->getScope(),
  386. 'scope_id' => $this->getScopeId(),
  387. 'scope_code' => $this->getScopeCode(),
  388. 'field_config' => $field->getData(),
  389. 'fieldset_data' => $fieldsetData,
  390. ];
  391. $backendModel->addData($data);
  392. $this->_checkSingleStoreMode($field, $backendModel);
  393. $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig);
  394. $backendModel->setPath($path)->setValue($fieldData['value']);
  395. $inherit = !empty($fieldData['inherit']);
  396. if (isset($oldConfig[$path])) {
  397. $backendModel->setConfigId($oldConfig[$path]['config_id']);
  398. /**
  399. * Delete config data if inherit
  400. */
  401. if (!$inherit) {
  402. $saveTransaction->addObject($backendModel);
  403. } else {
  404. $deleteTransaction->addObject($backendModel);
  405. }
  406. } elseif (!$inherit) {
  407. $backendModel->unsConfigId();
  408. $saveTransaction->addObject($backendModel);
  409. }
  410. }
  411. }
  412. if (isset($groupData['groups'])) {
  413. foreach ($groupData['groups'] as $subGroupId => $subGroupData) {
  414. $this->_processGroup(
  415. $subGroupId,
  416. $subGroupData,
  417. $groups,
  418. $groupPath,
  419. $extraOldGroups,
  420. $oldConfig,
  421. $saveTransaction,
  422. $deleteTransaction
  423. );
  424. }
  425. }
  426. }
  427. /**
  428. * Load config data for section
  429. *
  430. * @return array
  431. */
  432. public function load()
  433. {
  434. if ($this->_configData === null) {
  435. $this->initScope();
  436. $this->_configData = $this->_getConfig(false);
  437. }
  438. return $this->_configData;
  439. }
  440. /**
  441. * Extend config data with additional config data by specified path
  442. *
  443. * @param string $path Config path prefix
  444. * @param bool $full Simple config structure or not
  445. * @param array $oldConfig Config data to extend
  446. * @return array
  447. */
  448. public function extendConfig($path, $full = true, $oldConfig = [])
  449. {
  450. $extended = $this->_configLoader->getConfigByPath($path, $this->getScope(), $this->getScopeId(), $full);
  451. if (is_array($oldConfig) && !empty($oldConfig)) {
  452. return $oldConfig + $extended;
  453. }
  454. return $extended;
  455. }
  456. /**
  457. * Add data by path section/group/field
  458. *
  459. * @param string $path
  460. * @param mixed $value
  461. * @return void
  462. * @throws \UnexpectedValueException
  463. */
  464. public function setDataByPath($path, $value)
  465. {
  466. $path = trim($path);
  467. if ($path === '') {
  468. throw new \UnexpectedValueException('Path must not be empty');
  469. }
  470. $pathParts = explode('/', $path);
  471. $keyDepth = count($pathParts);
  472. if ($keyDepth < 3) {
  473. throw new \UnexpectedValueException(
  474. 'Minimal depth of configuration is 3. Your configuration depth is ' . $keyDepth
  475. );
  476. }
  477. $section = array_shift($pathParts);
  478. $data = [
  479. 'fields' => [
  480. array_pop($pathParts) => ['value' => $value],
  481. ],
  482. ];
  483. while ($pathParts) {
  484. $data = [
  485. 'groups' => [
  486. array_pop($pathParts) => $data,
  487. ],
  488. ];
  489. }
  490. $data['section'] = $section;
  491. $this->addData($data);
  492. }
  493. /**
  494. * Set scope data
  495. *
  496. * @return void
  497. */
  498. private function initScope()
  499. {
  500. if ($this->getSection() === null) {
  501. $this->setSection('');
  502. }
  503. $scope = $this->retrieveScope();
  504. $this->setScope($this->scopeTypeNormalizer->normalize($scope->getScopeType()));
  505. $this->setScopeCode($scope->getCode());
  506. $this->setScopeId($scope->getId());
  507. if ($this->getWebsite() === null) {
  508. $this->setWebsite(StoreScopeInterface::SCOPE_WEBSITES === $this->getScope() ? $scope->getId() : '');
  509. }
  510. if ($this->getStore() === null) {
  511. $this->setStore(StoreScopeInterface::SCOPE_STORES === $this->getScope() ? $scope->getId() : '');
  512. }
  513. }
  514. /**
  515. * Retrieve scope from initial data
  516. *
  517. * @return ScopeInterface
  518. */
  519. private function retrieveScope(): ScopeInterface
  520. {
  521. $scopeType = $this->getScope();
  522. if (!$scopeType) {
  523. switch (true) {
  524. case $this->getStore():
  525. $scopeType = StoreScopeInterface::SCOPE_STORES;
  526. $scopeIdentifier = $this->getStore();
  527. break;
  528. case $this->getWebsite():
  529. $scopeType = StoreScopeInterface::SCOPE_WEBSITES;
  530. $scopeIdentifier = $this->getWebsite();
  531. break;
  532. default:
  533. $scopeType = ScopeInterface::SCOPE_DEFAULT;
  534. $scopeIdentifier = null;
  535. break;
  536. }
  537. } else {
  538. switch (true) {
  539. case $this->getScopeId() !== null:
  540. $scopeIdentifier = $this->getScopeId();
  541. break;
  542. case $this->getScopeCode() !== null:
  543. $scopeIdentifier = $this->getScopeCode();
  544. break;
  545. case $this->getStore() !== null:
  546. $scopeIdentifier = $this->getStore();
  547. break;
  548. case $this->getWebsite() !== null:
  549. $scopeIdentifier = $this->getWebsite();
  550. break;
  551. default:
  552. $scopeIdentifier = null;
  553. break;
  554. }
  555. }
  556. $scope = $this->scopeResolverPool->get($scopeType)
  557. ->getScope($scopeIdentifier);
  558. return $scope;
  559. }
  560. /**
  561. * Return formatted config data for current section
  562. *
  563. * @param bool $full Simple config structure or not
  564. * @return array
  565. */
  566. protected function _getConfig($full = true)
  567. {
  568. return $this->_configLoader->getConfigByPath(
  569. $this->getSection(),
  570. $this->getScope(),
  571. $this->getScopeId(),
  572. $full
  573. );
  574. }
  575. /**
  576. * Set correct scope if isSingleStoreMode = true
  577. *
  578. * @param \Magento\Config\Model\Config\Structure\Element\Field $fieldConfig
  579. * @param \Magento\Framework\App\Config\ValueInterface $dataObject
  580. * @return void
  581. */
  582. protected function _checkSingleStoreMode(
  583. \Magento\Config\Model\Config\Structure\Element\Field $fieldConfig,
  584. $dataObject
  585. ) {
  586. $isSingleStoreMode = $this->_storeManager->isSingleStoreMode();
  587. if (!$isSingleStoreMode) {
  588. return;
  589. }
  590. if (!$fieldConfig->showInDefault()) {
  591. $websites = $this->_storeManager->getWebsites();
  592. $singleStoreWebsite = array_shift($websites);
  593. $dataObject->setScope('websites');
  594. $dataObject->setWebsiteCode($singleStoreWebsite->getCode());
  595. $dataObject->setScopeCode($singleStoreWebsite->getCode());
  596. $dataObject->setScopeId($singleStoreWebsite->getId());
  597. }
  598. }
  599. /**
  600. * Get config data value
  601. *
  602. * @param string $path
  603. * @param null|bool &$inherit
  604. * @param null|array $configData
  605. * @return \Magento\Framework\Simplexml\Element
  606. */
  607. public function getConfigDataValue($path, &$inherit = null, $configData = null)
  608. {
  609. $this->load();
  610. if ($configData === null) {
  611. $configData = $this->_configData;
  612. }
  613. if (isset($configData[$path])) {
  614. $data = $configData[$path];
  615. $inherit = false;
  616. } else {
  617. $data = $this->_appConfig->getValue($path, $this->getScope(), $this->getScopeCode());
  618. $inherit = true;
  619. }
  620. return $data;
  621. }
  622. }