PK $arr * * @return bool * * @psalm-pure */ public static function isAssoc(array $arr): bool { return !count($arr) || count(array_filter(array_keys($arr), 'is_string')) == count($arr); } /** * Merge contents from one associtative array to another * * @param mixed $to * @param mixed $from * @param DataInterface::PRESERVE|DataInterface::REPLACE|DataInterface::MERGE $mode * * @return mixed * * @psalm-pure */ public static function mergeAssocArray($to, $from, int $mode = DataInterface::REPLACE) { if ($mode === DataInterface::MERGE && self::isList($to) && self::isList($from)) { return array_merge($to, $from); } if (is_array($from) && is_array($to)) { foreach ($from as $k => $v) { if (!isset($to[$k])) { $to[$k] = $v; } else { $to[$k] = self::mergeAssocArray($to[$k], $v, $mode); } } return $to; } return $mode === DataInterface::PRESERVE ? $to : $from; } /** * @param mixed $value * * @return bool * * @psalm-pure */ private static function isList($value): bool { return is_array($value) && array_values($value) === $value; } } PK */ class Data implements DataInterface, ArrayAccess { private const DELIMITERS = ['.', '/']; /** * Internal representation of data data * * @var array */ protected $data; /** * Constructor * * @param array $data */ public function __construct(array $data = []) { $this->data = $data; } /** * {@inheritdoc} */ public function append(string $key, $value = null): void { $currentValue =& $this->data; $keyPath = self::keyToPathArray($key); $endKey = array_pop($keyPath); foreach ($keyPath as $currentKey) { if (! isset($currentValue[$currentKey])) { $currentValue[$currentKey] = []; } $currentValue =& $currentValue[$currentKey]; } if (!isset($currentValue[$endKey])) { $currentValue[$endKey] = []; } if (!is_array($currentValue[$endKey])) { // Promote this key to an array. // TODO: Is this really what we want to do? $currentValue[$endKey] = [$currentValue[$endKey]]; } $currentValue[$endKey][] = $value; } /** * {@inheritdoc} */ public function set(string $key, $value = null): void { $currentValue =& $this->data; $keyPath = self::keyToPathArray($key); $endKey = array_pop($keyPath); foreach ($keyPath as $currentKey) { if (!isset($currentValue[$currentKey])) { $currentValue[$currentKey] = []; } if (!is_array($currentValue[$currentKey])) { throw new DataException(sprintf('Key path "%s" within "%s" cannot be indexed into (is not an array)', $currentKey, self::formatPath($key))); } $currentValue =& $currentValue[$currentKey]; } $currentValue[$endKey] = $value; } /** * {@inheritdoc} */ public function remove(string $key): void { $currentValue =& $this->data; $keyPath = self::keyToPathArray($key); $endKey = array_pop($keyPath); foreach ($keyPath as $currentKey) { if (!isset($currentValue[$currentKey])) { return; } $currentValue =& $currentValue[$currentKey]; } unset($currentValue[$endKey]); } /** * {@inheritdoc} * * @psalm-mutation-free */ public function get(string $key, $default = null) { /** @psalm-suppress ImpureFunctionCall */ $hasDefault = \func_num_args() > 1; $currentValue = $this->data; $keyPath = self::keyToPathArray($key); foreach ($keyPath as $currentKey) { if (!is_array($currentValue) || !array_key_exists($currentKey, $currentValue)) { if ($hasDefault) { return $default; } throw new MissingPathException($key, sprintf('No data exists at the given path: "%s"', self::formatPath($keyPath))); } $currentValue = $currentValue[$currentKey]; } return $currentValue === null ? $default : $currentValue; } /** * {@inheritdoc} * * @psalm-mutation-free */ public function has(string $key): bool { $currentValue = $this->data; foreach (self::keyToPathArray($key) as $currentKey) { if ( !is_array($currentValue) || !array_key_exists($currentKey, $currentValue) ) { return false; } $currentValue = $currentValue[$currentKey]; } return true; } /** * {@inheritdoc} * * @psalm-mutation-free */ public function getData(string $key): DataInterface { $value = $this->get($key); if (is_array($value) && Util::isAssoc($value)) { return new Data($value); } throw new DataException(sprintf('Value at "%s" could not be represented as a DataInterface', self::formatPath($key))); } /** * {@inheritdoc} */ public function import(array $data, int $mode = self::REPLACE): void { $this->data = Util::mergeAssocArray($this->data, $data, $mode); } /** * {@inheritdoc} */ public function importData(DataInterface $data, int $mode = self::REPLACE): void { $this->import($data->export(), $mode); } /** * {@inheritdoc} * * @psalm-mutation-free */ public function export(): array { return $this->data; } /** * {@inheritdoc} * * @return bool */ #[\ReturnTypeWillChange] public function offsetExists($key) { return $this->has($key); } /** * {@inheritdoc} * * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->get($key, null); } /** * {@inheritdoc} * * @param string $key * @param mixed $value * * @return void */ #[\ReturnTypeWillChange] public function offsetSet($key, $value) { $this->set($key, $value); } /** * {@inheritdoc} * * @return void */ #[\ReturnTypeWillChange] public function offsetUnset($key) { $this->remove($key); } /** * @param string $path * * @return string[] * * @psalm-return non-empty-list * * @psalm-pure */ protected static function keyToPathArray(string $path): array { if (\strlen($path) === 0) { throw new InvalidPathException('Path cannot be an empty string'); } $path = \str_replace(self::DELIMITERS, '.', $path); return \explode('.', $path); } /** * @param string|string[] $path * * @return string * * @psalm-pure */ protected static function formatPath($path): string { if (is_string($path)) { $path = self::keyToPathArray($path); } return implode(' » ', $path); } } PK $data * @param self::PRESERVE|self::REPLACE|self::MERGE $mode */ public function import(array $data, int $mode = self::REPLACE): void; /** * Import data from an external data into existing data * * @param DataInterface $data * @param self::PRESERVE|self::REPLACE|self::MERGE $mode */ public function importData(DataInterface $data, int $mode = self::REPLACE): void; /** * Export data as raw data * * @return array * * @psalm-mutation-free */ public function export(): array; } PKpath = $path; parent::__construct($message, $code, $previous); } public function getPath(): string { return $this->path; } } PK For PHP (5.3+) please refer to version `1.0`. Usage ----- Abstract example: ```php use Dflydev\DotAccessData\Data; $data = new Data; $data->set('a.b.c', 'C'); $data->set('a.b.d', 'D1'); $data->append('a.b.d', 'D2'); $data->set('a.b.e', ['E0', 'E1', 'E2']); // C $data->get('a.b.c'); // ['D1', 'D2'] $data->get('a.b.d'); // ['E0', 'E1', 'E2'] $data->get('a.b.e'); // true $data->has('a.b.c'); // false $data->has('a.b.d.j'); // 'some-default-value' $data->get('some.path.that.does.not.exist', 'some-default-value'); // throws a MissingPathException because no default was given $data->get('some.path.that.does.not.exist'); ``` A more concrete example: ```php use Dflydev\DotAccessData\Data; $data = new Data([ 'hosts' => [ 'hewey' => [ 'username' => 'hman', 'password' => 'HPASS', 'roles' => ['web'], ], 'dewey' => [ 'username' => 'dman', 'password' => 'D---S', 'roles' => ['web', 'db'], 'nick' => 'dewey dman', ], 'lewey' => [ 'username' => 'lman', 'password' => 'LP@$$', 'roles' => ['db'], ], ], ]); // hman $username = $data->get('hosts.hewey.username'); // HPASS $password = $data->get('hosts.hewey.password'); // ['web'] $roles = $data->get('hosts.hewey.roles'); // dewey dman $nick = $data->get('hosts.dewey.nick'); // Unknown $nick = $data->get('hosts.lewey.nick', 'Unknown'); // DataInterface instance $dewey = $data->getData('hosts.dewey'); // dman $username = $dewey->get('username'); // D---S $password = $dewey->get('password'); // ['web', 'db'] $roles = $dewey->get('roles'); // No more lewey $data->remove('hosts.lewey'); // Add DB to hewey's roles $data->append('hosts.hewey.roles', 'db'); $data->set('hosts.april', [ 'username' => 'aman', 'password' => '@---S', 'roles' => ['web'], ]); // Check if a key exists (true to this case) $hasKey = $data->has('hosts.dewey.username'); ``` `Data` may be used as an array, since it implements `ArrayAccess` interface: ```php // Get $data->get('name') === $data['name']; // true $data['name'] = 'Dewey'; // is equivalent to $data->set($name, 'Dewey'); isset($data['name']) === $data->has('name'); // Remove key unset($data['name']); ``` `/` can also be used as a path delimiter: ```php $data->set('a/b/c', 'd'); echo $data->get('a/b/c'); // "d" $data->get('a/b/c') === $data->get('a.b.c'); // true ``` License ------- This library is licensed under the MIT License - see the LICENSE file for details. Community --------- If you have questions or want to help out, join us in the [#dflydev](irc://irc.freenode.net/#dflydev) channel on irc.freenode.net. PK