├── .cs.php ├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── composer.json ├── phpcs.xml ├── phpstan.neon └── src └── ArrayReader.php /.cs.php: -------------------------------------------------------------------------------- 1 | setUsingCache(false) 7 | ->setRiskyAllowed(true) 8 | ->setRules( 9 | [ 10 | '@PSR1' => true, 11 | '@PSR2' => true, 12 | // custom rules 13 | 'psr_autoloading' => true, 14 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_only'], // psr-5 15 | 'phpdoc_to_comment' => false, 16 | 'no_superfluous_phpdoc_tags' => false, 17 | 'array_indentation' => true, 18 | 'array_syntax' => ['syntax' => 'short'], 19 | 'cast_spaces' => ['space' => 'none'], 20 | 'concat_space' => ['spacing' => 'one'], 21 | 'compact_nullable_type_declaration' => true, 22 | 'declare_equal_normalize' => ['space' => 'single'], 23 | 'general_phpdoc_annotation_remove' => [ 24 | 'annotations' => [ 25 | 'author', 26 | 'package', 27 | ], 28 | ], 29 | 'increment_style' => ['style' => 'post'], 30 | 'list_syntax' => ['syntax' => 'short'], 31 | 'echo_tag_syntax' => ['format' => 'long'], 32 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 33 | 'phpdoc_align' => false, 34 | 'phpdoc_no_empty_return' => false, 35 | 'phpdoc_order' => true, // psr-5 36 | 'phpdoc_no_useless_inheritdoc' => false, 37 | 'protected_to_private' => false, 38 | 'yoda_style' => [ 39 | 'equal' => false, 40 | 'identical' => false, 41 | 'less_and_greater' => false 42 | ], 43 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 44 | 'ordered_imports' => [ 45 | 'sort_algorithm' => 'alpha', 46 | 'imports_order' => ['class', 'const', 'function'], 47 | ], 48 | 'single_line_throw' => false, 49 | 'declare_strict_types' => false, 50 | 'blank_line_between_import_groups' => true, 51 | 'fully_qualified_strict_types' => true, 52 | 'no_null_property_initialization' => false, 53 | 'nullable_type_declaration_for_default_null_value' => false, 54 | 'operator_linebreak' => [ 55 | 'only_booleans' => true, 56 | 'position' => 'beginning', 57 | ], 58 | 'global_namespace_import' => [ 59 | 'import_classes' => true, 60 | 'import_constants' => null, 61 | 'import_functions' => null 62 | ], 63 | 'class_definition' => [ 64 | 'space_before_parenthesis' => true, 65 | ], 66 | 'trailing_comma_in_multiline' => [ 67 | 'after_heredoc' => true, 68 | 'elements' => ['array_destructuring', 'arrays', 'match'] 69 | ], 70 | 'function_declaration' => [ 71 | 'closure_fn_spacing' => 'none', 72 | ] 73 | ] 74 | ) 75 | ->setFinder( 76 | PhpCsFixer\Finder::create() 77 | ->in(__DIR__ . '/src') 78 | ->in(__DIR__ . '/tests') 79 | ->name('*.php') 80 | ->ignoreDotFiles(true) 81 | ->ignoreVCS(true) 82 | ); 83 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ ubuntu-latest ] 11 | php-versions: [ '8.1', '8.2', '8.3', '8.4' ] 12 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v1 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extensions: mbstring, intl, zip 23 | 24 | - name: Check PHP Version 25 | run: php -v 26 | 27 | - name: Check Composer Version 28 | run: composer -V 29 | 30 | - name: Check PHP Extensions 31 | run: php -m 32 | 33 | - name: Validate composer.json and composer.lock 34 | run: composer validate 35 | 36 | - name: Install dependencies 37 | run: composer install --prefer-dist --no-progress --no-suggest 38 | 39 | - name: Run PHP CodeSniffer 40 | run: composer sniffer:check 41 | 42 | - name: Run PHPStan 43 | run: composer stan 44 | 45 | - name: Run tests 46 | if: ${{ matrix.php-versions != '8.4' }} 47 | run: composer test 48 | 49 | - name: Run tests with coverage 50 | if: ${{ matrix.php-versions == '8.4' }} 51 | run: composer test:coverage 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 odan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # selective/array-reader 2 | 3 | A strictly typed array reader for PHP. 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/github/release/selective-php/array-reader.svg)](https://packagist.org/packages/selective/array-reader) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 7 | [![Build Status](https://github.com/selective-php/array-reader/workflows/build/badge.svg)](https://github.com/selective-php/array-reader/actions) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/selective/array-reader.svg)](https://packagist.org/packages/selective/array-reader/stats) 9 | 10 | ## Requirements 11 | 12 | * PHP 8.1 - 8.4 13 | 14 | ## Installation 15 | 16 | ```bash 17 | composer require selective/array-reader 18 | ``` 19 | 20 | ## Usage 21 | 22 | You can use the `ArrayReader` to read single values from a multidimensional 23 | array by passing the path to one of the `get{type}()` and `find{type}()` methods. 24 | 25 | Each `get*() / find*()` method takes a default value as second argument. 26 | If the path cannot be found in the original array, the default is used as return value. 27 | 28 | A `get*()` method returns only the declared return type. 29 | If the default value is not given and the element cannot be found, an exception is thrown. 30 | 31 | A `find*()` method returns only the declared return type or `null`. 32 | No exception is thrown if the element cannot be found. 33 | 34 | ```php 35 | [ 41 | 'key2' => [ 42 | 'key3' => 'value1', 43 | ] 44 | ] 45 | ]); 46 | 47 | // Output: value1 48 | echo $arrayReader->getString('key1.key2.key3'); 49 | ``` 50 | 51 | ## Better Code Quality 52 | 53 | Converting complex data with simple PHP works by using a lot of type casting and `if` conditions etc. 54 | This leads to very high cyclomatic complexity and nesting depth, and thus poor "code rating". 55 | 56 | **Before**: Conditions: 10, Paths: 512, CRAP Score: 10 57 |
58 | Click to expand! 59 | 60 |
61 | 62 | **After**: Conditions: 1, Paths: 1, CRAP Score: 1 63 |
64 | Click to expand! 65 | 66 |
67 | 68 | ## Similar libraries 69 | 70 | * https://github.com/michaelpetri/typed-input 71 | * https://github.com/codeliner/array-reader 72 | * https://github.com/adbario/php-dot-notation 73 | * https://symfony.com/doc/current/components/property_access.html 74 | 75 | ## License 76 | 77 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selective/array-reader", 3 | "description": "A strictly typed array reader", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "array", 8 | "reader", 9 | "strict", 10 | "strictly", 11 | "typed", 12 | "strong" 13 | ], 14 | "homepage": "https://github.com/selective-php/array-reader", 15 | "require": { 16 | "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*", 17 | "cakephp/chronos": "^2 || ^3" 18 | }, 19 | "require-dev": { 20 | "friendsofphp/php-cs-fixer": "^3", 21 | "phpstan/phpstan": "^1 || ^2", 22 | "phpunit/phpunit": "^10", 23 | "squizlabs/php_codesniffer": "^3" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Selective\\ArrayReader\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Selective\\ArrayReader\\Test\\": "tests/" 33 | } 34 | }, 35 | "config": { 36 | "sort-packages": true 37 | }, 38 | "scripts": { 39 | "cs:check": [ 40 | "@putenv PHP_CS_FIXER_IGNORE_ENV=1", 41 | "php-cs-fixer fix --dry-run --format=txt --verbose --diff --config=.cs.php --ansi" 42 | ], 43 | "cs:fix": [ 44 | "@putenv PHP_CS_FIXER_IGNORE_ENV=1", 45 | "php-cs-fixer fix --config=.cs.php --ansi --verbose" 46 | ], 47 | "sniffer:check": "phpcs --standard=phpcs.xml", 48 | "sniffer:fix": "phpcbf --standard=phpcs.xml", 49 | "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi", 50 | "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --display-warnings --display-deprecations --no-coverage", 51 | "test:all": [ 52 | "@cs:check", 53 | "@sniffer:check", 54 | "@stan", 55 | "@test" 56 | ], 57 | "test:coverage": [ 58 | "@putenv XDEBUG_MODE=coverage", 59 | "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --display-warnings --display-deprecations --coverage-clover build/coverage/clover.xml --coverage-html build/coverage --coverage-text" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ./src 10 | ./tests 11 | 12 | 13 | 14 | 15 | warning 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | reportUnmatchedIgnoredErrors: false 4 | paths: 5 | - src -------------------------------------------------------------------------------- /src/ArrayReader.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $data; 17 | 18 | /** 19 | * The constructor. 20 | * 21 | * @param array $data Data 22 | */ 23 | public function __construct(array $data = []) 24 | { 25 | $this->data = $data; 26 | } 27 | 28 | /** 29 | * Crate instance from array. 30 | * 31 | * @param array $data The data 32 | * 33 | * @return self The new instance 34 | */ 35 | public static function createFromArray(array $data = []): self 36 | { 37 | return new static($data); 38 | } 39 | 40 | /** 41 | * Get value as integer. 42 | * 43 | * @param string $key The key 44 | * @param int|null $default The default value 45 | * 46 | * @throws InvalidArgumentException 47 | * 48 | * @return int The value 49 | */ 50 | public function getInt(string $key, ?int $default = null): int 51 | { 52 | $value = $this->find($key, $default); 53 | 54 | if ($this->isNullOrBlank($value)) { 55 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 56 | } 57 | 58 | return (int)$value; 59 | } 60 | 61 | /** 62 | * Get value as integer or null. 63 | * 64 | * @param string $key The key 65 | * @param int|null $default The default value 66 | * 67 | * @return int|null The value 68 | */ 69 | public function findInt(string $key, ?int $default = null): ?int 70 | { 71 | $value = $this->find($key, $default); 72 | 73 | if ($this->isNullOrBlank($value)) { 74 | return null; 75 | } 76 | 77 | return (int)$value; 78 | } 79 | 80 | /** 81 | * Get value as string. 82 | * 83 | * @param string $key The key 84 | * @param string|null $default The default value 85 | * 86 | * @throws InvalidArgumentException 87 | * 88 | * @return string The value 89 | */ 90 | public function getString(string $key, ?string $default = null): string 91 | { 92 | $value = $this->find($key, $default); 93 | 94 | if ($value === null) { 95 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 96 | } 97 | 98 | return (string)$value; 99 | } 100 | 101 | /** 102 | * Get value as string or null. 103 | * 104 | * @param string $key The key 105 | * @param string|null $default The default value 106 | * 107 | * @return string|null The value 108 | */ 109 | public function findString(string $key, ?string $default = null): ?string 110 | { 111 | $value = $this->find($key, $default); 112 | 113 | if ($value === null) { 114 | return null; 115 | } 116 | 117 | return (string)$value; 118 | } 119 | 120 | /** 121 | * Get value as array. 122 | * 123 | * @param string $key The key 124 | * @param array|null $default The default value 125 | * 126 | * @throws InvalidArgumentException 127 | * 128 | * @return array The value 129 | */ 130 | public function getArray(string $key, ?array $default = null): array 131 | { 132 | $value = $this->find($key, $default); 133 | 134 | if ($this->isNullOrBlank($value)) { 135 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 136 | } 137 | 138 | return (array)$value; 139 | } 140 | 141 | /** 142 | * Get value as array or null. 143 | * 144 | * @param string $key The key 145 | * @param array|null $default The default value 146 | * 147 | * @return array|null The value 148 | */ 149 | public function findArray(string $key, ?array $default = null): ?array 150 | { 151 | $value = $this->find($key, $default); 152 | 153 | if ($this->isNullOrBlank($value)) { 154 | return null; 155 | } 156 | 157 | return (array)$value; 158 | } 159 | 160 | /** 161 | * Get value as float. 162 | * 163 | * @param string $key The key 164 | * @param float|null $default The default value 165 | * 166 | * @throws InvalidArgumentException 167 | * 168 | * @return float The value 169 | */ 170 | public function getFloat(string $key, ?float $default = null): float 171 | { 172 | $value = $this->find($key, $default); 173 | 174 | if ($this->isNullOrBlank($value)) { 175 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 176 | } 177 | 178 | return (float)$value; 179 | } 180 | 181 | /** 182 | * Get value as float or null. 183 | * 184 | * @param string $key The key 185 | * @param float|null $default The default value 186 | * 187 | * @return float|null The value 188 | */ 189 | public function findFloat(string $key, ?float $default = null): ?float 190 | { 191 | $value = $this->find($key, $default); 192 | 193 | if ($this->isNullOrBlank($value)) { 194 | return null; 195 | } 196 | 197 | return (float)$value; 198 | } 199 | 200 | /** 201 | * Get value as boolean. 202 | * 203 | * @param string $key The key 204 | * @param bool|null $default The default value 205 | * 206 | * @throws InvalidArgumentException 207 | * 208 | * @return bool The value 209 | */ 210 | public function getBool(string $key, ?bool $default = null): bool 211 | { 212 | $value = $this->find($key, $default); 213 | 214 | if ($this->isNullOrBlank($value)) { 215 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 216 | } 217 | 218 | return (bool)$value; 219 | } 220 | 221 | /** 222 | * Get value as boolean or null. 223 | * 224 | * @param string $key The key 225 | * @param bool $default The default value 226 | * 227 | * @return bool|null The value 228 | */ 229 | public function findBool(string $key, ?bool $default = null): ?bool 230 | { 231 | $value = $this->find($key, $default); 232 | 233 | if ($this->isNullOrBlank($value)) { 234 | return null; 235 | } 236 | 237 | return (bool)$value; 238 | } 239 | 240 | /** 241 | * Get value as Chronos. 242 | * 243 | * @param string $key The key 244 | * @param Chronos|null $default The default value 245 | * 246 | * @throws InvalidArgumentException 247 | * 248 | * @return Chronos The value 249 | */ 250 | public function getChronos(string $key, ?Chronos $default = null): Chronos 251 | { 252 | $value = $this->find($key, $default); 253 | 254 | if ($this->isNullOrBlank($value)) { 255 | throw new InvalidArgumentException(sprintf('No value found for key "%s"', $key)); 256 | } 257 | 258 | if ($value instanceof Chronos) { 259 | return $value; 260 | } 261 | 262 | return new Chronos($value); 263 | } 264 | 265 | /** 266 | * Get value as Chronos or null. 267 | * 268 | * @param string $key The key 269 | * @param Chronos|null $default The default value 270 | * 271 | * @return Chronos|null The value 272 | */ 273 | public function findChronos(string $key, ?Chronos $default = null): ?Chronos 274 | { 275 | $value = $this->find($key, $default); 276 | 277 | if ($this->isNullOrBlank($value)) { 278 | return null; 279 | } 280 | 281 | if ($value instanceof Chronos) { 282 | return $value; 283 | } 284 | 285 | return new Chronos($value); 286 | } 287 | 288 | /** 289 | * Find mixed value. 290 | * 291 | * @param string $path The path 292 | * @param mixed|null $default The default value 293 | * 294 | * @return mixed|null The value 295 | */ 296 | public function find(string $path, mixed $default = null): mixed 297 | { 298 | if (array_key_exists($path, $this->data)) { 299 | return $this->data[$path] ?? $default; 300 | } 301 | 302 | if (!str_contains($path, '.')) { 303 | return $default; 304 | } 305 | 306 | $pathKeys = explode('.', $path); 307 | 308 | $arrayCopyOrValue = $this->data; 309 | 310 | foreach ($pathKeys as $pathKey) { 311 | if (!isset($arrayCopyOrValue[$pathKey])) { 312 | return $default; 313 | } 314 | $arrayCopyOrValue = $arrayCopyOrValue[$pathKey]; 315 | } 316 | 317 | return $arrayCopyOrValue; 318 | } 319 | 320 | /** 321 | * Return all data as array. 322 | * 323 | * @return array The data 324 | */ 325 | public function all(): array 326 | { 327 | return $this->data; 328 | } 329 | 330 | /** 331 | * Test whether a given path exists in $data. 332 | * This method uses the same path syntax as Hash::extract(). 333 | * 334 | * Checking for paths that could target more than one element will 335 | * make sure that at least one matching element exists. 336 | * 337 | * @param string $path The path to check for 338 | * 339 | * @return bool The existence of path 340 | */ 341 | public function exists(string $path): bool 342 | { 343 | $pathKeys = explode('.', $path); 344 | 345 | $arrayCopyOrValue = $this->data; 346 | 347 | foreach ($pathKeys as $pathKey) { 348 | if (!array_key_exists($pathKey, $arrayCopyOrValue)) { 349 | return false; 350 | } 351 | $arrayCopyOrValue = $arrayCopyOrValue[$pathKey]; 352 | } 353 | 354 | return true; 355 | } 356 | 357 | /** 358 | * Is empty. 359 | * 360 | * @param string $path The path 361 | * 362 | * @return bool Status 363 | */ 364 | public function isEmpty(string $path): bool 365 | { 366 | return empty($this->find($path)); 367 | } 368 | 369 | /** 370 | * Is null or blank. 371 | * 372 | * @param mixed $value The value 373 | * 374 | * @return bool The status 375 | */ 376 | private function isNullOrBlank(mixed $value): bool 377 | { 378 | return $value === null || $value === ''; 379 | } 380 | } 381 | --------------------------------------------------------------------------------