├── .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 | [](https://packagist.org/packages/selective/array-reader)
6 | [](LICENSE)
7 | [](https://github.com/selective-php/array-reader/actions)
8 | [](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 |
--------------------------------------------------------------------------------