Skip to content

Commit

Permalink
docs(type) Provides an initial documentation document for the Type co…
Browse files Browse the repository at this point in the history
…mponent
  • Loading branch information
veewee committed Apr 5, 2024
1 parent 12ab1d9 commit a10263e
Showing 1 changed file with 201 additions and 0 deletions.
201 changes: 201 additions & 0 deletions src/Psl/Type/README.md
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>

0 comments on commit a10263e

Please sign in to comment.