feat: introduce utility class to build messages

The new class `\CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder` can be
used to easily create an instance of (error) message.

This new straightforward way of creating messages leads to the
depreciation of `\CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage`.

```php
$message = MessageBuilder::newError('Some message / {some_parameter}.')
    ->withCode('some_code')
    ->withParameter('some_parameter', 'some_value')
    ->build();
```
This commit is contained in:
Romain Canon 2022-09-29 13:18:12 +02:00
parent de8aa9f440
commit cb87925aac
6 changed files with 307 additions and 12 deletions

View File

@ -37,7 +37,7 @@ Here is an implementation example for the [nesbot/carbon] library:
// Carbon uses its own exceptions, so we need to wrap it for the mapper
->filterExceptions(function (Throwable $exception) {
if ($exception instanceof \Carbon\Exceptions\Exception) {
return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
return \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::from($exception);
}
throw $exception;

View File

@ -110,21 +110,22 @@ try {
}
```
### 2. Use provided message class
### 2. Use provided message builder
The built-in class `\CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage` can be
thrown.
The utility class `\CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder` can be used
to build a message.
```php
final class SomeClass
{
public function __construct(private string $value)
{
if ($this->value === 'foo') {
throw \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::new(
'Some custom error message.',
'some_code'
);
if (str_starts_with($this->value, 'foo_')) {
throw \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::newError(
'Some custom error message: {value}.'
)
->withCode('some_code')
->withParameter('value', $this->value);
}
}
}
@ -161,7 +162,7 @@ try {
(new \CuyZ\Valinor\MapperBuilder())
->filterExceptions(function (Throwable $exception) {
if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
return \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::from($exception);
}
// If the exception should not be caught by this library, it

View File

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Message;
use LanguageServerProtocol\MessageType;
use RuntimeException;
use Throwable;
/**
* Can be used to easily create an instance of (error) message.
*
* ```php
* $message = MessageBuilder::newError('Some message with {some_parameter}.')
* ->withCode('some_code')
* ->withParameter('some_parameter', 'some_value')
* ->build();
* ```
*
* @api
*
* @template MessageType of Message
*/
final class MessageBuilder
{
private bool $isError = false;
private string $body;
private string $code = 'unknown';
/** @var array<string, string> */
private array $parameters = [];
private function __construct(string $body)
{
$this->body = $body;
}
/**
* @return self<Message>
*/
public static function new(string $body): self
{
return new self($body);
}
/**
* @return self<ErrorMessage>
*/
public static function newError(string $body): self
{
$instance = new self($body);
$instance->isError = true;
/** @var self<ErrorMessage> */
return $instance;
}
public static function from(Throwable $error): ErrorMessage
{
if ($error instanceof ErrorMessage) {
return $error;
}
return self::newError($error->getMessage())
->withCode((string)$error->getCode())
->build();
}
/**
* @return self<MessageType>
*/
public function withBody(string $body): self
{
$clone = clone $this;
$clone->body = $body;
return $clone;
}
public function body(): string
{
return $this->body;
}
/**
* @return self<MessageType>
*/
public function withCode(string $code): self
{
$clone = clone $this;
$clone->code = $code;
return $clone;
}
public function code(): string
{
return $this->code;
}
/**
* @return self<MessageType>
*/
public function withParameter(string $name, string $value): self
{
$clone = clone $this;
$clone->parameters[$name] = $value;
return $clone;
}
/**
* @return array<string, string>
*/
public function parameters(): array
{
return $this->parameters;
}
/**
* @return MessageType&HasCode&HasParameters
*/
public function build(): Message
{
/** @var MessageType&HasCode&HasParameters */
return $this->isError
? $this->buildErrorMessage()
: $this->buildMessage();
}
private function buildMessage(): Message
{
return new class ($this->body, $this->code, $this->parameters) implements Message, HasCode, HasParameters {
private string $body;
private string $code;
/** @var array<string, string> */
private array $parameters;
/**
* @param array<string, string> $parameters
*/
public function __construct(string $body, string $code, array $parameters)
{
$this->body = $body;
$this->code = $code;
$this->parameters = $parameters;
}
public function body(): string
{
return $this->body;
}
public function code(): string
{
return $this->code;
}
public function parameters(): array
{
return $this->parameters;
}
};
}
private function buildErrorMessage(): ErrorMessage
{
return new class ($this->body, $this->code, $this->parameters) extends RuntimeException implements ErrorMessage, HasCode, HasParameters {
/** @var array<string, string> */
private array $parameters;
/**
* @param array<string, string> $parameters
*/
public function __construct(string $body, string $code, array $parameters)
{
parent::__construct($body);
$this->code = $code;
$this->parameters = $parameters;
}
public function body(): string
{
return $this->message;
}
public function code(): string
{
return $this->code;
}
public function parameters(): array
{
return $this->parameters;
}
};
}
}

View File

@ -7,7 +7,11 @@ namespace CuyZ\Valinor\Mapper\Tree\Message;
use RuntimeException;
use Throwable;
/** @api */
/**
* @api
*
* @deprecated Use {@see \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder}
*/
final class ThrowableMessage extends RuntimeException implements ErrorMessage, HasCode
{
public static function new(string $message, string $code): self

View File

@ -291,7 +291,7 @@ final class MapperBuilder
* (new \CuyZ\Valinor\MapperBuilder())
* ->filterExceptions(function (Throwable $exception) {
* if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
* return \CuyZ\Valinor\Mapper\Tree\Message\ThrowableMessage::from($exception);
* return \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::from($exception);
* }
*
* // If the exception should not be caught by this library, it must

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree\Message;
use CuyZ\Valinor\Mapper\Tree\Message\HasCode;
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use Exception;
use PHPUnit\Framework\TestCase;
final class MessageBuilderTest extends TestCase
{
public function test_body_can_be_retrieved(): void
{
$message = MessageBuilder::new('some message body');
$messageError = MessageBuilder::newError('some error message body');
self::assertSame('some message body', $message->body());
self::assertSame('some message body', $message->build()->body());
self::assertSame('some error message body', $messageError->body());
self::assertSame('some error message body', $messageError->build()->body());
$message = $message->withBody('some new message body');
$messageError = $messageError->withBody('some new error message body');
self::assertSame('some new message body', $message->body());
self::assertSame('some new message body', $message->build()->body());
self::assertSame('some new error message body', $messageError->body());
self::assertSame('some new error message body', $messageError->build()->body());
}
public function test_code_can_be_retrieved(): void
{
$message = MessageBuilder::new('some message body');
$message = $message->withCode('some_code');
self::assertSame('some_code', $message->code());
self::assertSame('some_code', $message->build()->code());
}
public function test_parameters_can_be_retrieved(): void
{
$message = MessageBuilder::new('some message body');
$message = $message
->withParameter('parameter_a', 'valueA')
->withParameter('parameter_b', 'valueB');
self::assertSame(['parameter_a' => 'valueA', 'parameter_b' => 'valueB'], $message->parameters());
self::assertSame(['parameter_a' => 'valueA', 'parameter_b' => 'valueB'], $message->build()->parameters());
}
public function test_modifiers_return_clone_instances(): void
{
$messageA = MessageBuilder::new('some message body');
$messageB = $messageA->withBody('some new message body');
$messageC = $messageB->withCode('some_code');
$messageD = $messageC->withParameter('parameter_a', 'valueA');
self::assertNotSame($messageA, $messageB);
self::assertNotSame($messageB, $messageC);
self::assertNotSame($messageC, $messageD);
}
public function test_from_throwable_build_error_message(): void
{
$exception = new Exception('some error message', 1664450422);
$message = MessageBuilder::from($exception);
self::assertSame('some error message', $message->body());
self::assertInstanceOf(HasCode::class, $message);
self::assertSame('1664450422', $message->code());
}
public function test_from_error_message_returns_same_instance(): void
{
$error = new FakeErrorMessage();
$message = MessageBuilder::from($error);
self::assertSame($error, $message);
}
}