From cb87925aac45ca1babea3f34b6cd6e24c9172905 Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Thu, 29 Sep 2022 13:18:12 +0200 Subject: [PATCH] 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(); ``` --- docs/pages/other/dealing-with-dates.md | 2 +- docs/pages/validation.md | 19 +- src/Mapper/Tree/Message/MessageBuilder.php | 204 ++++++++++++++++++ src/Mapper/Tree/Message/ThrowableMessage.php | 6 +- src/MapperBuilder.php | 2 +- .../Tree/Message/MessageBuilderTest.php | 86 ++++++++ 6 files changed, 307 insertions(+), 12 deletions(-) create mode 100644 src/Mapper/Tree/Message/MessageBuilder.php create mode 100644 tests/Unit/Mapper/Tree/Message/MessageBuilderTest.php diff --git a/docs/pages/other/dealing-with-dates.md b/docs/pages/other/dealing-with-dates.md index c455800..65e5f30 100644 --- a/docs/pages/other/dealing-with-dates.md +++ b/docs/pages/other/dealing-with-dates.md @@ -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; diff --git a/docs/pages/validation.md b/docs/pages/validation.md index d2e9de7..746f766 100644 --- a/docs/pages/validation.md +++ b/docs/pages/validation.md @@ -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 diff --git a/src/Mapper/Tree/Message/MessageBuilder.php b/src/Mapper/Tree/Message/MessageBuilder.php new file mode 100644 index 0000000..74d6580 --- /dev/null +++ b/src/Mapper/Tree/Message/MessageBuilder.php @@ -0,0 +1,204 @@ +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 */ + private array $parameters = []; + + private function __construct(string $body) + { + $this->body = $body; + } + + /** + * @return self + */ + public static function new(string $body): self + { + return new self($body); + } + + /** + * @return self + */ + public static function newError(string $body): self + { + $instance = new self($body); + $instance->isError = true; + + /** @var self */ + 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 + */ + public function withBody(string $body): self + { + $clone = clone $this; + $clone->body = $body; + + return $clone; + } + + public function body(): string + { + return $this->body; + } + + /** + * @return self + */ + public function withCode(string $code): self + { + $clone = clone $this; + $clone->code = $code; + + return $clone; + } + + public function code(): string + { + return $this->code; + } + + /** + * @return self + */ + public function withParameter(string $name, string $value): self + { + $clone = clone $this; + $clone->parameters[$name] = $value; + + return $clone; + } + + /** + * @return array + */ + 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 */ + private array $parameters; + + /** + * @param array $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 */ + private array $parameters; + + /** + * @param array $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; + } + }; + } +} diff --git a/src/Mapper/Tree/Message/ThrowableMessage.php b/src/Mapper/Tree/Message/ThrowableMessage.php index 609cf53..2b94a7a 100644 --- a/src/Mapper/Tree/Message/ThrowableMessage.php +++ b/src/Mapper/Tree/Message/ThrowableMessage.php @@ -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 diff --git a/src/MapperBuilder.php b/src/MapperBuilder.php index a279c1e..f308e2b 100644 --- a/src/MapperBuilder.php +++ b/src/MapperBuilder.php @@ -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 diff --git a/tests/Unit/Mapper/Tree/Message/MessageBuilderTest.php b/tests/Unit/Mapper/Tree/Message/MessageBuilderTest.php new file mode 100644 index 0000000..7829d5f --- /dev/null +++ b/tests/Unit/Mapper/Tree/Message/MessageBuilderTest.php @@ -0,0 +1,86 @@ +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); + } +}