Skip to content

Commit 57d0c01

Browse files
authored
Merge pull request #68 from spiks/strict-static-analysis-typing
Strict typing for Psalm and PHPStan
2 parents 8efd848 + cb6d660 commit 57d0c01

32 files changed

+726
-870
lines changed

.github/workflows/tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
phpunit:
77
strategy:
88
matrix:
9-
php: ['8.0', '8.1']
9+
php: ['8.1']
1010

1111
name: PHPUnit (PHP ${{ matrix.php }})
1212
runs-on: ubuntu-latest

.php-cs-fixer.php

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
$rules['single_line_throw'] = false;
8181
$rules['static_lambda'] = true;
8282
$rules['use_arrow_functions'] = true;
83+
$rules['no_superfluous_phpdoc_tags'] = false;
84+
$rules['phpdoc_to_comment'] = [
85+
'ignored_tags' => ['psalm-suppress'],
86+
];
8387

8488
return (new PhpCsFixer\Config())
8589
->setRules($rules)

.run/PHPUnit.run.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<component name="ProjectRunConfigurationManager">
22
<configuration default="false" name="PHPUnit" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
33
<CommandLine workingDirectory="$PROJECT_DIR$" />
4-
<TestRunner configuration_file="/var/www/html/phpunit.xml" scope="XML" />
4+
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" scope="XML" />
55
<method v="2" />
66
</configuration>
7-
</component>
7+
</component>

.run/Run Psalm.run.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<component name="ProjectRunConfigurationManager">
2-
<configuration default="false" name="Run Psalm" type="PhpLocalRunConfigurationType" factoryName="PHP Console" path="$PROJECT_DIR$/vendor/bin/psalm">
2+
<configuration default="false" name="Run Psalm" type="PhpLocalRunConfigurationType" factoryName="PHP Console" path="$PROJECT_DIR$/vendor/bin/psalm" scriptParameters="--output-format=phpstorm">
33
<CommandLine workingDirectory="$PROJECT_DIR$" />
44
<method v="2" />
55
</configuration>

README.md

+18-114
Original file line numberDiff line numberDiff line change
@@ -2,135 +2,39 @@
22

33
Denormalizes and validates any kind of user input, so it may be easily used in:
44

5-
- HTML forms
6-
- APIs
7-
- console command arguments
8-
9-
... and in a lot of other scenarios.
5+
- HTML forms,
6+
- APIs,
7+
- console command arguments,
8+
- ... and in a lot of other scenarios.
109

1110
## Installation
1211

1312
_**Warning:** At this moment the library is being tested in real projects to detect possible problems in its design, so API changes are possible. Please wait for stable version._
1413

15-
PHP 8.0 or newer is required. The library is available in [Packagist](https://packagist.org/packages/spiks/user-input-processor) and may be installed with Composer:
14+
PHP 8.1 or newer is required. The library is available in [Packagist](https://packagist.org/packages/spiks/user-input-processor) and may be installed with Composer:
1615

1716
```console
1817
composer require spiks/user-input-processor
1918
```
2019

21-
## Conception
22-
23-
### Denormalizer
24-
25-
Denormalizer is something that should be used to validate and denormalize data. It may also re-use other denormalizers, which should be passed via [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection).
26-
27-
- If denormalization was successful, denormalizer may return anything: unmodified data, [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) or [value object](https://en.wikipedia.org/wiki/Value_object).
28-
- If validation error happened (e.g. email has invalid format), denormalizer throws [`ValidationError`](src/Exception/ValidationError.php) exception that has [`ConstraintViolationCollection`](src/ConstraintViolation/ConstraintViolationCollection.php).
29-
30-
The library is bundled with some basic denormalizers for each type that may appear in JSON. Most of them come with validation options inspired by [JSON Schema specification](https://json-schema.org/specification.html). Opinionated denormalizers and constraint violations for emails, phone numbers, IP addresses and for other cases are out of scope of the library.
31-
32-
### Constraint violation
33-
34-
Constraint violation object describes which field contains invalid value. [`ConstraintViolationInterface`](src/ConstraintViolation/ConstraintViolationInterface.php) has several public methods:
35-
36-
- `public static function getType(): string` — constraint violation type. It is a string identifier of an error (e.g. `string_is_too_long`).
37-
- `public function getDescription(): string` — human-readable description of an error. Content of this field is a message for development purposes, and it's not intended to be shown to the end user.
38-
- `public function getPointer(): Pointer` — path to the invalid property.
39-
40-
Any class implementing the interface may add its own public methods specific to its kind of constraint. For example, class [`StringIsTooLong`](src/ConstraintViolation/StringIsTooLong.php) has extra public method `public function getMaxLength(): int` that allows to get max length from the violation.
41-
42-
### Pointer
43-
44-
Every denormalizer and constraint violation accepts [`Pointer`](src/Pointer.php) as an argument. Pointer is a special object that contains path to your property. In most cases you will create only one pointer per form as `Pointer::empty()` for root object denormalizer.
45-
46-
Pointer may be easily converted to something specific to be shown to your client applications. For example, you may serialize it to [JSON Pointer](https://tools.ietf.org/html/rfc6901):
47-
48-
```php
49-
public function getJsonPointer(Pointer $pointer): string
50-
{
51-
$jsonPointer = '';
52-
53-
foreach ($pointer->propertyPath as $pathItem) {
54-
$jsonPointer .= '/' . $pathItem;
55-
}
56-
57-
return $jsonPointer;
58-
}
59-
```
60-
61-
Converting pointers to strings is out of scope of the library, so you should do it by yourself on another abstraction layer.
62-
63-
## Examples
64-
65-
### Your own object denormalizer
66-
67-
In most cases you will want to create your own object types. For example, it may be user profile structure that has display name, username and optional contact email. To implement such denormalizer that returns `UserProfileData` value object, you may write something like this:
68-
69-
```php
70-
declare(strict_types=1);
71-
72-
namespace App\Denormalizer;
73-
74-
use App\Denormalizer\DisplayNameDenormalizer;
75-
use App\Denormalizer\EmailDenormalizer;
76-
use App\Denormalizer\UsernameDenormalizer;
77-
use App\ValueObject\UserProfileData;
78-
use Spiks\UserInputProcessor\Denormalizer\ObjectDenormalizer;
79-
use Spiks\UserInputProcessor\ObjectField;
80-
use Spiks\UserInputProcessor\ObjectStaticFields;
81-
use Spiks\UserInputProcessor\Pointer;
82-
83-
final class UserProfileDenormalizer {
84-
public function __construct(
85-
private ObjectDenormalizer $objectDenormalizer,
86-
private EmailDenormalizer $emailDenormalizer,
87-
private DisplayNameDenormalizer $displayNameDenormalizer,
88-
private UsernameDenormalizer $usernameDenormalizer,
89-
) {}
90-
91-
public function denormalize(
92-
mixed $data
93-
): UserProfileData {
94-
$processedData = $this->objectDenormalizer->denormalize(
95-
$data,
96-
Pointer::empty(),
97-
new ObjectStaticFields([
98-
'contactEmail' => new ObjectField(
99-
static fn (mixed $fieldData, Pointer $fieldPointer) => $this->emailDenormalizer->denormalize($fieldData, $fieldPointer),
100-
isMandatory: true,
101-
),
102-
'displayName' => new ObjectField(
103-
static fn (mixed $fieldData, Pointer $fieldPointer) => $this->displayNameDenormalizer->denormalize($fieldData, $fieldPointer),
104-
isMandatory: true,
105-
),
106-
'username' => new ObjectField(
107-
static fn (mixed $fieldData, Pointer $fieldPointer) => $this->usernameDenormalizer->denormalize($fieldData, $fieldPointer),
108-
isMandatory: true,
109-
),
110-
]),
111-
);
112-
113-
return new UserProfileData(
114-
$processedData['contactEmail'],
115-
$processedData['displayName'],
116-
$processedData['username'],
117-
);
118-
}
119-
}
120-
```
20+
## Features
12121

122-
[`ObjectDenormalizer`](src/Denormalizer/ObjectDenormalizer.php) accepts data (variable `$data`) in any format in the first argument. The second argument accepts pointer. And the third one allows us to describe structure of input data and denormalization rules for each field using [`ObjectStaticFields`](src/ObjectStaticFields.php) object. This object accepts associative array: key is field name and value is [`ObjectField`](src/ObjectField.php). `ObjectField` accepts callable as first argument, object denormalizer passes field's data and its pointer to this callable, so you may simply pass them into denormalizer as shown in the example above. `isMandatory` means the property must be presented in request payload, even if it has `null` value. If `isMandatory` is `false`, client application is allowed to omit the field from request, and `$processedData` variable will miss such key too.
22+
The main principles behind the library:
12323

124-
## FAQ
24+
- **Keep it simple**. The library implements only necessary subset of features. It's easy to extend the library to add more. The library is designed as a base for your own implementation, it's not swiss knife that tries to do everything.
25+
- **Full type coverage for static analysis tools.** Public API and the library internals follow the strictest rules of [Psalm](https://psalm.dev) and [PHPStan](https://phpstan.org) - most popular static analysis tools for PHP. We are designing the library keeping type safety in mind. You will appreciate it if you are using static analysis in your projects too.
12526

126-
### How to get localized validation error message for user?
27+
## Motivation
12728

128-
There is no such functionality out-of-the-box, because formatting error messages for end-user is not something denormalizer and validator should do. It should be implemented on another abstraction layer. It should be a method in another service that accepts [`ConstraintViolationInterface`](src/ConstraintViolation/ConstraintViolationInterface.php) and returns localized error message for user.
29+
In our internal projects we use Symfony framework which offers [Symfony Forms](https://symfony.com/doc/current/forms.html) package for user input validation. Symfony Forms has a lot if disadvantages:
12930

130-
`public function getDescription(): string` method exists only for debugging and logging purposes. This value is not recommended being rendered in UI because some constraints may contain very nerd messages like [`ValueDoesNotMatchRegex`](src/ConstraintViolation/ValueDoesNotMatchRegex.php) violation has:
31+
- Its internals are very complex,
32+
- It's designed only for HTML forms, and it's not suitable for JSON APIs,
33+
- It's pain to use Symfony Forms with static analysis tools,
34+
- Difficult to maintain forms with complex logic within.
13135

132-
> Property does not match regex "/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/".
36+
There aren't lots of alternative solutions for user input validation. That's why we decided to create our own.
13337

134-
### Why [`ValidationError`](src/Exception/ValidationError.php) exception contains [`ConstraintViolationCollection`](src/ConstraintViolation/ConstraintViolationCollection.php) (collection of constraint violations), not a single violation?
38+
## Usage
13539

136-
Your denormalizers should return as much constraint violations as possible in one time for better user experience. Check out simple [`StringDenormalizer`](src/Denormalizer/StringDenormalizer.php) to see how it may be implemented in your denormalizers.
40+
This section will be written when we will be sure API and design is stable enough.

composer.json

+11-8
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,9 @@
2525
"docs": "https://github.com/spiks/user-input-processor/blob/main/README.md"
2626
},
2727
"require": {
28-
"php": ">= 8.0",
28+
"php": ">= 8.1",
2929
"ext-mbstring": "*"
3030
},
31-
"require-dev": {
32-
"friendsofphp/php-cs-fixer": "^3.4",
33-
"jetbrains/phpstorm-attributes": "^1.0",
34-
"phpstan/phpstan": "^1.2",
35-
"phpunit/phpunit": "^9.5",
36-
"vimeo/psalm": "^4.15"
37-
},
3831
"config": {
3932
"sort-packages": true
4033
},
@@ -47,5 +40,15 @@
4740
"psr-4": {
4841
"Tests\\Spiks\\UserInputProcessor\\": "tests/"
4942
}
43+
},
44+
"require-dev": {
45+
"friendsofphp/php-cs-fixer": "^3.5",
46+
"jetbrains/phpstorm-attributes": "^1.0",
47+
"phpstan/phpstan": "^1.4",
48+
"phpstan/phpstan-phpunit": "^1.0",
49+
"phpstan/phpstan-strict-rules": "^1.1",
50+
"phpunit/phpunit": "^9.5",
51+
"psalm/plugin-phpunit": "^0.16.1",
52+
"vimeo/psalm": "^4.20"
5053
}
5154
}

0 commit comments

Comments
 (0)