Structure.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Config\Model\Config;
  7. use Magento\Framework\Exception\LocalizedException;
  8. /**
  9. * System configuration structure.
  10. *
  11. * All paths are declared in module's system.xml.
  12. *
  13. * ```xml
  14. * <section id="section_id">
  15. * <group id="group_id" ...>
  16. * <field id="field_one_id" ...>
  17. * <label>Field One</label>
  18. * ...
  19. * </field>
  20. * <field id="field_two_id" ...>
  21. * <label>Field Two</label>
  22. * <config_path>section/group/field</config_path>
  23. * ...
  24. * </field>
  25. * </group>
  26. * </section>
  27. * ```
  28. *
  29. * Structure path is the nested path of node ids (section, group, field).
  30. *
  31. * Config path is the path which is declared in <config_path> node.
  32. * If this node is not provided then config path is the same as structure path.
  33. *
  34. * With the example above you can see that the field <field id="field_one_id"> has the next paths:
  35. * - the structure path section_id/group_id/field_one_id
  36. * - the configuration path section_id/group_id/field_one_id
  37. *
  38. * Also you can see that the field <field id="field_two_id"> has the next paths:
  39. * - the structure path section_id/group_id/field_two_id
  40. * - the configuration path section/group/field
  41. *
  42. * @api
  43. * @since 100.0.2
  44. */
  45. class Structure implements \Magento\Config\Model\Config\Structure\SearchInterface
  46. {
  47. /**
  48. * Key that contains field type in structure array
  49. */
  50. const TYPE_KEY = '_elementType';
  51. /**
  52. * Configuration structure represented as tree
  53. *
  54. * @var array
  55. */
  56. protected $_data;
  57. /**
  58. * Config tab iterator
  59. *
  60. * @var \Magento\Config\Model\Config\Structure\Element\Iterator\Tab
  61. */
  62. protected $_tabIterator;
  63. /**
  64. * Pool of config element flyweight objects
  65. *
  66. * @var \Magento\Config\Model\Config\Structure\Element\FlyweightFactory
  67. */
  68. protected $_flyweightFactory;
  69. /**
  70. * Provider of current config scope
  71. *
  72. * @var ScopeDefiner
  73. */
  74. protected $_scopeDefiner;
  75. /**
  76. * List of cached elements
  77. *
  78. * @var \Magento\Config\Model\Config\Structure\ElementInterface[]
  79. */
  80. protected $_elements;
  81. /**
  82. * List of config sections
  83. *
  84. * @var array
  85. * @since 100.1.0
  86. */
  87. protected $sectionList;
  88. /**
  89. * Collects config paths and their structure paths from configuration files
  90. *
  91. * For example:
  92. * ```php
  93. * [
  94. * 'section_id/group_id/field_one_id' => [
  95. * 'section_id/group_id/field_one_id'
  96. * ],
  97. * 'section/group/field' => [
  98. * 'section_id/group_id/field_two_id'
  99. * ]
  100. * ```
  101. *
  102. * @var array
  103. */
  104. private $mappedPaths;
  105. /**
  106. * @param \Magento\Config\Model\Config\Structure\Data $structureData
  107. * @param \Magento\Config\Model\Config\Structure\Element\Iterator\Tab $tabIterator
  108. * @param \Magento\Config\Model\Config\Structure\Element\FlyweightFactory $flyweightFactory
  109. * @param ScopeDefiner $scopeDefiner
  110. */
  111. public function __construct(
  112. \Magento\Config\Model\Config\Structure\Data $structureData,
  113. \Magento\Config\Model\Config\Structure\Element\Iterator\Tab $tabIterator,
  114. \Magento\Config\Model\Config\Structure\Element\FlyweightFactory $flyweightFactory,
  115. ScopeDefiner $scopeDefiner
  116. ) {
  117. $this->_data = $structureData->get();
  118. $this->_tabIterator = $tabIterator;
  119. $this->_flyweightFactory = $flyweightFactory;
  120. $this->_scopeDefiner = $scopeDefiner;
  121. }
  122. /**
  123. * Retrieve tab iterator
  124. *
  125. * @return \Magento\Config\Model\Config\Structure\Element\Iterator
  126. */
  127. public function getTabs()
  128. {
  129. if (isset($this->_data['sections'])) {
  130. foreach ($this->_data['sections'] as $sectionId => $section) {
  131. if (isset($section['tab']) && $section['tab']) {
  132. $this->_data['tabs'][$section['tab']]['children'][$sectionId] = $section;
  133. }
  134. }
  135. $this->_tabIterator->setElements($this->_data['tabs'], $this->_scopeDefiner->getScope());
  136. }
  137. return $this->_tabIterator;
  138. }
  139. /**
  140. * Retrieve config section list
  141. *
  142. * @return array
  143. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  144. * @since 100.1.0
  145. */
  146. public function getSectionList()
  147. {
  148. if (empty($this->sectionList)) {
  149. foreach ($this->_data['sections'] as $sectionId => $section) {
  150. if (array_key_exists('children', $section) && is_array($section['children'])) {
  151. foreach ($section['children'] as $childId => $child) {
  152. $this->sectionList[$sectionId . '_' . $childId] = true;
  153. }
  154. }
  155. }
  156. }
  157. return $this->sectionList;
  158. }
  159. /**
  160. * Find element by structure path
  161. *
  162. * @param string $path The structure path
  163. * @return \Magento\Config\Model\Config\Structure\ElementInterface|null
  164. */
  165. public function getElement($path)
  166. {
  167. return $this->getElementByPathParts(explode('/', $path));
  168. }
  169. /**
  170. * Find element by config path
  171. *
  172. * @param string $path The configuration path
  173. * @return \Magento\Config\Model\Config\Structure\ElementInterface|null
  174. * @since 101.0.0
  175. */
  176. public function getElementByConfigPath($path)
  177. {
  178. $allPaths = $this->getFieldPaths();
  179. if (isset($allPaths[$path])) {
  180. $path = array_shift($allPaths[$path]);
  181. }
  182. return $this->getElementByPathParts(explode('/', $path));
  183. }
  184. /**
  185. * Retrieve first available section in config structure
  186. *
  187. * @return Structure\ElementInterface
  188. * @throws LocalizedException
  189. */
  190. public function getFirstSection()
  191. {
  192. $tabs = $this->getTabs();
  193. $tabs->rewind();
  194. /** @var $tab \Magento\Config\Model\Config\Structure\Element\Tab */
  195. $tab = $tabs->current();
  196. $tab->getChildren()->rewind();
  197. if (!$tab->getChildren()->current()->isVisible()) {
  198. throw new LocalizedException(__('Visible section not found.'));
  199. }
  200. return $tab->getChildren()->current();
  201. }
  202. /**
  203. * Find element by path parts
  204. *
  205. * @param string[] $pathParts
  206. * @return \Magento\Config\Model\Config\Structure\ElementInterface|null
  207. */
  208. public function getElementByPathParts(array $pathParts)
  209. {
  210. $path = implode('_', $pathParts);
  211. if (isset($this->_elements[$path])) {
  212. return $this->_elements[$path];
  213. }
  214. $children = [];
  215. if ($this->_data) {
  216. $children = $this->_data['sections'];
  217. }
  218. $child = [];
  219. foreach ($pathParts as $pathPart) {
  220. if ($children && (array_key_exists($pathPart, $children))) {
  221. $child = $children[$pathPart];
  222. $children = array_key_exists('children', $child) ? $child['children'] : [];
  223. } else {
  224. $child = $this->_createEmptyElement($pathParts);
  225. break;
  226. }
  227. }
  228. $this->_elements[$path] = $this->_flyweightFactory->create($child['_elementType']);
  229. $this->_elements[$path]->setData($child, $this->_scopeDefiner->getScope());
  230. return $this->_elements[$path];
  231. }
  232. /**
  233. * Create empty element data
  234. *
  235. * @param string[] $pathParts
  236. * @return array
  237. */
  238. protected function _createEmptyElement(array $pathParts)
  239. {
  240. switch (count($pathParts)) {
  241. case 1:
  242. $elementType = 'section';
  243. break;
  244. case 2:
  245. $elementType = 'group';
  246. break;
  247. default:
  248. $elementType = 'field';
  249. }
  250. $elementId = array_pop($pathParts);
  251. return ['id' => $elementId, 'path' => implode('/', $pathParts), '_elementType' => $elementType];
  252. }
  253. /**
  254. * Retrieve paths of fields that have provided attributes with provided values
  255. *
  256. * @param string $attributeName
  257. * @param mixed $attributeValue
  258. * @return array
  259. */
  260. public function getFieldPathsByAttribute($attributeName, $attributeValue)
  261. {
  262. $result = [];
  263. if (empty($this->_data['sections'])) {
  264. return $result;
  265. }
  266. foreach ($this->_data['sections'] as $section) {
  267. if (!isset($section['children'])) {
  268. continue;
  269. }
  270. foreach ($section['children'] as $group) {
  271. if (isset($group['children'])) {
  272. $path = $section['id'] . '/' . $group['id'];
  273. $result = array_merge(
  274. $result,
  275. $this->_getGroupFieldPathsByAttribute(
  276. $group['children'],
  277. $path,
  278. $attributeName,
  279. $attributeValue
  280. )
  281. );
  282. }
  283. }
  284. }
  285. return $result;
  286. }
  287. /**
  288. * Find group fields with specified attribute and attribute value
  289. *
  290. * @param array $fields
  291. * @param string $parentPath
  292. * @param string $attributeName
  293. * @param mixed $attributeValue
  294. * @return array
  295. */
  296. protected function _getGroupFieldPathsByAttribute(array $fields, $parentPath, $attributeName, $attributeValue)
  297. {
  298. $result = [];
  299. foreach ($fields as $field) {
  300. if (isset($field['children'])) {
  301. $result += $this->_getGroupFieldPathsByAttribute(
  302. $field['children'],
  303. $parentPath . '/' . $field['id'],
  304. $attributeName,
  305. $attributeValue
  306. );
  307. } elseif (isset($field[$attributeName]) && $field[$attributeName] == $attributeValue) {
  308. $result[] = $parentPath . '/' . $field['id'];
  309. }
  310. }
  311. return $result;
  312. }
  313. /**
  314. * Collects config paths and their structure paths from configuration files.
  315. * Returns the map of config paths and their structure paths.
  316. * All paths are declared in module's system.xml.
  317. *
  318. * ```xml
  319. * <section id="section_id">
  320. * <group id="group_id" ...>
  321. * <field id="field_one_id" ...>
  322. * <label>Field One</label>
  323. * ...
  324. * </field>
  325. * <field id="field_two_id" ...>
  326. * <label>Field Two</label>
  327. * <config_path>section/group/field</config_path>
  328. * ...
  329. * </field>
  330. * </group>
  331. * </section>
  332. * ```
  333. * If <config_path> node does not exist, then config path duplicates structure path.
  334. * The result of this example will be:
  335. *
  336. * ```php
  337. * [
  338. * 'section_id/group_id/field_one_id' => [
  339. * 'section_id/group_id/field_one_id'
  340. * ],
  341. * 'section/group/field' => [
  342. * 'section_id/group_id/field_two_id'
  343. * ]
  344. * ```
  345. *
  346. * @return array An array of config path to config structure path map
  347. * @since 101.0.0
  348. */
  349. public function getFieldPaths()
  350. {
  351. $sections = !empty($this->_data['sections']) ? $this->_data['sections'] : [];
  352. if (!$this->mappedPaths) {
  353. $this->mappedPaths = $this->getFieldsRecursively($sections);
  354. }
  355. return $this->mappedPaths;
  356. }
  357. /**
  358. * Iteration that collects config field paths recursively from config files.
  359. *
  360. * @param array $elements The elements to be parsed
  361. * @return array An array of config path to config structure path map
  362. */
  363. private function getFieldsRecursively(array $elements = [])
  364. {
  365. $result = [];
  366. foreach ($elements as $element) {
  367. if (isset($element['children'])) {
  368. $result = array_merge_recursive(
  369. $result,
  370. $this->getFieldsRecursively($element['children'])
  371. );
  372. } else {
  373. if ($element['_elementType'] === 'field' && isset($element['label'])) {
  374. $structurePath = (isset($element['path']) ? $element['path'] . '/' : '') . $element['id'];
  375. $configPath = isset($element['config_path']) ? $element['config_path'] : $structurePath;
  376. if (!isset($result[$configPath])) {
  377. $result[$configPath] = [];
  378. }
  379. $result[$configPath][] = $structurePath;
  380. }
  381. }
  382. }
  383. return $result;
  384. }
  385. }