mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
feat: introduce helper class MessagesFlattener
Will recursively flatten messages of a node and all its children. This helper can for instance be used when errors occurred during a mapping to flatten all caught errors into a basic array of string that can then easily be used to inform the user of what is wrong. ``` try { // … } catch(MappingError $error) { $messages = (new MessagesFlattener($error->node()))->errors(); foreach ($messages as $message) { echo $message; } } ```
This commit is contained in:
parent
ddf69efaaa
commit
a97b406154
110
README.md
110
README.md
@ -202,11 +202,7 @@ try {
|
||||
['someValue' => 'bar_baz']
|
||||
);
|
||||
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
|
||||
$node = $error->node()->children()['someValue'];
|
||||
|
||||
// Should print something similar to:
|
||||
// > Expected a value to start with "foo_". Got: "bar_baz"
|
||||
var_dump($node->messages()[0]);
|
||||
$node = $error->node();
|
||||
|
||||
// The name of a node can be accessed
|
||||
$name = $node->name();
|
||||
@ -217,24 +213,100 @@ try {
|
||||
// The type of the node can be cast to string to enhance suggestion messages
|
||||
$type = (string)$node->type();
|
||||
|
||||
// It is important to check if a node is valid before getting its value
|
||||
if ($node->isValid()) {
|
||||
// The processed value of the node can be different from original input
|
||||
$value = $node->value();
|
||||
}
|
||||
|
||||
// All messages bound to the node can be accessed
|
||||
foreach ($node->messages() as $message) {
|
||||
// Errors can be retrieved by filtering like below:
|
||||
if ($message->isError()) {
|
||||
// Do something…
|
||||
}
|
||||
}
|
||||
|
||||
// If the node is a branch, its children can be recursively accessed
|
||||
foreach ($node->children() as $child) {
|
||||
// Do something…
|
||||
}
|
||||
|
||||
// Get flatten list of all messages through the whole nodes tree
|
||||
$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 ($errorsMessages as $message) {
|
||||
echo $message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Message customization / translation
|
||||
|
||||
When working with messages, it can sometimes be useful to customize the content
|
||||
of a message — for instance to translate it.
|
||||
|
||||
The helper class `\CuyZ\Valinor\Mapper\Tree\Message\MessageMapFormatter` can be
|
||||
used to provide a list of new formats. It can be instantiated with an array
|
||||
where each key represents either:
|
||||
|
||||
- The code of the message to be replaced
|
||||
- The content 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 translation service is used — to
|
||||
avoid useless greedy operations.
|
||||
|
||||
In any case, the content can contain placeholders that will automatically be
|
||||
replaced by, in order:
|
||||
|
||||
1. The original code of the message
|
||||
2. The original content of the message
|
||||
3. A string representation of the node type
|
||||
4. The name of the node
|
||||
5. The path of the node
|
||||
|
||||
```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);
|
||||
|
||||
$formatter = (new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([
|
||||
// Will match if the given message has this exact code
|
||||
'some_code' => 'new content / previous code was: %1$s',
|
||||
|
||||
// Will match if the given message has this exact content
|
||||
'Some message content' => 'new content / previous message: %2$s',
|
||||
|
||||
// Will match if the given message is an instance of `SomeError`
|
||||
SomeError::class => '
|
||||
- Original code of the message: %1$s
|
||||
- Original content of the message: %2$s
|
||||
- Node type: %3$s
|
||||
- Node name: %4$s
|
||||
- Node path: %5$s
|
||||
',
|
||||
|
||||
// A callback can be used to get access to the message instance
|
||||
OtherError::class => function (NodeMessage $message): string {
|
||||
if ((string)$message->type() === 'string|int') {
|
||||
// …
|
||||
}
|
||||
|
||||
return 'Some message content';
|
||||
},
|
||||
|
||||
// For greedy operation, it is advised to use a lazy-callback
|
||||
'foo' => fn () => $this->translator->translate('foo.bar'),
|
||||
]))
|
||||
->defaultsTo('some default message')
|
||||
// …or…
|
||||
->defaultsTo(fn () => $this->translator->translate('default_message'));
|
||||
|
||||
foreach ($messages as $message) {
|
||||
echo $formatter->format($message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
66
src/Mapper/Tree/Message/MessagesFlattener.php
Normal file
66
src/Mapper/Tree/Message/MessagesFlattener.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Message;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Node;
|
||||
use CuyZ\Valinor\Mapper\Tree\NodeTraverser;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
use function array_filter;
|
||||
|
||||
/**
|
||||
* Will recursively flatten messages of a node and all its children.
|
||||
*
|
||||
* This helper can for instance be used when errors occurred during a mapping to
|
||||
* flatten all caught errors into a basic array of string that can then easily
|
||||
* be used to inform the user of what is wrong.
|
||||
*
|
||||
* ```
|
||||
* try {
|
||||
* // …
|
||||
* } catch(MappingError $error) {
|
||||
* $messages = (new MessagesFlattener($error->node()))->errors();
|
||||
*
|
||||
* foreach ($messages as $message) {
|
||||
* echo $message;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @implements IteratorAggregate<NodeMessage>
|
||||
*/
|
||||
final class MessagesFlattener implements IteratorAggregate
|
||||
{
|
||||
/** @var array<NodeMessage> */
|
||||
private array $messages = [];
|
||||
|
||||
public function __construct(Node $node)
|
||||
{
|
||||
$grouped = (new NodeTraverser(
|
||||
fn (Node $node) => $node->messages()
|
||||
))->traverse($node);
|
||||
|
||||
foreach ($grouped as $messages) {
|
||||
$this->messages = [...$this->messages, ...$messages];
|
||||
}
|
||||
}
|
||||
|
||||
public function errors(): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->messages = array_filter($clone->messages, fn (NodeMessage $message) => $message->isError());
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<NodeMessage>
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
yield from $this->messages;
|
||||
}
|
||||
}
|
31
tests/Unit/Mapper/Tree/Message/MessagesFlattenerTest.php
Normal file
31
tests/Unit/Mapper/Tree/Message/MessagesFlattenerTest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree\Message;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\MessagesFlattener;
|
||||
use CuyZ\Valinor\Tests\Fake\Mapper\FakeNode;
|
||||
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
|
||||
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeMessage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class MessagesFlattenerTest extends TestCase
|
||||
{
|
||||
public function test_messages_are_filtered_and_can_be_iterated_through(): void
|
||||
{
|
||||
$messageA = new FakeMessage();
|
||||
$errorA = new FakeErrorMessage('some error message A');
|
||||
$errorB = new FakeErrorMessage('some error message B');
|
||||
|
||||
$node = FakeNode::branch([
|
||||
'foo' => ['message' => $messageA],
|
||||
'bar' => ['message' => $errorA],
|
||||
])->withMessage($errorB);
|
||||
|
||||
$messages = [...(new MessagesFlattener($node))->errors()];
|
||||
|
||||
self::assertSame('some error message B', (string)$messages[0]);
|
||||
self::assertSame('some error message A', (string)$messages[1]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user