This repository has been archived by the owner on Aug 28, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit bb06f96
Showing
18 changed files
with
2,500 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
root = true | ||
|
||
[*] | ||
end_of_line = lf | ||
charset = utf-8 | ||
trim_trailing_whitespace = true | ||
insert_final_newline = true | ||
indent_style = space | ||
indent_size = 4 | ||
|
||
[*.neon] | ||
indent_style = tab | ||
|
||
[*.md] | ||
trim_trailing_whitespace = false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
name: Quality Assurance | ||
|
||
on: [push, pull_request] | ||
|
||
jobs: | ||
quality-assurance: | ||
runs-on: ubuntu-22.04 | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
|
||
- name: Setup PHP | ||
uses: shivammathur/setup-php@v2 | ||
with: | ||
php-version: '8.1' | ||
tools: phpcs | ||
|
||
- name: Get composer cache directory | ||
id: composercache | ||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" | ||
|
||
- name: Cache dependencies | ||
uses: actions/cache@v2 | ||
with: | ||
path: ${{ steps.composercache.outputs.dir }} | ||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} | ||
restore-keys: ${{ runner.os }}-composer- | ||
|
||
- name: Install composer dependencies | ||
run: | | ||
composer install | ||
- name: Check Code Quality (PHP Code Sniffer) | ||
run: | | ||
composer lint | ||
- name: Static Code Analysis (PHPStan) | ||
run: | | ||
composer lint | ||
- name: Unit Tests (PHPUnit) | ||
run: | | ||
composer test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
vendor/ | ||
build/coverage-report/ | ||
composer.lock | ||
.phpunit.result.cache | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
# PackageFactory.Extractor | ||
|
||
> A fluent interface that allows to validate primitive PHP data structures while also reading them | ||
## Installation | ||
|
||
``` | ||
composer require --dev packagefactory/extractor | ||
``` | ||
|
||
## Usage | ||
|
||
Let's say, you have a PHP-native array structure like this one: | ||
|
||
```php | ||
$configuration = [ | ||
'mailer' => [ | ||
'transport' => 'smtp', | ||
'host' => 'smtp.example.com', | ||
'port' => 465 | ||
] | ||
]; | ||
``` | ||
|
||
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. | ||
|
||
It is much more desirable to handle the given configuration using a value object like this one: | ||
|
||
```php | ||
final class MailerConfiguration | ||
{ | ||
private function __construct( | ||
public readonly MailerTransport $transport, | ||
public readonly string $host, | ||
public readonly int $port | ||
) { | ||
} | ||
} | ||
``` | ||
|
||
To convert the array structure into this object, it may be suitable to write a static factory method: | ||
|
||
```php | ||
final class MailerConfiguration | ||
{ | ||
/* ... */ | ||
|
||
public static function fromArray(array $array): self | ||
{ | ||
if (!isset($array['transport']) || !is_string($array['transport'])) { | ||
throw new \Exception('Transport must be a string!'); | ||
} | ||
|
||
if (!isset($array['host']) || !is_string($array['host'])) { | ||
throw new \Exception('Host must be a string!'); | ||
} | ||
|
||
if (!isset($array['port']) || !is_int($array['port'])) { | ||
throw new \Exception('Port must be an integer!'); | ||
} | ||
|
||
return new self( | ||
transport: MailerTransport::from($array['transport']), | ||
host: $array['host'], | ||
port: $array['port'] | ||
); | ||
} | ||
} | ||
``` | ||
|
||
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. | ||
|
||
This is where the `Extractor` comes in. Using the `Extractor` API, we can write a static factory method like this: | ||
|
||
```php | ||
final class MailerConfiguration | ||
{ | ||
/* ... */ | ||
|
||
public static function fromExtractor(Extractor $extractor): self | ||
{ | ||
return new self( | ||
transport: MailerTransport::from($extractor['transport']->string()), | ||
host: $extractor['host']->string(), | ||
port: $extractor['port']->int() | ||
); | ||
} | ||
} | ||
``` | ||
|
||
The extractor handles the runtime type checks for us and throws helpful error messages, if the datastructure doesn't follow our assumptions. | ||
|
||
To complete the example from the beginning: | ||
|
||
```php | ||
$configuration = [ | ||
'mailer' => [ | ||
'transport' => 'smtp', | ||
'host' => 'smtp.example.com', | ||
'port' => 465 | ||
] | ||
]; | ||
|
||
$mailerConfiguration = MailerConfiguration::fromExtractor( | ||
Extractor::for($configuration)['mailer'] | ||
); | ||
``` | ||
|
||
## API | ||
|
||
### Type Guards | ||
|
||
#### `bool` and `boolOrNull` | ||
|
||
```php | ||
Extractor::for(true)->bool(); // returns `true` | ||
Extractor::for(false)->bool(); // returns `false` | ||
Extractor::for(true)->boolOrNull(); // returns `true` | ||
Extractor::for(false)->boolOrNull(); // returns `false` | ||
Extractor::for(null)->boolOrNull(); // returns `null` | ||
``` | ||
|
||
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. | ||
|
||
#### `int` and `intOrNull` | ||
|
||
```php | ||
Extractor::for(42)->int(); // returns `42` | ||
Extractor::for(42)->intOrNull(); // returns `42` | ||
Extractor::for(null)->intOrNull(); // returns `null` | ||
``` | ||
|
||
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. | ||
|
||
#### `float` and `floatOrNull` | ||
|
||
```php | ||
Extractor::for(47.11)->float(); // returns `47.11` | ||
Extractor::for(47.11)->floatOrNull(); // returns `47.11` | ||
Extractor::for(null)->floatOrNull(); // returns `null` | ||
``` | ||
|
||
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. | ||
|
||
#### `intOrFloat` and `intOrFloatOrNull` | ||
|
||
```php | ||
Extractor::for(42)->intOrFloat(); // returns `42` | ||
Extractor::for(47.11)->intOrFloat(); // returns `47.11` | ||
Extractor::for(42)->intOrfloatOrNull(); // returns `42` | ||
Extractor::for(47.11)->intOrfloatOrNull(); // returns `47.11` | ||
Extractor::for(null)->intOrfloatOrNull(); // returns `null` | ||
``` | ||
|
||
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. | ||
|
||
#### `string` and `stringOrNull` | ||
|
||
```php | ||
Extractor::for('string')->string(); // returns `"string"` | ||
Extractor::for('string')->stringOrNull(); // returns `"string"` | ||
Extractor::for(null)->stringOrNull(); // returns `null` | ||
``` | ||
|
||
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. | ||
|
||
#### `array` and `arrayOrNull` | ||
|
||
```php | ||
Extractor::for([])->array(); // returns `[]` | ||
Extractor::for([])->arrayOrNull(); // returns `[]` | ||
Extractor::for(null)->arrayOrNull(); // returns `null` | ||
``` | ||
|
||
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. | ||
|
||
### Array Access | ||
|
||
In order to deal with nested array structures, `Extractor` implements the `\ArrayAccess` interface. | ||
|
||
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: | ||
|
||
```php | ||
$extractor = Extractor::for([ 'key' => 'value' ]); | ||
$extractor['key']->string(); // returns `"value"` | ||
$extractor['key']->int(); // throws | ||
``` | ||
|
||
If you access an unknown key, it'll be treated like `Extractor::for(null)`: | ||
|
||
```php | ||
$extractor['unknown key']->stringOrNull(); // returns `null` | ||
$extractor['unknown key']->string(); // throws | ||
``` | ||
|
||
If you access a key on something other than an array, `Extractor` will throw: | ||
|
||
```php | ||
$extractor = Extractor::for('This is not an array...'); | ||
$extractor['key']; // throws | ||
``` | ||
|
||
#### `getPath` | ||
|
||
Each `Extractor` instance provides you with the access path by which it has been retrieved: | ||
|
||
```php | ||
$extractor = Extractor::for([ | ||
'some' => [ | ||
'deep' => [ | ||
'path' => '1234' | ||
] | ||
] | ||
]); | ||
|
||
$nested = $extractor['some']['deep']['path']; | ||
var_dump($nested->getPath()); | ||
// Output: | ||
// array(3) { | ||
// [0] => | ||
// string(4) "some" | ||
// [1] => | ||
// string(4) "deep" | ||
// [2] => | ||
// string(4) "path" | ||
// } | ||
``` | ||
|
||
### Iterable | ||
|
||
`Extractor` implements the `\IterableAggregate` interface, which allows you to loop over it using `foreach`: | ||
|
||
```php | ||
foreach (Extractor::for([ 'key' => 'value' ]) as $key => $value) { | ||
$key->string(); // returns `"key"` | ||
$value->string(); // returns `"value"` | ||
|
||
$key->int(); // throws | ||
} | ||
``` | ||
|
||
As you see, both `$key` and `$value` are themselves instances of `Extractor`. | ||
|
||
If you try to iterate over an `Extractor` that wraps something other than an array, the `Extractor` will throw: | ||
|
||
```php | ||
foreach (Extractor::for('This is not an array...') as $key => $value) { // throws | ||
} | ||
``` | ||
|
||
### Error Handling | ||
|
||
`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: | ||
|
||
```php | ||
$extractor = Extractor::for([ | ||
'some' => [ | ||
'deep' => [ | ||
'path' => '1234' | ||
] | ||
] | ||
]); | ||
|
||
try { | ||
$extractor['some']['deep']['path']->int(); | ||
} catch (ExtractorException $e) { | ||
var_dump($e->getPath()); | ||
// Output: | ||
// array(3) { | ||
// [0] => | ||
// string(4) "some" | ||
// [1] => | ||
// string(4) "deep" | ||
// [2] => | ||
// string(4) "path" | ||
// } | ||
|
||
var_dump($e->getMessage()); | ||
// Output: | ||
// string(65) "Value was expected to be of type int, got string("1234") instead." | ||
} | ||
``` | ||
|
||
## Contribution | ||
|
||
We will gladly accept contributions. Please send us pull requests. | ||
|
||
## License | ||
|
||
see [LICENSE](./LICENSE) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
{ | ||
"name": "packagefactory/extractor", | ||
"type": "library", | ||
"description": "A fluent interface that allows to validate primitive PHP data structures while also reading them", | ||
"license": [ | ||
"GPL-3.0-or-later" | ||
], | ||
"scripts": { | ||
"cleanup": [ | ||
"rm -rf build", | ||
"rm -rf vendor", | ||
"rm -f composer.lock" | ||
], | ||
"lint": "phpcs --standard=PSR2 --extensions=php src/", | ||
"analyse": "phpstan analyse --level 8 src tests", | ||
"test": "phpunit --enforce-time-limit --bootstrap vendor/autoload.php --testdox tests --coverage-html build/coverage-report --whitelist src" | ||
}, | ||
"require": { | ||
"php": ">=8.1" | ||
}, | ||
"require-dev": { | ||
"phpunit/phpunit": "^9.5", | ||
"phpstan/phpstan": "^1.9", | ||
"squizlabs/php_codesniffer": "^3.7" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"PackageFactory\\Extractor\\": "src" | ||
} | ||
}, | ||
"autoload-dev": { | ||
"psr-4": { | ||
"PackageFactory\\Extractor\\Tests\\": "tests" | ||
} | ||
} | ||
} |
Oops, something went wrong.