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 5ab5416 commit 67533ec
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 74 deletions.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [Psl\Async](../src/Psl/Async/README.md)
* [Psl\Default](../src/Psl/Default/README.md)
* [Psl\Range](../src/Psl/Range/README.md)
* [Psl\Type](../src/Psl/Type/README.md)

---

Expand Down Expand Up @@ -53,7 +54,6 @@
- [Psl\Str\Grapheme](./component/str-grapheme.md)
- [Psl\TCP](./component/tcp.md)
- [Psl\Trait](./component/trait.md)
- [Psl\Type](./component/type.md)
- [Psl\Unix](./component/unix.md)
- [Psl\Vec](./component/vec.md)

72 changes: 0 additions & 72 deletions docs/component/type.md

This file was deleted.

1 change: 0 additions & 1 deletion docs/documenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ function get_all_components(): array
'Psl\\Str\\Grapheme',
'Psl\\TCP',
'Psl\\Trait',
'Psl\\Type',
'Psl\\Unix',
'Psl\\Locale',
'Psl\\Vec',
Expand Down
1 change: 1 addition & 0 deletions docs/templates/README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [Psl\Async](../src/Psl/Async/README.md)
* [Psl\Default](../src/Psl/Default/README.md)
* [Psl\Range](../src/Psl/Range/README.md)
* [Psl\Type](../src/Psl/Type/README.md)

---

Expand Down
263 changes: 263 additions & 0 deletions src/Psl/Type/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Type

## Introduction

The type component provides a set functions to ensure that a given value is of a specific type **at Runtime**.
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.


## Static Analysis

Your static analyzer fully understands the types provided by this component.
But it takes an additional step to activate this knowledge:

### Psalm Integration

Please refer to the [`php-standard-library/psalm-plugin`](https://github.com/php-standard-library/psalm-plugin) repository.

### PHPStan Integration

Please refer to the [`php-standard-library/phpstan-extension`](https://github.com/php-standard-library/phpstan-extension) repository.

## API

### Functions

<div class="api-functions">

* [Type\array_key](array_key.php)
* [Type\backed_enum](backed_enum.php)
* [Type\bool](bool.php)
* [Type\class_string](class_string.php)

* [`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
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\dict](dict.php)
* [Type\f32](f32.php)
* [Type\f64](f64.php)
* [Type\float](float.php)
* [Type\i16](i16.php)
* [Type\i32](i32.php)
* [Type\i64](i64.php)
* [Type\i8](i8.php)
* [Type\instance_of](instance_of.php)
* [Type\int](int.php)
* [Type\intersection](intersection.php)
* [Type\is_nan](is_nan.php)
* [Type\iterable](iterable.php)
* [Type\literal_scalar](literal_scalar.php)
* [Type\map](map.php)
* [Type\mixed](mixed.php)
* [Type\mixed_dict](mixed_dict.php)
* [Type\mixed_vec](mixed_vec.php)
* [Type\mutable_map](mutable_map.php)
* [Type\mutable_vector](mutable_vector.php)
* [Type\non_empty_dict](non_empty_dict.php)
* [Type\non_empty_string](non_empty_string.php)
* [Type\non_empty_vec](non_empty_vec.php)
* [Type\nonnull](nonnull.php)
* [Type\null](null.php)
* [Type\nullable](nullable.php)
* [Type\num](num.php)
* [Type\numeric_string](numeric_string.php)
* [Type\object](object.php)
* [Type\optional](optional.php)
* [Type\positive_int](positive_int.php)
* [Type\resource](resource.php)
* [Type\scalar](scalar.php)

* [`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 "int" **at path "articles.0.comments.0.user"**.
* [Type\string](string.php)
* [Type\u16](u16.php)
* [Type\u32](u32.php)
* [Type\u8](u8.php)
* [Type\uint](uint.php)
* [Type\union](union.php)
* [Type\unit_enum](unit_enum.php)
* [Type\vec](vec.php)
* [Type\vector](vector.php)


</div>

0 comments on commit 67533ec

Please sign in to comment.