ArrayManager.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Stdlib;
  7. /**
  8. * Provides methods for nested array manipulations
  9. *
  10. * @api
  11. * @since 100.1.0
  12. */
  13. class ArrayManager
  14. {
  15. /**
  16. * Default node delimiter for path
  17. */
  18. const DEFAULT_PATH_DELIMITER = '/';
  19. /**
  20. * @var array
  21. * @since 100.1.0
  22. */
  23. protected $parentNode;
  24. /**
  25. * @var string
  26. * @since 100.1.0
  27. */
  28. protected $nodeIndex;
  29. /**
  30. * Check if node exists
  31. *
  32. * @param array|string $path
  33. * @param array $data
  34. * @param string $delimiter
  35. * @return bool
  36. * @since 100.1.0
  37. */
  38. public function exists($path, array $data, $delimiter = self::DEFAULT_PATH_DELIMITER)
  39. {
  40. return $this->find($path, $data, $delimiter);
  41. }
  42. /**
  43. * Retrieve node
  44. *
  45. * @param array|string $path
  46. * @param array $data
  47. * @param null $defaultValue
  48. * @param string $delimiter
  49. * @return mixed|null
  50. * @since 100.1.0
  51. */
  52. public function get($path, array $data, $defaultValue = null, $delimiter = self::DEFAULT_PATH_DELIMITER)
  53. {
  54. return $this->find($path, $data, $delimiter) ? $this->parentNode[$this->nodeIndex] : $defaultValue;
  55. }
  56. /**
  57. * Set value into node and return modified data
  58. *
  59. * @param array|string $path
  60. * @param array $data
  61. * @param mixed $value
  62. * @param string $delimiter
  63. * @return array
  64. * @since 100.1.0
  65. */
  66. public function set($path, array $data, $value, $delimiter = self::DEFAULT_PATH_DELIMITER)
  67. {
  68. if ($this->find($path, $data, $delimiter, true)) {
  69. $this->parentNode[$this->nodeIndex] = $value;
  70. }
  71. return $data;
  72. }
  73. /**
  74. * Set value into existing node and return modified data
  75. *
  76. * @param array|string $path
  77. * @param array $data
  78. * @param mixed $value
  79. * @param string $delimiter
  80. * @return array
  81. * @since 100.1.0
  82. */
  83. public function replace($path, array $data, $value, $delimiter = self::DEFAULT_PATH_DELIMITER)
  84. {
  85. if ($this->find($path, $data, $delimiter)) {
  86. $this->parentNode[$this->nodeIndex] = $value;
  87. }
  88. return $data;
  89. }
  90. /**
  91. * Move value from one location to another
  92. *
  93. * @param array|string $path
  94. * @param string $targetPath
  95. * @param array $data
  96. * @param bool $overwrite
  97. * @param string $delimiter
  98. * @return array
  99. * @since 100.1.0
  100. */
  101. public function move($path, $targetPath, array $data, $overwrite = false, $delimiter = self::DEFAULT_PATH_DELIMITER)
  102. {
  103. if ($this->find($path, $data, $delimiter)) {
  104. $parentNode = &$this->parentNode;
  105. $nodeIndex = &$this->nodeIndex;
  106. if ((!$this->find($targetPath, $data, $delimiter) || $overwrite)
  107. && $this->find($targetPath, $data, $delimiter, true)
  108. ) {
  109. $this->parentNode[$this->nodeIndex] = $parentNode[$nodeIndex];
  110. unset($parentNode[$nodeIndex]);
  111. }
  112. }
  113. return $data;
  114. }
  115. /**
  116. * Merge value with node and return modified data
  117. *
  118. * @param array|string $path
  119. * @param array $data
  120. * @param array $value
  121. * @param string $delimiter
  122. * @return array
  123. * @since 100.1.0
  124. */
  125. public function merge($path, array $data, array $value, $delimiter = self::DEFAULT_PATH_DELIMITER)
  126. {
  127. if ($this->find($path, $data, $delimiter) && is_array($this->parentNode[$this->nodeIndex])) {
  128. $this->parentNode[$this->nodeIndex] = array_replace_recursive(
  129. $this->parentNode[$this->nodeIndex],
  130. $value
  131. );
  132. }
  133. return $data;
  134. }
  135. /**
  136. * Populate nested array if possible and needed
  137. *
  138. * @param array|string $path
  139. * @param array $data
  140. * @param string $delimiter
  141. * @return array
  142. * @since 100.1.0
  143. */
  144. public function populate($path, array $data, $delimiter = self::DEFAULT_PATH_DELIMITER)
  145. {
  146. $this->find($path, $data, $delimiter, true);
  147. return $data;
  148. }
  149. /**
  150. * Remove node and return modified data
  151. *
  152. * @param array|string $path
  153. * @param array $data
  154. * @param string $delimiter
  155. * @return array
  156. * @since 100.1.0
  157. */
  158. public function remove($path, array $data, $delimiter = self::DEFAULT_PATH_DELIMITER)
  159. {
  160. if ($this->find($path, $data, $delimiter)) {
  161. unset($this->parentNode[$this->nodeIndex]);
  162. }
  163. return $data;
  164. }
  165. /**
  166. * Finds node in nested array and saves its index and parent node reference
  167. *
  168. * @param array|string $path
  169. * @param array $data
  170. * @param string $delimiter
  171. * @param bool $populate
  172. * @return bool
  173. * @since 100.1.0
  174. */
  175. protected function find($path, array &$data, $delimiter, $populate = false)
  176. {
  177. if (is_array($path)) {
  178. $path = implode($delimiter, $path);
  179. }
  180. if ($path === null) {
  181. return false;
  182. }
  183. $currentNode = &$data;
  184. $path = explode($delimiter, $path);
  185. foreach ($path as $index) {
  186. if (!is_array($currentNode)) {
  187. return false;
  188. }
  189. if (!array_key_exists($index, $currentNode)) {
  190. if (!$populate) {
  191. return false;
  192. }
  193. $currentNode[$index] = [];
  194. }
  195. $this->nodeIndex = $index;
  196. $this->parentNode = &$currentNode;
  197. $currentNode = &$currentNode[$index];
  198. }
  199. return true;
  200. }
  201. /**
  202. * Get matching paths for elements with specified indexes
  203. *
  204. * @param array|mixed $indexes
  205. * @param array $data
  206. * @param string|array|null $startPath
  207. * @param string|array|null $internalPath
  208. * @param int|null $maxResults
  209. * @param string $delimiter
  210. * @return array
  211. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  212. * @SuppressWarnings(PHPMD.NPathComplexity)
  213. * @since 100.1.0
  214. */
  215. public function findPaths(
  216. $indexes,
  217. array $data,
  218. $startPath = null,
  219. $internalPath = null,
  220. $maxResults = null,
  221. $delimiter = self::DEFAULT_PATH_DELIMITER
  222. ) {
  223. $indexes = (array)$indexes;
  224. $startPath = is_array($startPath) ? implode($delimiter, $startPath) : $startPath;
  225. $internalPath = is_array($internalPath) ? implode($delimiter, $internalPath) : $internalPath;
  226. $data = $startPath !== null ? $this->get($startPath, $data, [], $delimiter) : $data;
  227. $checkList = [$startPath => ['start' => $startPath === null, 'children' => $data]];
  228. $paths = [];
  229. while ($checkList) {
  230. $nextCheckList = [];
  231. foreach ($checkList as $path => $config) {
  232. foreach ($config['children'] as $childIndex => $childData) {
  233. $childPath = $path . (!$config['start'] ? $delimiter : '') . $childIndex;
  234. if (in_array($childIndex, $indexes, true)) {
  235. $paths[] = $childPath;
  236. if ($maxResults !== null && count($paths) >= $maxResults) {
  237. return $paths;
  238. }
  239. }
  240. $searchData = $internalPath !== null && is_array($childData)
  241. ? $this->get($internalPath, $childData, null, $delimiter)
  242. : $childData;
  243. if (!empty($searchData) && is_array($searchData)) {
  244. $searchPath = $childPath . ($internalPath !== null ? $delimiter . $internalPath : '');
  245. $nextCheckList[$searchPath] = ['start' => false, 'children' => $searchData];
  246. }
  247. }
  248. }
  249. $checkList = $nextCheckList;
  250. }
  251. return $paths;
  252. }
  253. /**
  254. * Get first matching path for elements with specified indexes
  255. *
  256. * @param array|mixed $indexes
  257. * @param array $data
  258. * @param string|array|null $startPath
  259. * @param string|array|null $internalPath
  260. * @param string $delimiter
  261. * @return string|null
  262. * @since 100.1.0
  263. */
  264. public function findPath(
  265. $indexes,
  266. array $data,
  267. $startPath = null,
  268. $internalPath = null,
  269. $delimiter = self::DEFAULT_PATH_DELIMITER
  270. ) {
  271. $paths = $this->findPaths($indexes, $data, $startPath, $internalPath, 1, $delimiter);
  272. return $paths ? reset($paths) : null;
  273. }
  274. /**
  275. * Retrieve slice of specified path
  276. *
  277. * @param string $path
  278. * @param int $offset
  279. * @param int|null $length
  280. * @param string $delimiter
  281. * @return string
  282. * @since 100.1.0
  283. */
  284. public function slicePath($path, $offset, $length = null, $delimiter = self::DEFAULT_PATH_DELIMITER)
  285. {
  286. return implode($delimiter, array_slice(explode($delimiter, $path), $offset, $length));
  287. }
  288. }