Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Commit bb06f96

Browse files
committed
TASK: Initial Commit
0 parents  commit bb06f96

18 files changed

Lines changed: 2500 additions & 0 deletions

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
charset = utf-8
6+
trim_trailing_whitespace = true
7+
insert_final_newline = true
8+
indent_style = space
9+
indent_size = 4
10+
11+
[*.neon]
12+
indent_style = tab
13+
14+
[*.md]
15+
trim_trailing_whitespace = false
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Quality Assurance
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
quality-assurance:
7+
runs-on: ubuntu-22.04
8+
steps:
9+
- name: Checkout
10+
uses: actions/checkout@v2
11+
12+
- name: Setup PHP
13+
uses: shivammathur/setup-php@v2
14+
with:
15+
php-version: '8.1'
16+
tools: phpcs
17+
18+
- name: Get composer cache directory
19+
id: composercache
20+
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
21+
22+
- name: Cache dependencies
23+
uses: actions/cache@v2
24+
with:
25+
path: ${{ steps.composercache.outputs.dir }}
26+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
27+
restore-keys: ${{ runner.os }}-composer-
28+
29+
- name: Install composer dependencies
30+
run: |
31+
composer install
32+
33+
- name: Check Code Quality (PHP Code Sniffer)
34+
run: |
35+
composer lint
36+
37+
- name: Static Code Analysis (PHPStan)
38+
run: |
39+
composer lint
40+
41+
- name: Unit Tests (PHPUnit)
42+
run: |
43+
composer test

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
vendor/
2+
build/coverage-report/
3+
composer.lock
4+
.phpunit.result.cache
5+
.idea

README.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# PackageFactory.Extractor
2+
3+
> A fluent interface that allows to validate primitive PHP data structures while also reading them
4+
5+
## Installation
6+
7+
```
8+
composer require --dev packagefactory/extractor
9+
```
10+
11+
## Usage
12+
13+
Let's say, you have a PHP-native array structure like this one:
14+
15+
```php
16+
$configuration = [
17+
'mailer' => [
18+
'transport' => 'smtp',
19+
'host' => 'smtp.example.com',
20+
'port' => 465
21+
]
22+
];
23+
```
24+
25+
It contains configuration for a mailing service. In a lot of PHP projects, configuration comes in this format, usually by being parsed from YAML or JSON sources. While these formats are nicely readable and writable, the result PHP array data structure is completely exempt from type safety.
26+
27+
It is much more desirable to handle the given configuration using a value object like this one:
28+
29+
```php
30+
final class MailerConfiguration
31+
{
32+
private function __construct(
33+
public readonly MailerTransport $transport,
34+
public readonly string $host,
35+
public readonly int $port
36+
) {
37+
}
38+
}
39+
```
40+
41+
To convert the array structure into this object, it may be suitable to write a static factory method:
42+
43+
```php
44+
final class MailerConfiguration
45+
{
46+
/* ... */
47+
48+
public static function fromArray(array $array): self
49+
{
50+
if (!isset($array['transport']) || !is_string($array['transport'])) {
51+
throw new \Exception('Transport must be a string!');
52+
}
53+
54+
if (!isset($array['host']) || !is_string($array['host'])) {
55+
throw new \Exception('Host must be a string!');
56+
}
57+
58+
if (!isset($array['port']) || !is_int($array['port'])) {
59+
throw new \Exception('Port must be an integer!');
60+
}
61+
62+
return new self(
63+
transport: MailerTransport::from($array['transport']),
64+
host: $array['host'],
65+
port: $array['port']
66+
);
67+
}
68+
}
69+
```
70+
71+
Unfortunately, this is a lot of code to write and it would become even more, if we'd actually like to have more helpful error messages.
72+
73+
This is where the `Extractor` comes in. Using the `Extractor` API, we can write a static factory method like this:
74+
75+
```php
76+
final class MailerConfiguration
77+
{
78+
/* ... */
79+
80+
public static function fromExtractor(Extractor $extractor): self
81+
{
82+
return new self(
83+
transport: MailerTransport::from($extractor['transport']->string()),
84+
host: $extractor['host']->string(),
85+
port: $extractor['port']->int()
86+
);
87+
}
88+
}
89+
```
90+
91+
The extractor handles the runtime type checks for us and throws helpful error messages, if the datastructure doesn't follow our assumptions.
92+
93+
To complete the example from the beginning:
94+
95+
```php
96+
$configuration = [
97+
'mailer' => [
98+
'transport' => 'smtp',
99+
'host' => 'smtp.example.com',
100+
'port' => 465
101+
]
102+
];
103+
104+
$mailerConfiguration = MailerConfiguration::fromExtractor(
105+
Extractor::for($configuration)['mailer']
106+
);
107+
```
108+
109+
## API
110+
111+
### Type Guards
112+
113+
#### `bool` and `boolOrNull`
114+
115+
```php
116+
Extractor::for(true)->bool(); // returns `true`
117+
Extractor::for(false)->bool(); // returns `false`
118+
Extractor::for(true)->boolOrNull(); // returns `true`
119+
Extractor::for(false)->boolOrNull(); // returns `false`
120+
Extractor::for(null)->boolOrNull(); // returns `null`
121+
```
122+
123+
Checks if the data given to the extractor is a boolean and returns it if thats the case. When `boolOrNull` is used, `null` will pass as well.
124+
125+
#### `int` and `intOrNull`
126+
127+
```php
128+
Extractor::for(42)->int(); // returns `42`
129+
Extractor::for(42)->intOrNull(); // returns `42`
130+
Extractor::for(null)->intOrNull(); // returns `null`
131+
```
132+
133+
Checks if the data given to the extractor is an integer and returns it if thats the case. When `intOrNull` is used, `null` will pass as well.
134+
135+
#### `float` and `floatOrNull`
136+
137+
```php
138+
Extractor::for(47.11)->float(); // returns `47.11`
139+
Extractor::for(47.11)->floatOrNull(); // returns `47.11`
140+
Extractor::for(null)->floatOrNull(); // returns `null`
141+
```
142+
143+
Checks if the data given to the extractor is a float and returns it if thats the case. When `floatOrNull` is used, `null` will pass as well.
144+
145+
#### `intOrFloat` and `intOrFloatOrNull`
146+
147+
```php
148+
Extractor::for(42)->intOrFloat(); // returns `42`
149+
Extractor::for(47.11)->intOrFloat(); // returns `47.11`
150+
Extractor::for(42)->intOrfloatOrNull(); // returns `42`
151+
Extractor::for(47.11)->intOrfloatOrNull(); // returns `47.11`
152+
Extractor::for(null)->intOrfloatOrNull(); // returns `null`
153+
```
154+
155+
In `JSON` there's no distinction between integer and float types. Everything is just a `number`. These two methods check if the data given to the extractor is a float or an integer (and therefore a `number`) and returns it if thats the case. When `intOrfloatOrNull` is used, `null` will pass as well.
156+
157+
#### `string` and `stringOrNull`
158+
159+
```php
160+
Extractor::for('string')->string(); // returns `"string"`
161+
Extractor::for('string')->stringOrNull(); // returns `"string"`
162+
Extractor::for(null)->stringOrNull(); // returns `null`
163+
```
164+
165+
Checks if the data given to the extractor is a string and returns it if thats the case. When `stringOrNull` is used, `null` will pass as well.
166+
167+
#### `array` and `arrayOrNull`
168+
169+
```php
170+
Extractor::for([])->array(); // returns `[]`
171+
Extractor::for([])->arrayOrNull(); // returns `[]`
172+
Extractor::for(null)->arrayOrNull(); // returns `null`
173+
```
174+
175+
Checks if the data given to the extractor is an array and returns it if thats the case. When `stringOrNull` is used, `null` will pass as well.
176+
177+
### Array Access
178+
179+
In order to deal with nested array structures, `Extractor` implements the `\ArrayAccess` interface.
180+
181+
Given you have an `Extractor` that wraps an array, when you access a key, you'll receive the value for that key wrapped in another `Extractor` instance:
182+
183+
```php
184+
$extractor = Extractor::for([ 'key' => 'value' ]);
185+
$extractor['key']->string(); // returns `"value"`
186+
$extractor['key']->int(); // throws
187+
```
188+
189+
If you access an unknown key, it'll be treated like `Extractor::for(null)`:
190+
191+
```php
192+
$extractor['unknown key']->stringOrNull(); // returns `null`
193+
$extractor['unknown key']->string(); // throws
194+
```
195+
196+
If you access a key on something other than an array, `Extractor` will throw:
197+
198+
```php
199+
$extractor = Extractor::for('This is not an array...');
200+
$extractor['key']; // throws
201+
```
202+
203+
#### `getPath`
204+
205+
Each `Extractor` instance provides you with the access path by which it has been retrieved:
206+
207+
```php
208+
$extractor = Extractor::for([
209+
'some' => [
210+
'deep' => [
211+
'path' => '1234'
212+
]
213+
]
214+
]);
215+
216+
$nested = $extractor['some']['deep']['path'];
217+
var_dump($nested->getPath());
218+
// Output:
219+
// array(3) {
220+
// [0] =>
221+
// string(4) "some"
222+
// [1] =>
223+
// string(4) "deep"
224+
// [2] =>
225+
// string(4) "path"
226+
// }
227+
```
228+
229+
### Iterable
230+
231+
`Extractor` implements the `\IterableAggregate` interface, which allows you to loop over it using `foreach`:
232+
233+
```php
234+
foreach (Extractor::for([ 'key' => 'value' ]) as $key => $value) {
235+
$key->string(); // returns `"key"`
236+
$value->string(); // returns `"value"`
237+
238+
$key->int(); // throws
239+
}
240+
```
241+
242+
As you see, both `$key` and `$value` are themselves instances of `Extractor`.
243+
244+
If you try to iterate over an `Extractor` that wraps something other than an array, the `Extractor` will throw:
245+
246+
```php
247+
foreach (Extractor::for('This is not an array...') as $key => $value) { // throws
248+
}
249+
```
250+
251+
### Error Handling
252+
253+
`Extractor` may throw instances of `ExtractorException`. Each `ExtractorException` carries the access path by which the throwing `Extractor` has been retrieved and tries to provide a helpful error message:
254+
255+
```php
256+
$extractor = Extractor::for([
257+
'some' => [
258+
'deep' => [
259+
'path' => '1234'
260+
]
261+
]
262+
]);
263+
264+
try {
265+
$extractor['some']['deep']['path']->int();
266+
} catch (ExtractorException $e) {
267+
var_dump($e->getPath());
268+
// Output:
269+
// array(3) {
270+
// [0] =>
271+
// string(4) "some"
272+
// [1] =>
273+
// string(4) "deep"
274+
// [2] =>
275+
// string(4) "path"
276+
// }
277+
278+
var_dump($e->getMessage());
279+
// Output:
280+
// string(65) "Value was expected to be of type int, got string("1234") instead."
281+
}
282+
```
283+
284+
## Contribution
285+
286+
We will gladly accept contributions. Please send us pull requests.
287+
288+
## License
289+
290+
see [LICENSE](./LICENSE)

composer.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "packagefactory/extractor",
3+
"type": "library",
4+
"description": "A fluent interface that allows to validate primitive PHP data structures while also reading them",
5+
"license": [
6+
"GPL-3.0-or-later"
7+
],
8+
"scripts": {
9+
"cleanup": [
10+
"rm -rf build",
11+
"rm -rf vendor",
12+
"rm -f composer.lock"
13+
],
14+
"lint": "phpcs --standard=PSR2 --extensions=php src/",
15+
"analyse": "phpstan analyse --level 8 src tests",
16+
"test": "phpunit --enforce-time-limit --bootstrap vendor/autoload.php --testdox tests --coverage-html build/coverage-report --whitelist src"
17+
},
18+
"require": {
19+
"php": ">=8.1"
20+
},
21+
"require-dev": {
22+
"phpunit/phpunit": "^9.5",
23+
"phpstan/phpstan": "^1.9",
24+
"squizlabs/php_codesniffer": "^3.7"
25+
},
26+
"autoload": {
27+
"psr-4": {
28+
"PackageFactory\\Extractor\\": "src"
29+
}
30+
},
31+
"autoload-dev": {
32+
"psr-4": {
33+
"PackageFactory\\Extractor\\Tests\\": "tests"
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)