-
-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(type) Provides an initial documentation document for the Type co…
…mponent
- Loading branch information
Showing
1 changed file
with
201 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,201 @@ | ||
# Type | ||
|
||
## Introduction | ||
|
||
The type component provides a set functions to ensure that a given value is of a specific type. | ||
It aims to provide a solution for the [Parse, Don't Validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) problem. | ||
|
||
## Usage | ||
|
||
```php | ||
use Psl; | ||
use Psl\Type; | ||
|
||
$untrustedInput = $request->get('input'); | ||
|
||
// Turns a string-like value into a non-empty-string | ||
$trustedInput = Type\non_empty_string()->coerce($untrustedInput); | ||
|
||
// Or assert that its already a non-empty-string | ||
$trustedInput = Type\non_empty_string()->assert($untrustedInput); | ||
|
||
// Or check if its a non-empty-string | ||
$isTrustworthy = Type\non_empty_string()->matches($untrustedInput); | ||
``` | ||
|
||
Every type provided by this component is an instance of `Type\TypeInterface<Tv>`. | ||
This interface provides the following methods: | ||
|
||
- `matches(mixed $value): $value is Tv` - Checks if the provided value is of the type. | ||
- `assert(mixed $value): Tv` - Asserts that the provided value is of the type or throws an `AssertException` on failure. | ||
- `coerce(mixed $value): Tv` - Coerces the provided value into the type or throws a `CoercionException` on failure. | ||
|
||
|
||
## API | ||
|
||
### Functions | ||
|
||
<div class="api-functions"> | ||
|
||
* [`Type\converted<I, 0>(TypeInterface<I> $from, TypeInteface<O>, (Closure(I): O)): TypeInterface<O>` php] | ||
|
||
Provides a type `O` that can be converted from a type `I` using a converter function. | ||
|
||
```php | ||
<?php | ||
use Psl\Type; | ||
|
||
$dateTimeType = Type\converted( | ||
Type\string(), | ||
Type\instance_of(DateTimeImmutable::class), | ||
static function (string $value): DateTimeImmutable { | ||
$date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value); | ||
if (!$date) { | ||
// Exceptions will be transformed to CoerceException | ||
throw new \RuntimeException('Invalid format given. Expected date to be of format {format}'); | ||
} | ||
|
||
return $date; | ||
} | ||
); | ||
|
||
$emailType = Type\converted( | ||
Type\string(), | ||
Type\instance_of(EmailValueObject::class), | ||
static function (string $value): EmailValueObject { | ||
// Exceptions will be transformed to CoerceException | ||
return EmailValueObject::tryParse($value); | ||
} | ||
); | ||
|
||
$shape = Type\shape([ | ||
'email' => $emailType, | ||
'dateTime' => $dateTimeType | ||
]); | ||
|
||
// Coerce will convert from -> into, If the provided value is already into - it will skip conversion. | ||
$coerced = $shape->coerce($data); | ||
|
||
// Assert will check if the value is of the type it converts into! | ||
$shape->assert($coerced); | ||
``` | ||
|
||
This type can also be used to transform array-shaped values into custom Data-Transfer-Objects: | ||
|
||
```php | ||
use Psl\Type; | ||
use Psl\Type\TypeInterface; | ||
|
||
/** | ||
* @psalm-immutable | ||
*/ | ||
final class Person { | ||
|
||
public function __construct( | ||
public readonly string $firstName, | ||
public readonly string $lastName, | ||
) { | ||
} | ||
|
||
/** | ||
* @pure | ||
* | ||
* @return TypeInterface<self> | ||
*/ | ||
public static function type(): TypeInterface { | ||
return Type\converted( | ||
Type\shape([ | ||
'firstName' => Type\string(), | ||
'lastName' => Type\string(), | ||
]), | ||
Type\instance_of(Person::class), | ||
fn (array $data): Person => new Person( | ||
$data['firstName'], | ||
$data['lastName'] | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* @pure | ||
*/ | ||
public static function parse(mixed $data): self | ||
{ | ||
return self::type()->coerce($data); | ||
} | ||
} | ||
|
||
// The Person::type() function can now be used as its own type so that it can easily be reused throughout your application. | ||
|
||
$nested = Type\shape([ | ||
'person' => Person::type(), | ||
]); | ||
``` | ||
|
||
* [`Type\shape<Tk of array-key, Tv>(dict<Tv, Tk> $elements, bool $allow_unknown_fields = false): TypeInterface<array<Tk, Tv>>` php] | ||
|
||
Provides a type that can parse (deeply nested) arrays. A shape can consist out of multiple child-shapes and structures: | ||
|
||
```php | ||
use Psl\Type; | ||
|
||
$shape = Type\shape([ | ||
'name' => Type\string(), | ||
'articles' => Type\vec(Type\shape([ | ||
'title' => Type\string(), | ||
'content' => Type\string(), | ||
'likes' => Type\int(), | ||
'comments' => Type\optional( | ||
Type\vec(Type\shape([ | ||
'user' => Type\string(), | ||
'comment' => Type\string() | ||
])) | ||
), | ||
])), | ||
'dictionary' => Type\dict(Type\string(), Type\vec(Type\shape([ | ||
'title' => Type\string(), | ||
'content' => Type\string(), | ||
]))), | ||
'pagination' => Type\optional(Type\shape([ | ||
'currentPage' => Type\uint(), | ||
'totalPages' => Type\uint(), | ||
'perPage' => Type\uint(), | ||
'totalRows' => Type\uint(), | ||
])) | ||
]); | ||
|
||
$validData = $shape->coerce([ | ||
'name' => 'ok', | ||
'articles' => [ | ||
[ | ||
'title' => 'ok', | ||
'content' => 'ok', | ||
'likes' => 1, | ||
'comments' => [ | ||
[ | ||
'user' => 'ok', | ||
'comment' => 'ok' | ||
], | ||
[ | ||
'user' => 'ok', | ||
'comment' => 'ok', | ||
] | ||
] | ||
] | ||
], | ||
'dictionary' => [ | ||
'key' => [ | ||
[ | ||
'title' => 'ok', | ||
'content' => 'ok', | ||
] | ||
] | ||
] | ||
]); | ||
``` | ||
|
||
When the data structure does not match the specified shape, you will get detailed information about what went wrong exactly: | ||
|
||
> Expected "array{'name': string, 'articles': vec<array{'title': string, 'content': string, 'likes': int, 'comments'?: vec<array{'user': string, 'comment': string}>}>, 'dictionary': dict<string, vec<array{'title': string, 'content': string}>>, 'pagination'?: array{'currentPage': uint, 'totalPages': uint, 'perPage': uint, 'totalRows': uint}}", got "array" at path **"articles.0.comments.0"**. | ||
</div> |