├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Data.php ├── DataInterface.php ├── Exception ├── DataException.php ├── InvalidPathException.php └── MissingPathException.php └── Util.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [3.0.3] - 2024-07-08 11 | 12 | ### Fixed 13 | 14 | - Fixed PHP 8.4 deprecation notices (#47) 15 | 16 | ## [3.0.2] - 2022-10-27 17 | 18 | ### Fixed 19 | 20 | - Added missing return types to docblocks (#44, #45) 21 | 22 | ## [3.0.1] - 2021-08-13 23 | 24 | ### Added 25 | 26 | - Adds ReturnTypeWillChange to suppress PHP 8.1 warnings (#40) 27 | 28 | ## [3.0.0] - 2021-01-01 29 | 30 | ### Added 31 | - Added support for both `.` and `/`-delimited key paths (#24) 32 | - Added parameter and return types to everything; enabled strict type checks (#18) 33 | - Added new exception classes to better identify certain types of errors (#20) 34 | - `Data` now implements `ArrayAccess` (#17) 35 | - Added ability to merge non-associative array values (#31, #32) 36 | 37 | ### Changed 38 | - All thrown exceptions are now instances or subclasses of `DataException` (#20) 39 | - Calling `get()` on a missing key path without providing a default will throw a `MissingPathException` instead of returning `null` (#29) 40 | - Bumped supported PHP versions to 7.1 - 8.x (#18) 41 | 42 | ### Fixed 43 | - Fixed incorrect merging of array values into string values (#32) 44 | - Fixed `get()` method behaving as if keys with `null` values didn't exist 45 | 46 | ## [2.0.0] - 2017-12-21 47 | 48 | ### Changed 49 | - Bumped supported PHP versions to 7.0 - 7.4 (#12) 50 | - Switched to PSR-4 autoloading 51 | 52 | ## [1.1.0] - 2017-01-20 53 | 54 | ### Added 55 | - Added new `has()` method to check for the existence of the given key (#4, #7) 56 | 57 | ## [1.0.1] - 2015-08-12 58 | 59 | ### Added 60 | - Added new optional `$default` parameter to the `get()` method (#2) 61 | 62 | ## [1.0.0] - 2012-07-17 63 | 64 | **Initial release!** 65 | 66 | [Unreleased]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.3...main 67 | [3.0.3]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.2...v3.0.3 68 | [3.0.2]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.1...v3.0.2 69 | [3.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.0...v3.0.1 70 | [3.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v2.0.0...v3.0.0 71 | [2.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.1.0...v2.0.0 72 | [1.1.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.1...v1.1.0 73 | [1.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.0...v1.0.1 74 | [1.0.0]: https://github.com/dflydev/dflydev-dot-access-data/releases/tag/v1.0.0 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Dragonfly Development Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dot Access Data 2 | =============== 3 | 4 | [![Latest Version](https://img.shields.io/packagist/v/dflydev/dot-access-data.svg?style=flat-square)](https://packagist.org/packages/dflydev/dot-access-data) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/dflydev/dot-access-data.svg?style=flat-square)](https://packagist.org/packages/dflydev/dot-access-data) 6 | [![Software License](https://img.shields.io/badge/License-MIT-brightgreen.svg?style=flat-square)](LICENSE) 7 | [![Build Status](https://img.shields.io/github/workflow/status/dflydev/dflydev-dot-access-data/Tests/main.svg?style=flat-square)](https://github.com/dflydev/dflydev-dot-access-data/actions?query=workflow%3ATests+branch%3Amain) 8 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/dflydev/dflydev-dot-access-data.svg?style=flat-square)](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data/code-structure/) 9 | [![Quality Score](https://img.shields.io/scrutinizer/g/dflydev/dflydev-dot-access-data.svg?style=flat-square)](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data) 10 | 11 | Given a deep data structure, access data by dot notation. 12 | 13 | 14 | Requirements 15 | ------------ 16 | 17 | * PHP (7.1+) 18 | 19 | > For PHP (5.3+) please refer to version `1.0`. 20 | 21 | 22 | Usage 23 | ----- 24 | 25 | Abstract example: 26 | 27 | ```php 28 | use Dflydev\DotAccessData\Data; 29 | 30 | $data = new Data; 31 | 32 | $data->set('a.b.c', 'C'); 33 | $data->set('a.b.d', 'D1'); 34 | $data->append('a.b.d', 'D2'); 35 | $data->set('a.b.e', ['E0', 'E1', 'E2']); 36 | 37 | // C 38 | $data->get('a.b.c'); 39 | 40 | // ['D1', 'D2'] 41 | $data->get('a.b.d'); 42 | 43 | // ['E0', 'E1', 'E2'] 44 | $data->get('a.b.e'); 45 | 46 | // true 47 | $data->has('a.b.c'); 48 | 49 | // false 50 | $data->has('a.b.d.j'); 51 | 52 | 53 | // 'some-default-value' 54 | $data->get('some.path.that.does.not.exist', 'some-default-value'); 55 | 56 | // throws a MissingPathException because no default was given 57 | $data->get('some.path.that.does.not.exist'); 58 | ``` 59 | 60 | A more concrete example: 61 | 62 | ```php 63 | use Dflydev\DotAccessData\Data; 64 | 65 | $data = new Data([ 66 | 'hosts' => [ 67 | 'hewey' => [ 68 | 'username' => 'hman', 69 | 'password' => 'HPASS', 70 | 'roles' => ['web'], 71 | ], 72 | 'dewey' => [ 73 | 'username' => 'dman', 74 | 'password' => 'D---S', 75 | 'roles' => ['web', 'db'], 76 | 'nick' => 'dewey dman', 77 | ], 78 | 'lewey' => [ 79 | 'username' => 'lman', 80 | 'password' => 'LP@$$', 81 | 'roles' => ['db'], 82 | ], 83 | ], 84 | ]); 85 | 86 | // hman 87 | $username = $data->get('hosts.hewey.username'); 88 | // HPASS 89 | $password = $data->get('hosts.hewey.password'); 90 | // ['web'] 91 | $roles = $data->get('hosts.hewey.roles'); 92 | // dewey dman 93 | $nick = $data->get('hosts.dewey.nick'); 94 | // Unknown 95 | $nick = $data->get('hosts.lewey.nick', 'Unknown'); 96 | 97 | // DataInterface instance 98 | $dewey = $data->getData('hosts.dewey'); 99 | // dman 100 | $username = $dewey->get('username'); 101 | // D---S 102 | $password = $dewey->get('password'); 103 | // ['web', 'db'] 104 | $roles = $dewey->get('roles'); 105 | 106 | // No more lewey 107 | $data->remove('hosts.lewey'); 108 | 109 | // Add DB to hewey's roles 110 | $data->append('hosts.hewey.roles', 'db'); 111 | 112 | $data->set('hosts.april', [ 113 | 'username' => 'aman', 114 | 'password' => '@---S', 115 | 'roles' => ['web'], 116 | ]); 117 | 118 | // Check if a key exists (true to this case) 119 | $hasKey = $data->has('hosts.dewey.username'); 120 | ``` 121 | 122 | `Data` may be used as an array, since it implements `ArrayAccess` interface: 123 | 124 | ```php 125 | // Get 126 | $data->get('name') === $data['name']; // true 127 | 128 | $data['name'] = 'Dewey'; 129 | // is equivalent to 130 | $data->set($name, 'Dewey'); 131 | 132 | isset($data['name']) === $data->has('name'); 133 | 134 | // Remove key 135 | unset($data['name']); 136 | ``` 137 | 138 | `/` can also be used as a path delimiter: 139 | 140 | ```php 141 | $data->set('a/b/c', 'd'); 142 | echo $data->get('a/b/c'); // "d" 143 | 144 | $data->get('a/b/c') === $data->get('a.b.c'); // true 145 | ``` 146 | 147 | License 148 | ------- 149 | 150 | This library is licensed under the MIT License - see the LICENSE file 151 | for details. 152 | 153 | 154 | Community 155 | --------- 156 | 157 | If you have questions or want to help out, join us in the 158 | [#dflydev](irc://irc.freenode.net/#dflydev) channel on irc.freenode.net. 159 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dflydev/dot-access-data", 3 | "type": "library", 4 | "description": "Given a deep data structure, access data by dot notation.", 5 | "homepage": "https://github.com/dflydev/dflydev-dot-access-data", 6 | "keywords": ["dot", "access", "data", "notation"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Dragonfly Development Inc.", 11 | "email": "info@dflydev.com", 12 | "homepage": "http://dflydev.com" 13 | }, 14 | { 15 | "name": "Beau Simensen", 16 | "email": "beau@dflydev.com", 17 | "homepage": "http://beausimensen.com" 18 | }, 19 | { 20 | "name": "Carlos Frutos", 21 | "email": "carlos@kiwing.it", 22 | "homepage": "https://github.com/cfrutos" 23 | }, 24 | { 25 | "name": "Colin O'Dell", 26 | "email": "colinodell@gmail.com", 27 | "homepage": "https://www.colinodell.com" 28 | } 29 | ], 30 | "require": { 31 | "php": "^7.1 || ^8.0" 32 | }, 33 | "require-dev": { 34 | "phpstan/phpstan": "^0.12.42", 35 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", 36 | "scrutinizer/ocular": "1.6.0", 37 | "squizlabs/php_codesniffer": "^3.5", 38 | "vimeo/psalm": "^4.0.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Dflydev\\DotAccessData\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Dflydev\\DotAccessData\\": "tests/" 48 | } 49 | }, 50 | "extra": { 51 | "branch-alias": { 52 | "dev-main": "3.x-dev" 53 | } 54 | }, 55 | "scripts": { 56 | "phpcs": "phpcs", 57 | "phpstan": "phpstan analyse", 58 | "phpunit": "phpunit --no-coverage", 59 | "psalm": "psalm", 60 | "test": [ 61 | "@phpcs", 62 | "@phpstan", 63 | "@psalm", 64 | "@phpunit" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Data.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Data implements DataInterface, ArrayAccess 25 | { 26 | private const DELIMITERS = ['.', '/']; 27 | 28 | /** 29 | * Internal representation of data data 30 | * 31 | * @var array 32 | */ 33 | protected $data; 34 | 35 | /** 36 | * Constructor 37 | * 38 | * @param array $data 39 | */ 40 | public function __construct(array $data = []) 41 | { 42 | $this->data = $data; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function append(string $key, $value = null): void 49 | { 50 | $currentValue =& $this->data; 51 | $keyPath = self::keyToPathArray($key); 52 | 53 | $endKey = array_pop($keyPath); 54 | foreach ($keyPath as $currentKey) { 55 | if (! isset($currentValue[$currentKey])) { 56 | $currentValue[$currentKey] = []; 57 | } 58 | $currentValue =& $currentValue[$currentKey]; 59 | } 60 | 61 | if (!isset($currentValue[$endKey])) { 62 | $currentValue[$endKey] = []; 63 | } 64 | 65 | if (!is_array($currentValue[$endKey])) { 66 | // Promote this key to an array. 67 | // TODO: Is this really what we want to do? 68 | $currentValue[$endKey] = [$currentValue[$endKey]]; 69 | } 70 | 71 | $currentValue[$endKey][] = $value; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function set(string $key, $value = null): void 78 | { 79 | $currentValue =& $this->data; 80 | $keyPath = self::keyToPathArray($key); 81 | 82 | $endKey = array_pop($keyPath); 83 | foreach ($keyPath as $currentKey) { 84 | if (!isset($currentValue[$currentKey])) { 85 | $currentValue[$currentKey] = []; 86 | } 87 | if (!is_array($currentValue[$currentKey])) { 88 | throw new DataException(sprintf('Key path "%s" within "%s" cannot be indexed into (is not an array)', $currentKey, self::formatPath($key))); 89 | } 90 | $currentValue =& $currentValue[$currentKey]; 91 | } 92 | $currentValue[$endKey] = $value; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function remove(string $key): void 99 | { 100 | $currentValue =& $this->data; 101 | $keyPath = self::keyToPathArray($key); 102 | 103 | $endKey = array_pop($keyPath); 104 | foreach ($keyPath as $currentKey) { 105 | if (!isset($currentValue[$currentKey])) { 106 | return; 107 | } 108 | $currentValue =& $currentValue[$currentKey]; 109 | } 110 | unset($currentValue[$endKey]); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | * 116 | * @psalm-mutation-free 117 | */ 118 | public function get(string $key, $default = null) 119 | { 120 | /** @psalm-suppress ImpureFunctionCall */ 121 | $hasDefault = \func_num_args() > 1; 122 | 123 | $currentValue = $this->data; 124 | $keyPath = self::keyToPathArray($key); 125 | 126 | foreach ($keyPath as $currentKey) { 127 | if (!is_array($currentValue) || !array_key_exists($currentKey, $currentValue)) { 128 | if ($hasDefault) { 129 | return $default; 130 | } 131 | 132 | throw new MissingPathException($key, sprintf('No data exists at the given path: "%s"', self::formatPath($keyPath))); 133 | } 134 | 135 | $currentValue = $currentValue[$currentKey]; 136 | } 137 | 138 | return $currentValue === null ? $default : $currentValue; 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | * 144 | * @psalm-mutation-free 145 | */ 146 | public function has(string $key): bool 147 | { 148 | $currentValue = $this->data; 149 | 150 | foreach (self::keyToPathArray($key) as $currentKey) { 151 | if ( 152 | !is_array($currentValue) || 153 | !array_key_exists($currentKey, $currentValue) 154 | ) { 155 | return false; 156 | } 157 | $currentValue = $currentValue[$currentKey]; 158 | } 159 | 160 | return true; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | * 166 | * @psalm-mutation-free 167 | */ 168 | public function getData(string $key): DataInterface 169 | { 170 | $value = $this->get($key); 171 | if (is_array($value) && Util::isAssoc($value)) { 172 | return new Data($value); 173 | } 174 | 175 | throw new DataException(sprintf('Value at "%s" could not be represented as a DataInterface', self::formatPath($key))); 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function import(array $data, int $mode = self::REPLACE): void 182 | { 183 | $this->data = Util::mergeAssocArray($this->data, $data, $mode); 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function importData(DataInterface $data, int $mode = self::REPLACE): void 190 | { 191 | $this->import($data->export(), $mode); 192 | } 193 | 194 | /** 195 | * {@inheritdoc} 196 | * 197 | * @psalm-mutation-free 198 | */ 199 | public function export(): array 200 | { 201 | return $this->data; 202 | } 203 | 204 | /** 205 | * {@inheritdoc} 206 | * 207 | * @return bool 208 | */ 209 | #[\ReturnTypeWillChange] 210 | public function offsetExists($key) 211 | { 212 | return $this->has($key); 213 | } 214 | 215 | /** 216 | * {@inheritdoc} 217 | * 218 | * @return mixed 219 | */ 220 | #[\ReturnTypeWillChange] 221 | public function offsetGet($key) 222 | { 223 | return $this->get($key, null); 224 | } 225 | 226 | /** 227 | * {@inheritdoc} 228 | * 229 | * @param string $key 230 | * @param mixed $value 231 | * 232 | * @return void 233 | */ 234 | #[\ReturnTypeWillChange] 235 | public function offsetSet($key, $value) 236 | { 237 | $this->set($key, $value); 238 | } 239 | 240 | /** 241 | * {@inheritdoc} 242 | * 243 | * @return void 244 | */ 245 | #[\ReturnTypeWillChange] 246 | public function offsetUnset($key) 247 | { 248 | $this->remove($key); 249 | } 250 | 251 | /** 252 | * @param string $path 253 | * 254 | * @return string[] 255 | * 256 | * @psalm-return non-empty-list 257 | * 258 | * @psalm-pure 259 | */ 260 | protected static function keyToPathArray(string $path): array 261 | { 262 | if (\strlen($path) === 0) { 263 | throw new InvalidPathException('Path cannot be an empty string'); 264 | } 265 | 266 | $path = \str_replace(self::DELIMITERS, '.', $path); 267 | 268 | return \explode('.', $path); 269 | } 270 | 271 | /** 272 | * @param string|string[] $path 273 | * 274 | * @return string 275 | * 276 | * @psalm-pure 277 | */ 278 | protected static function formatPath($path): string 279 | { 280 | if (is_string($path)) { 281 | $path = self::keyToPathArray($path); 282 | } 283 | 284 | return implode(' » ', $path); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/DataInterface.php: -------------------------------------------------------------------------------- 1 | $data 111 | * @param self::PRESERVE|self::REPLACE|self::MERGE $mode 112 | */ 113 | public function import(array $data, int $mode = self::REPLACE): void; 114 | 115 | /** 116 | * Import data from an external data into existing data 117 | * 118 | * @param DataInterface $data 119 | * @param self::PRESERVE|self::REPLACE|self::MERGE $mode 120 | */ 121 | public function importData(DataInterface $data, int $mode = self::REPLACE): void; 122 | 123 | /** 124 | * Export data as raw data 125 | * 126 | * @return array 127 | * 128 | * @psalm-mutation-free 129 | */ 130 | public function export(): array; 131 | } 132 | -------------------------------------------------------------------------------- /src/Exception/DataException.php: -------------------------------------------------------------------------------- 1 | path = $path; 29 | 30 | parent::__construct($message, $code, $previous); 31 | } 32 | 33 | public function getPath(): string 34 | { 35 | return $this->path; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | $arr 25 | * 26 | * @return bool 27 | * 28 | * @psalm-pure 29 | */ 30 | public static function isAssoc(array $arr): bool 31 | { 32 | return !count($arr) || count(array_filter(array_keys($arr), 'is_string')) == count($arr); 33 | } 34 | 35 | /** 36 | * Merge contents from one associtative array to another 37 | * 38 | * @param mixed $to 39 | * @param mixed $from 40 | * @param DataInterface::PRESERVE|DataInterface::REPLACE|DataInterface::MERGE $mode 41 | * 42 | * @return mixed 43 | * 44 | * @psalm-pure 45 | */ 46 | public static function mergeAssocArray($to, $from, int $mode = DataInterface::REPLACE) 47 | { 48 | if ($mode === DataInterface::MERGE && self::isList($to) && self::isList($from)) { 49 | return array_merge($to, $from); 50 | } 51 | 52 | if (is_array($from) && is_array($to)) { 53 | foreach ($from as $k => $v) { 54 | if (!isset($to[$k])) { 55 | $to[$k] = $v; 56 | } else { 57 | $to[$k] = self::mergeAssocArray($to[$k], $v, $mode); 58 | } 59 | } 60 | 61 | return $to; 62 | } 63 | 64 | return $mode === DataInterface::PRESERVE ? $to : $from; 65 | } 66 | 67 | /** 68 | * @param mixed $value 69 | * 70 | * @return bool 71 | * 72 | * @psalm-pure 73 | */ 74 | private static function isList($value): bool 75 | { 76 | return is_array($value) && array_values($value) === $value; 77 | } 78 | } 79 | --------------------------------------------------------------------------------