mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
doc: introduce mkdocs as a static documentation generator
This commit is contained in:
parent
eebc6511a9
commit
56ff6849bc
96
.github/workflows/docs.yml
vendored
Normal file
96
.github/workflows/docs.yml
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
name: Publish docs
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Prepare
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
status: ${{ steps.check.outputs.status }}
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
tag: ${{ steps.check.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Find version
|
||||||
|
id: get_version
|
||||||
|
uses: battila7/get-version-action@v2
|
||||||
|
|
||||||
|
- name: Version format check
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.get_version.outputs.is-semver }}" == "true" ]
|
||||||
|
then
|
||||||
|
echo "::set-output name=status::ok"
|
||||||
|
echo "::set-output name=version::${{ steps.get_version.outputs.major }}.${{ steps.get_version.outputs.minor }}"
|
||||||
|
echo "::set-output name=tag::${{ steps.get_version.outputs.version-without-v }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Deploy docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check
|
||||||
|
if: needs.check.outputs.status == 'ok'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install -r docs/requirements.txt
|
||||||
|
|
||||||
|
- name: Set up git author
|
||||||
|
uses: oleksiyrudenko/gha-git-credentials@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Find latest release
|
||||||
|
id: latest
|
||||||
|
uses: pozetroninc/github-action-get-latest-release@v0.6.0
|
||||||
|
with:
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
excludes: draft
|
||||||
|
|
||||||
|
- name: Deploy docs
|
||||||
|
env:
|
||||||
|
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||||
|
run: |
|
||||||
|
# Setup SSH deploy key
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${SSH_DEPLOY_KEY}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan -H github.com > ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- run: git remote add doc git@github.com:CuyZ/Valinor-Documentation.git
|
||||||
|
- run: git fetch doc gh-pages --verbose
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
# Check if the "latest" alias exists
|
||||||
|
VALINOR_HAS_LATEST=$(mike list --config-file docs/mkdocs.yml --rebase --remote doc | grep latest) || true
|
||||||
|
|
||||||
|
# If so then it is set as the default version (to enable the index redirect)
|
||||||
|
if [ "${VALINOR_HAS_LATEST}" != "" ]
|
||||||
|
then
|
||||||
|
echo "Set latest as default"
|
||||||
|
mike set-default latest --config-file docs/mkdocs.yml --remote doc
|
||||||
|
fi
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
if [ "${{ steps.latest.outputs.release }}" = "${{ needs.check.outputs.tag }}" ]
|
||||||
|
then
|
||||||
|
# Here we deploy a new latest version
|
||||||
|
mike deploy ${{ needs.check.outputs.version }} latest --config-file docs/mkdocs.yml --update-aliases --push --remote doc
|
||||||
|
else
|
||||||
|
# Here we deploy a version that's not the latest one
|
||||||
|
mike deploy ${{ needs.check.outputs.version }} --config-file docs/mkdocs.yml --push --remote doc
|
||||||
|
fi
|
@ -59,6 +59,11 @@
|
|||||||
"@putenv XDEBUG_MODE=coverage",
|
"@putenv XDEBUG_MODE=coverage",
|
||||||
"phpunit --coverage-xml='var/cache/phpunit/coverage' --log-junit='var/cache/phpunit/coverage/junit.xml'",
|
"phpunit --coverage-xml='var/cache/phpunit/coverage' --log-junit='var/cache/phpunit/coverage/junit.xml'",
|
||||||
"infection --threads=12 --skip-initial-tests --coverage='var/cache/phpunit/coverage'"
|
"infection --threads=12 --skip-initial-tests --coverage='var/cache/phpunit/coverage'"
|
||||||
|
],
|
||||||
|
"doc": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"pip install -r docs/requirements.txt",
|
||||||
|
"mkdocs serve --config-file docs/mkdocs.yml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
17
docs/includes/links.md
Normal file
17
docs/includes/links.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[PHPStan]: https://phpstan.org/
|
||||||
|
|
||||||
|
[Psalm]: https://psalm.dev/
|
||||||
|
|
||||||
|
[Rector]: https://github.com/rectorphp/rector
|
||||||
|
|
||||||
|
[Webmozart Assert]: https://github.com/webmozarts/assert
|
||||||
|
|
||||||
|
[link-packagist]: https://packagist.org/packages/cuyz/valinor
|
||||||
|
|
||||||
|
[ICU library]: https://unicode-org.github.io/icu/
|
||||||
|
|
||||||
|
[ICU documentation]: https://unicode-org.github.io/icu/userguide/format_parse/messages/
|
||||||
|
|
||||||
|
[contributors]: https://github.com/CuyZ/Valinor/graphs/contributors
|
||||||
|
|
||||||
|
[blackfire-logo]: img/blackfire-logo.svg "Blackfire logo"
|
74
docs/mkdocs.yml
Normal file
74
docs/mkdocs.yml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json
|
||||||
|
|
||||||
|
site_name: Valinor
|
||||||
|
repo_url: https://github.com/CuyZ/Valinor
|
||||||
|
repo_name: CuyZ/Valinor
|
||||||
|
edit_uri: edit/master/docs/
|
||||||
|
docs_dir: pages
|
||||||
|
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/CuyZ
|
||||||
|
- icon: fontawesome/solid/globe
|
||||||
|
link: https://cuyz.io
|
||||||
|
version:
|
||||||
|
provider: mike
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
icon:
|
||||||
|
repo: fontawesome/brands/github
|
||||||
|
logo: material/pine-tree
|
||||||
|
features:
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.top
|
||||||
|
- navigation.indexes
|
||||||
|
- content.code.annotate
|
||||||
|
palette:
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
primary: deep purple
|
||||||
|
accent: deep purple
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: deep purple
|
||||||
|
accent: deep purple
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- meta
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
extend_pygments_lang:
|
||||||
|
- name: php
|
||||||
|
lang: php
|
||||||
|
options:
|
||||||
|
startinline: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets:
|
||||||
|
auto_append:
|
||||||
|
- docs/includes/links.md
|
||||||
|
- pymdownx.superfences
|
||||||
|
- admonition
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Introduction: index.md
|
||||||
|
- Getting Started: getting-started.md
|
||||||
|
- Credits: credits.md
|
||||||
|
- Usage:
|
||||||
|
- Validation: validation.md
|
||||||
|
- Message customization: message-customization.md
|
||||||
|
- Source: source.md
|
||||||
|
- Mapping:
|
||||||
|
- Construction strategy: mapping/construction-strategy.md
|
||||||
|
- Inferring interfaces: mapping/inferring-interfaces.md
|
||||||
|
- Handled types: mapping/handled-types.md
|
||||||
|
- Other:
|
||||||
|
- Performance & caching: other/performance-and-cache.md
|
||||||
|
- Static analysis: other/static-analysis.md
|
10
docs/pages/credits.md
Normal file
10
docs/pages/credits.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Credits & thank you
|
||||||
|
|
||||||
|
The development of this library is mainly motivated by the kind words and the
|
||||||
|
help of many people. I am grateful to everyone, especially to the [contributors]
|
||||||
|
of this repository who directly help to push the project forward.
|
||||||
|
|
||||||
|
I also want to thank
|
||||||
|
[![blackfire-logo] Blackfire](https://www.blackfire.io/?utm_source=valinor&utm_medium=readme&utm_campaign=free-open-source)
|
||||||
|
for providing a license of their awesome tool, leading to notable performance
|
||||||
|
gains when using this library.
|
121
docs/pages/getting-started.md
Normal file
121
docs/pages/getting-started.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# Getting started
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require cuyz/valinor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
An application must handle the data coming from an external API; the response
|
||||||
|
has a JSON format and describes a thread and its answers. The validity of this
|
||||||
|
input is unsure, besides manipulating a raw JSON string is laborious and
|
||||||
|
inefficient.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1337,
|
||||||
|
"content": "Do you like potatoes?",
|
||||||
|
"date": "1957-07-23 13:37:42",
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"user": "Ella F.",
|
||||||
|
"message": "I like potatoes",
|
||||||
|
"date": "1957-07-31 15:28:12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user": "Louis A.",
|
||||||
|
"message": "And I like tomatoes",
|
||||||
|
"date": "1957-08-13 09:05:24"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The application must be certain that it can handle this data correctly; wrapping
|
||||||
|
the input in a value object will help.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A schema representing the needed structure must be provided, using classes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class Thread
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $id,
|
||||||
|
public readonly string $content,
|
||||||
|
public readonly DateTimeInterface $date,
|
||||||
|
/** @var Answer[] */
|
||||||
|
public readonly array $answers,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Answer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $user,
|
||||||
|
public readonly string $message,
|
||||||
|
public readonly DateTimeInterface $date,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then a mapper is used to hydrate a source into these objects.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getThread(int $id): Thread
|
||||||
|
{
|
||||||
|
$rawJson = $this->client->request("https://example.com/thread/$id");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(
|
||||||
|
Thread::class,
|
||||||
|
new \CuyZ\Valinor\Mapper\Source\JsonSource($rawJson)
|
||||||
|
);
|
||||||
|
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||||
|
// Do something…
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mapping advanced types
|
||||||
|
|
||||||
|
Although it is recommended to map an input to a value object, in some cases
|
||||||
|
mapping to another type can be easier/more flexible.
|
||||||
|
|
||||||
|
It is for instance possible to map to an array of objects:
|
||||||
|
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
$objects = (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(
|
||||||
|
'array<' . SomeClass::class . '>',
|
||||||
|
[/* … */]
|
||||||
|
);
|
||||||
|
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||||
|
// Do something…
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For simple use-cases, an array shape can be used:
|
||||||
|
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
$array = (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(
|
||||||
|
'array{foo: string, bar: int}',
|
||||||
|
[/* … */]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo $array['foo'];
|
||||||
|
echo $array['bar'] * 2;
|
||||||
|
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||||
|
// Do something…
|
||||||
|
}
|
||||||
|
```
|
Before Width: | Height: | Size: 558 B After Width: | Height: | Size: 558 B |
47
docs/pages/index.md
Normal file
47
docs/pages/index.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
Valinor • PHP object mapper with strong type support
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
[![Total Downloads](http://poser.pugx.org/cuyz/valinor/downloads)][link-packagist]
|
||||||
|
[![Latest Stable Version](http://poser.pugx.org/cuyz/valinor/v)][link-packagist]
|
||||||
|
[![PHP Version Require](http://poser.pugx.org/cuyz/valinor/require/php)][link-packagist]
|
||||||
|
|
||||||
|
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FCuyZ%2FValinor%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/CuyZ/Valinor/master)
|
||||||
|
|
||||||
|
Valinor is a PHP library that helps to map any input into a strongly-typed value
|
||||||
|
object structure.
|
||||||
|
|
||||||
|
The conversion can handle native PHP types as well as other well-known advanced
|
||||||
|
type annotations like array shapes, generics and more.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
There are many benefits of using value objects instead of plain arrays and
|
||||||
|
scalar values in a modern codebase, among which:
|
||||||
|
|
||||||
|
1. **Data and behaviour encapsulation** — locks an object's behaviour inside its
|
||||||
|
class, preventing it from being scattered across the codebase.
|
||||||
|
2. **Data validation** — guarantees the valid state of an object.
|
||||||
|
3. **Immutability** — ensures the state of an object cannot be changed during
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
When mapping any source to an object structure, this library will ensure that
|
||||||
|
all input values are properly converted to match the types of the nodes — class
|
||||||
|
properties or method parameters. Any value that cannot be converted to the
|
||||||
|
correct type will trigger an error and prevent the mapping from completing.
|
||||||
|
|
||||||
|
These checks guarantee that if the mapping succeeds, the object structure is
|
||||||
|
perfectly valid, hence there is no need for further validation nor type
|
||||||
|
conversion: the objects are ready to be used.
|
||||||
|
|
||||||
|
### Static analysis
|
||||||
|
|
||||||
|
A strongly-typed codebase allows the usage of static analysis tools like
|
||||||
|
[PHPStan] and [Psalm] that can identify issues in a codebase without running it.
|
||||||
|
|
||||||
|
Moreover, static analysis can help during a refactoring of a codebase with tools
|
||||||
|
like an IDE or [Rector].
|
125
docs/pages/mapping/construction-strategy.md
Normal file
125
docs/pages/mapping/construction-strategy.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# Construction strategy
|
||||||
|
|
||||||
|
During the mapping, instances of objects are recursively created and hydrated
|
||||||
|
with transformed values. Construction strategies will determine what values are
|
||||||
|
needed and how an object is built.
|
||||||
|
|
||||||
|
## Native constructor
|
||||||
|
|
||||||
|
If a constructor exists and is public, its arguments will determine which values
|
||||||
|
are needed from the input.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $foo,
|
||||||
|
public readonly int $bar,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom constructor
|
||||||
|
|
||||||
|
An object may have custom ways of being created, in such cases these
|
||||||
|
constructors need to be registered to the mapper to be used. A constructor is a
|
||||||
|
callable that can be either:
|
||||||
|
|
||||||
|
1. A named constructor, also known as a static factory method
|
||||||
|
2. The method of a service — for instance a repository
|
||||||
|
3. A "callable object" — a class that declares an `__invoke` method
|
||||||
|
4. Any other callable — including anonymous functions
|
||||||
|
|
||||||
|
In any case, the return type of the callable will be resolved by the mapper to
|
||||||
|
know when to use it. Any argument can be provided and will automatically be
|
||||||
|
mapped using the given source. These arguments can then be used to instantiate
|
||||||
|
the object in the desired way.
|
||||||
|
|
||||||
|
Registering any constructor will disable the native constructor — the
|
||||||
|
`__construct` method — of the targeted class. If for some reason it still needs
|
||||||
|
to be handled as well, the name of the class must be given to the
|
||||||
|
registration method.
|
||||||
|
|
||||||
|
```php
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
// Allow the native constructor to be used
|
||||||
|
Color::class,
|
||||||
|
|
||||||
|
// Register a named constructor (1)
|
||||||
|
Color::fromHex(...),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An anonymous function can also be used, for instance when the desired
|
||||||
|
* object is an external dependency that cannot be modified.
|
||||||
|
*
|
||||||
|
* @param 'red'|'green'|'blue' $color
|
||||||
|
* @param 'dark'|'light' $darkness
|
||||||
|
*/
|
||||||
|
function (string $color, string $darkness): Color {
|
||||||
|
$main = $darkness === 'dark' ? 128 : 255;
|
||||||
|
$other = $darkness === 'dark' ? 0 : 128;
|
||||||
|
|
||||||
|
return new Color(
|
||||||
|
$color === 'red' ? $main : $other,
|
||||||
|
$color === 'green' ? $main : $other,
|
||||||
|
$color === 'blue' ? $main : $other,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(Color::class, [/* … */]);
|
||||||
|
|
||||||
|
final class Color
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param int<0, 255> $red
|
||||||
|
* @param int<0, 255> $green
|
||||||
|
* @param int<0, 255> $blue
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $red,
|
||||||
|
public readonly int $green,
|
||||||
|
public readonly int $blue
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param non-empty-string $hex
|
||||||
|
*/
|
||||||
|
public static function fromHex(string $hex): self
|
||||||
|
{
|
||||||
|
if (strlen($hex) !== 6) {
|
||||||
|
throw new DomainException('Must be 6 characters long');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var int<0, 255> $red */
|
||||||
|
$red = hexdec(substr($hex, 0, 2));
|
||||||
|
/** @var int<0, 255> $green */
|
||||||
|
$green = hexdec(substr($hex, 2, 2));
|
||||||
|
/** @var int<0, 255> $blue */
|
||||||
|
$blue = hexdec(substr($hex, 4, 2));
|
||||||
|
|
||||||
|
return new self($red, $green, $blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1. …or for PHP < 8.1:
|
||||||
|
|
||||||
|
```php
|
||||||
|
[Color::class, 'fromHex'],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
If no constructor is registered, properties will determine which values are
|
||||||
|
needed from the input.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public readonly string $foo;
|
||||||
|
|
||||||
|
public readonly int $bar;
|
||||||
|
}
|
||||||
|
```
|
150
docs/pages/mapping/handled-types.md
Normal file
150
docs/pages/mapping/handled-types.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Handled types
|
||||||
|
|
||||||
|
To prevent conflicts or duplication of the type annotations, this library tries
|
||||||
|
to handle most of the type annotations that are accepted by [PHPStan] and
|
||||||
|
[Psalm].
|
||||||
|
|
||||||
|
## Scalar
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private bool $boolean,
|
||||||
|
|
||||||
|
private float $float,
|
||||||
|
|
||||||
|
private int $integer,
|
||||||
|
|
||||||
|
/** @var positive-int */
|
||||||
|
private int $positiveInteger,
|
||||||
|
|
||||||
|
/** @var negative-int */
|
||||||
|
private int $negativeInteger,
|
||||||
|
|
||||||
|
/** @var int<-42, 1337> */
|
||||||
|
private int $integerRange,
|
||||||
|
|
||||||
|
/** @var int<min, 0> */
|
||||||
|
private int $integerRangeWithMinRange,
|
||||||
|
|
||||||
|
/** @var int<0, max> */
|
||||||
|
private int $integerRangeWithMaxRange,
|
||||||
|
|
||||||
|
private string $string,
|
||||||
|
|
||||||
|
/** @var non-empty-string */
|
||||||
|
private string $nonEmptyString,
|
||||||
|
|
||||||
|
/** @var class-string */
|
||||||
|
private string $classString,
|
||||||
|
|
||||||
|
/** @var class-string<SomeInterface> */
|
||||||
|
private string $classStringOfAnInterface,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private SomeClass $class,
|
||||||
|
|
||||||
|
private DateTimeInterface $interface,
|
||||||
|
|
||||||
|
/** @var SomeInterface&AnotherInterface */
|
||||||
|
private object $intersection,
|
||||||
|
|
||||||
|
/** @var SomeCollection<SomeClass> */
|
||||||
|
private SomeCollection $classWithGeneric,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of object
|
||||||
|
*/
|
||||||
|
final class SomeCollection
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
/** @var array<T> */
|
||||||
|
private array $objects,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Array & lists
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
/** @var string[] */
|
||||||
|
private array $simpleArray,
|
||||||
|
|
||||||
|
/** @var array<string> */
|
||||||
|
private array $arrayOfStrings,
|
||||||
|
|
||||||
|
/** @var array<string, SomeClass> */
|
||||||
|
private array $arrayOfClassWithStringKeys,
|
||||||
|
|
||||||
|
/** @var array<int, SomeClass> */
|
||||||
|
private array $arrayOfClassWithIntegerKeys,
|
||||||
|
|
||||||
|
/** @var non-empty-array<string> */
|
||||||
|
private array $nonEmptyArrayOfStrings,
|
||||||
|
|
||||||
|
/** @var non-empty-array<string, SomeClass> */
|
||||||
|
private array $nonEmptyArrayWithStringKeys,
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private array $listOfStrings,
|
||||||
|
|
||||||
|
/** @var non-empty-list<string> */
|
||||||
|
private array $nonEmptyListOfStrings,
|
||||||
|
|
||||||
|
/** @var array{foo: string, bar: int} */
|
||||||
|
private array $shapedArray,
|
||||||
|
|
||||||
|
/** @var array{foo: string, bar?: int} */
|
||||||
|
private array $shapedArrayWithOptionalElement,
|
||||||
|
|
||||||
|
/** @var array{string, bar: int} */
|
||||||
|
private array $shapedArrayWithUndefinedKey,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Union
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private int|string $simpleUnion,
|
||||||
|
|
||||||
|
/** @var class-string<SomeInterface|AnotherInterface> */
|
||||||
|
private string $unionOfClassString,
|
||||||
|
|
||||||
|
/** @var array<SomeInterface|AnotherInterface> */
|
||||||
|
private array $unionInsideArray,
|
||||||
|
|
||||||
|
/** @var int|true */
|
||||||
|
private int|bool $unionWithLiteralTrueType;
|
||||||
|
|
||||||
|
/** @var int|false */
|
||||||
|
private int|bool $unionWithLiteralFalseType;
|
||||||
|
|
||||||
|
/** @var 404.42|1337.42 */
|
||||||
|
private float $unionOfFloatValues,
|
||||||
|
|
||||||
|
/** @var 42|1337 */
|
||||||
|
private int $unionOfIntegerValues,
|
||||||
|
|
||||||
|
/** @var 'foo'|'bar' */
|
||||||
|
private string $unionOfStringValues,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
49
docs/pages/mapping/inferring-interfaces.md
Normal file
49
docs/pages/mapping/inferring-interfaces.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Inferring interfaces
|
||||||
|
|
||||||
|
When the mapper meets an interface, it needs to understand which implementation
|
||||||
|
(a class that implements this interface) will be used — this information must be
|
||||||
|
provided in the mapper builder, using the method `infer()`.
|
||||||
|
|
||||||
|
The callback given to this method must return the name of a class that
|
||||||
|
implements the interface. Any arguments can be required by the callback; they
|
||||||
|
will be mapped properly using the given source.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$mapper = (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->infer(UuidInterface::class, fn () => MyUuid::class)
|
||||||
|
->infer(SomeInterface::class, fn (string $type) => match($type) {
|
||||||
|
'first' => FirstImplementation::class,
|
||||||
|
'second' => SecondImplementation::class,
|
||||||
|
default => throw new DomainException("Unhandled type `$type`.")
|
||||||
|
})->mapper();
|
||||||
|
|
||||||
|
// Will return an instance of `FirstImplementation`
|
||||||
|
$mapper->map(SomeInterface::class, [
|
||||||
|
'type' => 'first',
|
||||||
|
'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',
|
||||||
|
'someString' => 'foo',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Will return an instance of `SecondImplementation`
|
||||||
|
$mapper->map(SomeInterface::class, [
|
||||||
|
'type' => 'second',
|
||||||
|
'uuid' => 'a6868d61-acba-406d-bcff-30ecd8c0ceb6',
|
||||||
|
'someInt' => 42,
|
||||||
|
]);
|
||||||
|
|
||||||
|
interface SomeInterface {}
|
||||||
|
|
||||||
|
final class FirstImplementation implements SomeInterface
|
||||||
|
{
|
||||||
|
public readonly UuidInterface $uuid;
|
||||||
|
|
||||||
|
public readonly string $someString;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SecondImplementation implements SomeInterface
|
||||||
|
{
|
||||||
|
public readonly UuidInterface $uuid;
|
||||||
|
|
||||||
|
public readonly int $someInt;
|
||||||
|
}
|
||||||
|
```
|
172
docs/pages/message-customization.md
Normal file
172
docs/pages/message-customization.md
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# Message customization
|
||||||
|
|
||||||
|
The content of a message can be changed to fit custom use cases; it can contain
|
||||||
|
placeholders that will be replaced with useful information.
|
||||||
|
|
||||||
|
The placeholders below are always available; even more may be used depending
|
||||||
|
on the original message.
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
|----------------------|:-----------------------------------------------------|
|
||||||
|
| `{message_code}` | the code of the message |
|
||||||
|
| `{node_name}` | name of the node to which the message is bound |
|
||||||
|
| `{node_path}` | path of the node to which the message is bound |
|
||||||
|
| `{node_type}` | type of the node to which the message is bound |
|
||||||
|
| `{original_value}` | the source value that was given to the node |
|
||||||
|
| `{original_message}` | the original message before being customized |
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(SomeClass::class, [/* … */]);
|
||||||
|
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||||
|
$node = $error->node();
|
||||||
|
$messages = new \CuyZ\Valinor\Mapper\Tree\Message\MessagesFlattener($node);
|
||||||
|
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
if ($message->code() === 'some_code') {
|
||||||
|
$message = $message->withBody('new message / {original_message}');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The messages are formatted using the [ICU library], enabling the placeholders to
|
||||||
|
use advanced syntax to perform proper translations, for instance currency
|
||||||
|
support.
|
||||||
|
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())->mapper()->map('int<0, 100>', 1337);
|
||||||
|
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||||
|
$message = $error->node()->messages()[0];
|
||||||
|
|
||||||
|
if (is_numeric($message->value())) {
|
||||||
|
$message = $message->withBody(
|
||||||
|
'Invalid amount {original_value, number, currency}'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid amount: $1,337.00
|
||||||
|
echo $message->withLocale('en_US');
|
||||||
|
|
||||||
|
// Invalid amount: £1,337.00
|
||||||
|
echo $message->withLocale('en_GB');
|
||||||
|
|
||||||
|
// Invalid amount: 1 337,00 €
|
||||||
|
echo $message->withLocale('fr_FR');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [ICU documentation] for more information on available syntax.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
If the `intl` extension is not installed, a shim will be
|
||||||
|
available to replace the placeholders, but it won't handle advanced syntax as
|
||||||
|
described above.
|
||||||
|
|
||||||
|
## Deeper message customization / translation
|
||||||
|
|
||||||
|
For deeper message changes, formatters can be used — for instance to translate
|
||||||
|
content.
|
||||||
|
|
||||||
|
### Translation
|
||||||
|
|
||||||
|
The formatter `TranslationMessageFormatter` can be used to translate the content
|
||||||
|
of messages.
|
||||||
|
|
||||||
|
The library provides a list of all messages that can be returned; this list can
|
||||||
|
be filled or modified with custom translations.
|
||||||
|
|
||||||
|
```php
|
||||||
|
\CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter::default()
|
||||||
|
// Create/override a single entry…
|
||||||
|
->withTranslation('fr', 'some custom message', 'un message personnalisé')
|
||||||
|
// …or several entries.
|
||||||
|
->withTranslations([
|
||||||
|
'some custom message' => [
|
||||||
|
'en' => 'Some custom message',
|
||||||
|
'fr' => 'Un message personnalisé',
|
||||||
|
'es' => 'Un mensaje personalizado',
|
||||||
|
],
|
||||||
|
'some other message' => [
|
||||||
|
// …
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->format($message);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replacement map
|
||||||
|
|
||||||
|
The formatter `MessageMapFormatter` can be used to provide a list of messages
|
||||||
|
replacements. It can be instantiated with an array where each key represents
|
||||||
|
either:
|
||||||
|
|
||||||
|
- The code of the message to be replaced
|
||||||
|
- The body of the message to be replaced
|
||||||
|
- The class name of the message to be replaced
|
||||||
|
|
||||||
|
If none of those is found, the content of the message will stay unchanged unless
|
||||||
|
a default one is given to the class.
|
||||||
|
|
||||||
|
If one of these keys is found, the array entry will be used to replace the
|
||||||
|
content of the message. This entry can be either a plain text or a callable that
|
||||||
|
takes the message as a parameter and returns a string; it is for instance
|
||||||
|
advised to use a callable in cases where a custom translation service is used —
|
||||||
|
to avoid useless greedy operations.
|
||||||
|
|
||||||
|
In any case, the content can contain placeholders as described
|
||||||
|
[above](#message-customization).
|
||||||
|
|
||||||
|
```php
|
||||||
|
(new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([
|
||||||
|
// Will match if the given message has this exact code
|
||||||
|
'some_code' => 'New content / code: {message_code}',
|
||||||
|
|
||||||
|
// Will match if the given message has this exact content
|
||||||
|
'Some message content' => 'New content / previous: {original_message}',
|
||||||
|
|
||||||
|
// Will match if the given message is an instance of `SomeError`
|
||||||
|
SomeError::class => 'New content / value: {original_value}',
|
||||||
|
|
||||||
|
// A callback can be used to get access to the message instance
|
||||||
|
OtherError::class => function (NodeMessage $message): string {
|
||||||
|
if ($message->path() === 'foo.bar') {
|
||||||
|
return 'Some custom message';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message->body();
|
||||||
|
},
|
||||||
|
|
||||||
|
// For greedy operation, it is advised to use a lazy-callback
|
||||||
|
'foo' => fn () => $this->customTranslator->translate('foo.bar'),
|
||||||
|
]))
|
||||||
|
->defaultsTo('some default message')
|
||||||
|
// …or…
|
||||||
|
->defaultsTo(fn () => $this->customTranslator->translate('default_message'))
|
||||||
|
->format($message);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Several formatters
|
||||||
|
|
||||||
|
It is possible to join several formatters into one formatter by using the
|
||||||
|
`AggregateMessageFormatter`. This instance can then easily be injected in a
|
||||||
|
service that will handle messages.
|
||||||
|
|
||||||
|
The formatters will be called in the same order they are given to the aggregate.
|
||||||
|
|
||||||
|
```php
|
||||||
|
(new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\AggregateMessageFormatter(
|
||||||
|
new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\LocaleMessageFormatter('fr'),
|
||||||
|
new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([
|
||||||
|
// …
|
||||||
|
],
|
||||||
|
\CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter::default(),
|
||||||
|
))->format($message)
|
||||||
|
```
|
55
docs/pages/other/performance-and-cache.md
Normal file
55
docs/pages/other/performance-and-cache.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Performance & caching
|
||||||
|
|
||||||
|
This library needs to parse a lot of information in order to handle all provided
|
||||||
|
features. Therefore, it is strongly advised to activate the cache to reduce
|
||||||
|
heavy workload between runtimes, especially when the application runs in a
|
||||||
|
production environment.
|
||||||
|
|
||||||
|
The library provides a cache implementation out of the box, which saves
|
||||||
|
cache entries into the file system.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
|
||||||
|
It is also possible to use any PSR-16 compliant implementation, as
|
||||||
|
long as it is capable of caching the entries handled by the library.
|
||||||
|
|
||||||
|
When the application runs in a development environment, the cache implementation
|
||||||
|
should be decorated with `FileWatchingCache`, which will watch the files of the
|
||||||
|
application and invalidate cache entries when a PHP file is modified by a
|
||||||
|
developer — preventing the library not behaving as expected when the signature
|
||||||
|
of a property or a method changes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-directory');
|
||||||
|
|
||||||
|
if ($isApplicationInDevelopmentEnvironment) {
|
||||||
|
$cache = new \CuyZ\Valinor\Cache\FileWatchingCache($cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->withCache($cache)
|
||||||
|
->mapper()
|
||||||
|
->map(SomeClass::class, [/* … */]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Warming up cache
|
||||||
|
|
||||||
|
The cache can be warmed up, for instance in a pipeline during the build and
|
||||||
|
deployment of the application.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
The cache has to be registered first, otherwise the warmup will end
|
||||||
|
up being useless.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir');
|
||||||
|
|
||||||
|
$mapperBuilder = (new \CuyZ\Valinor\MapperBuilder())->withCache($cache);
|
||||||
|
|
||||||
|
// During the build:
|
||||||
|
$mapperBuilder->warmup(SomeClass::class, SomeOtherClass::class);
|
||||||
|
|
||||||
|
// In the application:
|
||||||
|
$mapper->mapper()->map(SomeClass::class, [/* … */]);
|
||||||
|
```
|
80
docs/pages/other/static-analysis.md
Normal file
80
docs/pages/other/static-analysis.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Static analysis
|
||||||
|
|
||||||
|
To help static analysis of a codebase using this library, an extension for
|
||||||
|
[PHPStan] and a plugin for [Psalm] are provided. They enable these tools to
|
||||||
|
better understand the behaviour of the mapper.
|
||||||
|
|
||||||
|
Considering at least one of those tools are installed on a project, below are
|
||||||
|
examples of the kind of errors that would be reported.
|
||||||
|
|
||||||
|
**Mapping to an array of classes**
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $foo,
|
||||||
|
public readonly int $bar,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$objects = (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(
|
||||||
|
'array<' . SomeClass::class . '>',
|
||||||
|
[/* … */]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($objects as $object) {
|
||||||
|
// ✅
|
||||||
|
echo $object->foo;
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
echo $object->bar * 2;
|
||||||
|
|
||||||
|
// ❌ Cannot perform operation between `string` and `int`
|
||||||
|
echo $object->foo * $object->bar;
|
||||||
|
|
||||||
|
// ❌ Property `SomeClass::$fiz` is not defined
|
||||||
|
echo $object->fiz;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapping to a shaped array**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$array = (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(
|
||||||
|
'array{foo: string, bar: int}',
|
||||||
|
[/* … */]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
echo $array['foo'];
|
||||||
|
|
||||||
|
// ❌ Expected `string` but got `int`
|
||||||
|
echo strtolower($array['bar']);
|
||||||
|
|
||||||
|
// ❌ Cannot perform operation between `string` and `int`
|
||||||
|
echo $array['foo'] * $array['bar'];
|
||||||
|
|
||||||
|
// ❌ Offset `fiz` does not exist on array
|
||||||
|
echo $array['fiz'];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To activate this feature, the configuration must be updated for the installed
|
||||||
|
tool(s):
|
||||||
|
|
||||||
|
```yaml title="phpstan.neon"
|
||||||
|
includes:
|
||||||
|
- vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml title="psalm.xml"
|
||||||
|
<plugins>
|
||||||
|
<plugin filename="vendor/cuyz/valinor/qa/Psalm/Plugin/TreeMapperPsalmPlugin.php"/>
|
||||||
|
</plugins>
|
||||||
|
```
|
146
docs/pages/source.md
Normal file
146
docs/pages/source.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Source
|
||||||
|
|
||||||
|
Any source can be given to the mapper, be it an array, some json, yaml or even a
|
||||||
|
file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();
|
||||||
|
|
||||||
|
$mapper->map(
|
||||||
|
SomeClass::class,
|
||||||
|
\CuyZ\Valinor\Mapper\Source\Source::array($someData)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mapper->map(
|
||||||
|
SomeClass::class,
|
||||||
|
\CuyZ\Valinor\Mapper\Source\Source::json($jsonString)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mapper->map(
|
||||||
|
SomeClass::class,
|
||||||
|
\CuyZ\Valinor\Mapper\Source\Source::yaml($yamlString)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mapper->map(
|
||||||
|
SomeClass::class,
|
||||||
|
// File containing valid Json or Yaml content and with valid extension
|
||||||
|
\CuyZ\Valinor\Mapper\Source\Source::file(
|
||||||
|
new SplFileObject('path/to/my/file.json')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modifiers
|
||||||
|
|
||||||
|
Sometimes the source is not in the same format and/or organised in the same
|
||||||
|
way as a value object. Modifiers can be used to change a source before the
|
||||||
|
mapping occurs.
|
||||||
|
|
||||||
|
### Camel case keys
|
||||||
|
|
||||||
|
This modifier recursively forces all keys to be in camelCase format.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public readonly string $someValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = \CuyZ\Valinor\Mapper\Source\Source::array([
|
||||||
|
'some_value' => 'foo',
|
||||||
|
// …or…
|
||||||
|
'some-value' => 'foo',
|
||||||
|
// …or…
|
||||||
|
'some value' => 'foo',
|
||||||
|
// …will be replaced by `['someValue' => 'foo']`
|
||||||
|
])
|
||||||
|
->camelCaseKeys();
|
||||||
|
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(SomeClass::class, $source);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path mapping
|
||||||
|
|
||||||
|
This modifier can be used to change paths in the source data using a dot
|
||||||
|
notation.
|
||||||
|
|
||||||
|
The mapping is done using an associative array of path mappings. This array must
|
||||||
|
have the source path as key and the target path as value.
|
||||||
|
|
||||||
|
The source path uses the dot notation (eg `A.B.C`) and can contain one `*` for
|
||||||
|
array paths (eg `A.B.*.C`).
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class Country
|
||||||
|
{
|
||||||
|
/** @var City[] */
|
||||||
|
public readonly array $cities;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class City
|
||||||
|
{
|
||||||
|
public readonly string $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = \CuyZ\Valinor\Mapper\Source\Source::array([
|
||||||
|
'towns' => [
|
||||||
|
['label' => 'Ankh Morpork'],
|
||||||
|
['label' => 'Minas Tirith'],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->map([
|
||||||
|
'towns' => 'cities',
|
||||||
|
'towns.*.label' => 'name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// After modification this is what the source will look like:
|
||||||
|
[
|
||||||
|
'cities' => [
|
||||||
|
['name' => 'Ankh Morpork'],
|
||||||
|
['name' => 'Minas Tirith'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(Country::class, $source);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom source
|
||||||
|
|
||||||
|
The source is just an iterable, so it's easy to create a custom one.
|
||||||
|
It can even be combined with the provided builder.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class AcmeSource implements IteratorAggregate
|
||||||
|
{
|
||||||
|
private iterable $source;
|
||||||
|
|
||||||
|
public function __construct(iterable $source)
|
||||||
|
{
|
||||||
|
$this->source = $this->doSomething($source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function doSomething(iterable $source): iterable
|
||||||
|
{
|
||||||
|
// Do something with $source
|
||||||
|
|
||||||
|
return $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator()
|
||||||
|
{
|
||||||
|
yield from $this->source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = \CuyZ\Valinor\Mapper\Source\Source::iterable(
|
||||||
|
new AcmeSource(['value' => 'foo'])
|
||||||
|
)->camelCaseKeys();
|
||||||
|
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(SomeClass::class, $source);
|
||||||
|
```
|
53
docs/pages/validation.md
Normal file
53
docs/pages/validation.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Validation
|
||||||
|
|
||||||
|
The source given to a mapper can never be trusted, this is actually the very
|
||||||
|
goal of this library: transforming an unstructured input to a well-defined
|
||||||
|
object structure. If the mapper cannot guess how to cast a certain value, it
|
||||||
|
means that it is not able to guarantee the validity of the desired object thus
|
||||||
|
it will fail.
|
||||||
|
|
||||||
|
Any issue encountered during the mapping will add an error to an upstream
|
||||||
|
exception of type `\CuyZ\Valinor\Mapper\MappingError`. It is therefore always
|
||||||
|
recommended wrapping the mapping function call with a try/catch statement and
|
||||||
|
handle the error properly.
|
||||||
|
|
||||||
|
More specific validation should be done in the constructor of the value object,
|
||||||
|
by throwing an exception if something is wrong with the given data. A good
|
||||||
|
practice would be to use lightweight validation tools like [Webmozart Assert].
|
||||||
|
|
||||||
|
When the mapping fails, the exception gives access to the root node. This
|
||||||
|
recursive object allows retrieving all needed information through the whole
|
||||||
|
mapping tree: path, values, types and messages, including the issues that caused
|
||||||
|
the exception.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class SomeClass
|
||||||
|
{
|
||||||
|
public function __construct(private string $someValue)
|
||||||
|
{
|
||||||
|
Assert::startsWith($someValue, 'foo_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->mapper()
|
||||||
|
->map(
|
||||||
|
SomeClass::class,
|
||||||
|
['someValue' => 'bar_baz']
|
||||||
|
);
|
||||||
|
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||||
|
// Get flatten list of all messages through the whole nodes tree
|
||||||
|
$node = $error->node();
|
||||||
|
$messages = new \CuyZ\Valinor\Mapper\Tree\Message\MessagesFlattener($node);
|
||||||
|
|
||||||
|
// If only errors are wanted, they can be filtered
|
||||||
|
$errorMessages = $messages->errors();
|
||||||
|
|
||||||
|
// Should print something similar to:
|
||||||
|
// > Expected a value to start with "foo_". Got: "bar_baz"
|
||||||
|
foreach ($errorMessages as $message) {
|
||||||
|
echo $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
11
docs/requirements.txt
Normal file
11
docs/requirements.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mkdocs==1.3.0
|
||||||
|
mike==1.1.2
|
||||||
|
markdown==3.3.7
|
||||||
|
mkdocs-material==8.2.3
|
||||||
|
|
||||||
|
# Markdown extensions
|
||||||
|
Pygments==2.12.0
|
||||||
|
pymdown-extensions==9.4
|
||||||
|
|
||||||
|
# MkDocs plugins
|
||||||
|
mkdocs-material-extensions==1.0.3
|
Loading…
Reference in New Issue
Block a user