Data.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is a part of dflydev/dot-access-data.
  5. *
  6. * (c) Dragonfly Development Inc.
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Dflydev\DotAccessData;
  12. use ArrayAccess;
  13. use Dflydev\DotAccessData\Exception\DataException;
  14. use Dflydev\DotAccessData\Exception\InvalidPathException;
  15. use Dflydev\DotAccessData\Exception\MissingPathException;
  16. /**
  17. * @implements ArrayAccess<string, mixed>
  18. */
  19. class Data implements DataInterface, ArrayAccess
  20. {
  21. private const DELIMITERS = ['.', '/'];
  22. /**
  23. * Internal representation of data data
  24. *
  25. * @var array<string, mixed>
  26. */
  27. protected $data;
  28. /**
  29. * Constructor
  30. *
  31. * @param array<string, mixed> $data
  32. */
  33. public function __construct(array $data = [])
  34. {
  35. $this->data = $data;
  36. }
  37. /**
  38. * {@inheritdoc}
  39. */
  40. public function append(string $key, $value = null): void
  41. {
  42. $currentValue =& $this->data;
  43. $keyPath = self::keyToPathArray($key);
  44. $endKey = array_pop($keyPath);
  45. foreach ($keyPath as $currentKey) {
  46. if (! isset($currentValue[$currentKey])) {
  47. $currentValue[$currentKey] = [];
  48. }
  49. $currentValue =& $currentValue[$currentKey];
  50. }
  51. if (!isset($currentValue[$endKey])) {
  52. $currentValue[$endKey] = [];
  53. }
  54. if (!is_array($currentValue[$endKey])) {
  55. // Promote this key to an array.
  56. // TODO: Is this really what we want to do?
  57. $currentValue[$endKey] = [$currentValue[$endKey]];
  58. }
  59. $currentValue[$endKey][] = $value;
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. public function set(string $key, $value = null): void
  65. {
  66. $currentValue =& $this->data;
  67. $keyPath = self::keyToPathArray($key);
  68. $endKey = array_pop($keyPath);
  69. foreach ($keyPath as $currentKey) {
  70. if (!isset($currentValue[$currentKey])) {
  71. $currentValue[$currentKey] = [];
  72. }
  73. if (!is_array($currentValue[$currentKey])) {
  74. throw new DataException(sprintf('Key path "%s" within "%s" cannot be indexed into (is not an array)', $currentKey, self::formatPath($key)));
  75. }
  76. $currentValue =& $currentValue[$currentKey];
  77. }
  78. $currentValue[$endKey] = $value;
  79. }
  80. /**
  81. * {@inheritdoc}
  82. */
  83. public function remove(string $key): void
  84. {
  85. $currentValue =& $this->data;
  86. $keyPath = self::keyToPathArray($key);
  87. $endKey = array_pop($keyPath);
  88. foreach ($keyPath as $currentKey) {
  89. if (!isset($currentValue[$currentKey])) {
  90. return;
  91. }
  92. $currentValue =& $currentValue[$currentKey];
  93. }
  94. unset($currentValue[$endKey]);
  95. }
  96. /**
  97. * {@inheritdoc}
  98. *
  99. * @psalm-mutation-free
  100. */
  101. public function get(string $key, $default = null)
  102. {
  103. /** @psalm-suppress ImpureFunctionCall */
  104. $hasDefault = \func_num_args() > 1;
  105. $currentValue = $this->data;
  106. $keyPath = self::keyToPathArray($key);
  107. foreach ($keyPath as $currentKey) {
  108. if (!is_array($currentValue) || !array_key_exists($currentKey, $currentValue)) {
  109. if ($hasDefault) {
  110. return $default;
  111. }
  112. throw new MissingPathException($key, sprintf('No data exists at the given path: "%s"', self::formatPath($keyPath)));
  113. }
  114. $currentValue = $currentValue[$currentKey];
  115. }
  116. return $currentValue === null ? $default : $currentValue;
  117. }
  118. /**
  119. * {@inheritdoc}
  120. *
  121. * @psalm-mutation-free
  122. */
  123. public function has(string $key): bool
  124. {
  125. $currentValue = $this->data;
  126. foreach (self::keyToPathArray($key) as $currentKey) {
  127. if (
  128. !is_array($currentValue) ||
  129. !array_key_exists($currentKey, $currentValue)
  130. ) {
  131. return false;
  132. }
  133. $currentValue = $currentValue[$currentKey];
  134. }
  135. return true;
  136. }
  137. /**
  138. * {@inheritdoc}
  139. *
  140. * @psalm-mutation-free
  141. */
  142. public function getData(string $key): DataInterface
  143. {
  144. $value = $this->get($key);
  145. if (is_array($value) && Util::isAssoc($value)) {
  146. return new Data($value);
  147. }
  148. throw new DataException(sprintf('Value at "%s" could not be represented as a DataInterface', self::formatPath($key)));
  149. }
  150. /**
  151. * {@inheritdoc}
  152. */
  153. public function import(array $data, int $mode = self::REPLACE): void
  154. {
  155. $this->data = Util::mergeAssocArray($this->data, $data, $mode);
  156. }
  157. /**
  158. * {@inheritdoc}
  159. */
  160. public function importData(DataInterface $data, int $mode = self::REPLACE): void
  161. {
  162. $this->import($data->export(), $mode);
  163. }
  164. /**
  165. * {@inheritdoc}
  166. *
  167. * @psalm-mutation-free
  168. */
  169. public function export(): array
  170. {
  171. return $this->data;
  172. }
  173. /**
  174. * {@inheritdoc}
  175. *
  176. * @return bool
  177. */
  178. #[\ReturnTypeWillChange]
  179. public function offsetExists($key)
  180. {
  181. return $this->has($key);
  182. }
  183. /**
  184. * {@inheritdoc}
  185. *
  186. * @return mixed
  187. */
  188. #[\ReturnTypeWillChange]
  189. public function offsetGet($key)
  190. {
  191. return $this->get($key, null);
  192. }
  193. /**
  194. * {@inheritdoc}
  195. *
  196. * @param string $key
  197. * @param mixed $value
  198. *
  199. * @return void
  200. */
  201. #[\ReturnTypeWillChange]
  202. public function offsetSet($key, $value)
  203. {
  204. $this->set($key, $value);
  205. }
  206. /**
  207. * {@inheritdoc}
  208. *
  209. * @return void
  210. */
  211. #[\ReturnTypeWillChange]
  212. public function offsetUnset($key)
  213. {
  214. $this->remove($key);
  215. }
  216. /**
  217. * @param string $path
  218. *
  219. * @return string[]
  220. *
  221. * @psalm-return non-empty-list<string>
  222. *
  223. * @psalm-pure
  224. */
  225. protected static function keyToPathArray(string $path): array
  226. {
  227. if (\strlen($path) === 0) {
  228. throw new InvalidPathException('Path cannot be an empty string');
  229. }
  230. $path = \str_replace(self::DELIMITERS, '.', $path);
  231. return \explode('.', $path);
  232. }
  233. /**
  234. * @param string|string[] $path
  235. *
  236. * @return string
  237. *
  238. * @psalm-pure
  239. */
  240. protected static function formatPath($path): string
  241. {
  242. if (is_string($path)) {
  243. $path = self::keyToPathArray($path);
  244. }
  245. return implode(' » ', $path);
  246. }
  247. }