diff --git a/docs/Value-Objects.md b/docs/Value-Objects.md index 0d2f6ef..a85e7a2 100644 --- a/docs/Value-Objects.md +++ b/docs/Value-Objects.md @@ -239,6 +239,51 @@ $size->setSeparator('X'); $asString2 = (string)$size; // "200X100" ``` +### Template + +##### Namespace + +`Meritoo\Common\ValueObject\Template` + +##### Info + +Template with placeholders that may be filled by real data. Contains properties: +1. `$content` - raw string with placeholders (content of the template) + +##### New instance + +New instance can be created using constructor: + +```php +new Template('First name: %first_name%'); +``` + +Each placeholder should be wrapped by `%` character, e.g. `%first_name%`. If content of template is an empty string or does not contain 1 placeholder at least, an `Meritoo\Common\Exception\ValueObject\Template\InvalidContentException` exception will be thrown. + +Examples of invalid content of template: + +```php +new Template(''); // An empty string +new Template('test'); // Without placeholders +new Template('This is %test'); // With starting tag only (invalid placeholder) +``` + +##### Methods + +Has 1 public method: `fill(array $values)`. Returns content of the template filled with given values (by replacing placeholders with their proper values). + +Example of usage: + +```php +$template = new Template('My name is %name% and I am %profession%'); +$result = $template->fill([ + 'name' => 'Jane', + 'profession' => 'photographer', +]); // "My name is Jane and I am photographer" +``` + +Throws an `Meritoo\Common\Exception\ValueObject\Template\NotEnoughValuesException` exception if there is not enough values (iow. more placeholders than values). + ### Version ##### Namespace @@ -263,13 +308,13 @@ New instance can be created using: ``` 2. Static methods: - 1. `fromArray()` - creates new instance using given version as array + 1. `fromArray(array $version)` - creates new instance using given version as array ```php Version::fromArray([1, 0, 2]); ``` - 2. `fromString()` - creates new instance using given version as string: + 2. `fromString(string $version)` - creates new instance using given version as string: ```php Version::fromString('1.0.2'); diff --git a/src/Exception/ValueObject/Template/InvalidContentException.php b/src/Exception/ValueObject/Template/InvalidContentException.php new file mode 100644 index 0000000..c9fb140 --- /dev/null +++ b/src/Exception/ValueObject/Template/InvalidContentException.php @@ -0,0 +1,36 @@ + + * @copyright Meritoo + */ +class InvalidContentException extends Exception +{ + /** + * Creates an exception + * + * @param string $content Invalid content of template + * @return InvalidContentException + */ + public static function create(string $content): InvalidContentException + { + $template = 'Content of template \'%s\' is invalid. Did you use string with 1 placeholder at least?'; + $message = sprintf($template, $content); + + return new static($message); + } +} diff --git a/src/Exception/ValueObject/Template/NotEnoughValuesException.php b/src/Exception/ValueObject/Template/NotEnoughValuesException.php new file mode 100644 index 0000000..3ff696b --- /dev/null +++ b/src/Exception/ValueObject/Template/NotEnoughValuesException.php @@ -0,0 +1,39 @@ + + * @copyright Meritoo + */ +class NotEnoughValuesException extends Exception +{ + /** + * Creates an exception + * + * @param string $content Invalid content of template + * @param int $valuesCount Count of values + * @param int $placeholdersCount Count of placeholders + * @return NotEnoughValuesException + */ + public static function create(string $content, int $valuesCount, int $placeholdersCount): NotEnoughValuesException + { + $template = 'Not enough values (%d) to fill all placeholders (%d) in template \'%s\'. Did you provide all' + . ' required values?'; + $message = sprintf($template, $valuesCount, $placeholdersCount, $content); + + return new static($message); + } +} diff --git a/src/ValueObject/Template.php b/src/ValueObject/Template.php new file mode 100644 index 0000000..f3aa60f --- /dev/null +++ b/src/ValueObject/Template.php @@ -0,0 +1,151 @@ + + * @copyright Meritoo + */ +class Template +{ + /** + * Tag used at beginning and ending of placeholder + * + * @var string + */ + private const PLACEHOLDER_TAG = '%'; + + /** + * Raw string with placeholders (content of the template) + * + * @var string + */ + private $content; + + /** + * Class constructor + * + * @param string $content Raw string with placeholders (content of the template) + * @throws InvalidContentException + */ + public function __construct(string $content) + { + if (!static::isValid($content)) { + throw InvalidContentException::create($content); + } + + $this->content = $content; + } + + /** + * Returns content of the template filled with given values (by replacing placeholders with their proper values) + * + * @param array $values Pairs of key-value where: key - name of placeholder, value - value of the placeholder + * @throws NotEnoughValuesException + * @return string + */ + public function fill(array $values): string + { + $placeholders = static::getPlaceholders($this->content); + $valuesCount = count($values); + $placeholdersCount = count($placeholders[0]); + + // Oops, not enough values (iow. more placeholders than values) + if ($placeholdersCount > $valuesCount) { + throw NotEnoughValuesException::create($this->content, $valuesCount, $placeholdersCount); + } + + $result = $this->content; + + foreach ($placeholders[0] as $index => $placeholder) { + $placeholderName = $placeholders[1][$index]; + + if (isset($values[$placeholderName])) { + $value = $values[$placeholderName]; + $result = str_replace($placeholder, $value, $result); + } + } + + return $result; + } + + /** + * Returns information if given template is valid + * + * @param string $content Raw string with placeholders to validate (content of the template) + * @return bool + */ + private static function isValid(string $content): bool + { + if ('' === $content) { + return false; + } + + return (bool)preg_match_all(static::getPlaceholderPattern(), $content); + } + + /** + * Returns placeholders of given template + * + * @param string $content Content of template + * @return array + */ + private static function getPlaceholders(string $content): array + { + $result = []; + $matchCount = preg_match_all(static::getPlaceholderPattern(), $content, $result); + + if (false !== $matchCount && 0 < $matchCount) { + foreach ($result as $index => $placeholders) { + $result[$index] = array_unique($placeholders); + } + } + + return $result; + } + + /** + * Returns regular expression that defines format of placeholder + * + * Expectations: + * - surrounded by the placeholder's tags (at beginning and at the end) + * - at least 1 character + * - no placeholder's tag inside name of placeholder + * + * Invalid placeholders: + * - test + * - test% + * - % test% + * + * Valid placeholders: + * - %test% + * - %another_test% + * - %another-test% + * - %anotherTest% + * - %another test% + * + * @return string + */ + private static function getPlaceholderPattern(): string + { + return sprintf( + '/%s([^%s]+)%s/', + static::PLACEHOLDER_TAG, + static::PLACEHOLDER_TAG, + static::PLACEHOLDER_TAG + ); + } +} diff --git a/src/ValueObject/Version.php b/src/ValueObject/Version.php index 55e7cb4..d24bbe3 100644 --- a/src/ValueObject/Version.php +++ b/src/ValueObject/Version.php @@ -108,7 +108,7 @@ class Version * @param string $version The version * @return Version|null */ - public static function fromString($version) + public static function fromString(string $version) { $version = trim($version); diff --git a/tests/Exception/ValueObject/Template/InvalidContentExceptionTest.php b/tests/Exception/ValueObject/Template/InvalidContentExceptionTest.php new file mode 100644 index 0000000..e5bab54 --- /dev/null +++ b/tests/Exception/ValueObject/Template/InvalidContentExceptionTest.php @@ -0,0 +1,71 @@ + + * @copyright Meritoo + * + * @internal + * @covers \Meritoo\Common\Exception\ValueObject\Template\InvalidContentException + */ +class InvalidContentExceptionTest extends BaseTestCase +{ + public function testConstructorVisibilityAndArguments(): void + { + static::assertConstructorVisibilityAndArguments( + InvalidContentException::class, + OopVisibilityType::IS_PUBLIC, + 3 + ); + } + + /** + * @param string $description Description of test + * @param string $content Invalid content of template + * @param string $expectedMessage Expected exception's message + * + * @dataProvider provideContent + */ + public function testCreate(string $description, string $content, string $expectedMessage): void + { + $exception = InvalidContentException::create($content); + static::assertSame($expectedMessage, $exception->getMessage(), $description); + } + + public function provideContent(): ?Generator + { + $template = 'Content of template \'%s\' is invalid. Did you use string with 1 placeholder at least?'; + + yield[ + 'An empty string', + '', + sprintf($template, ''), + ]; + + yield[ + 'Simple string', + 'Lorem ipsum', + sprintf($template, 'Lorem ipsum'), + ]; + + yield[ + 'One sentence', + 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.', + sprintf($template, 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.'), + ]; + } +} diff --git a/tests/Exception/ValueObject/Template/NotEnoughValuesExceptionTest.php b/tests/Exception/ValueObject/Template/NotEnoughValuesExceptionTest.php new file mode 100644 index 0000000..533fb55 --- /dev/null +++ b/tests/Exception/ValueObject/Template/NotEnoughValuesExceptionTest.php @@ -0,0 +1,85 @@ + + * @copyright Meritoo + * + * @internal + * @covers \Meritoo\Common\Exception\ValueObject\Template\NotEnoughValuesException + */ +class NotEnoughValuesExceptionTest extends BaseTestCase +{ + public function testConstructorVisibilityAndArguments(): void + { + static::assertConstructorVisibilityAndArguments( + NotEnoughValuesException::class, + OopVisibilityType::IS_PUBLIC, + 3 + ); + } + + /** + * @param string $description Description of test + * @param string $content Invalid content of template + * @param int $valuesCount Count of values + * @param int $placeholdersCount Count of placeholders + * @param string $expectedMessage Expected exception's message + * + * @dataProvider provideContentAndValuesPlaceholdersCount + */ + public function testCreate( + string $description, + string $content, + int $valuesCount, + int $placeholdersCount, + string $expectedMessage + ): void { + $exception = NotEnoughValuesException::create($content, $valuesCount, $placeholdersCount); + static::assertSame($expectedMessage, $exception->getMessage(), $description); + } + + public function provideContentAndValuesPlaceholdersCount(): ?Generator + { + $template = 'Not enough values (%d) to fill all placeholders (%d) in template \'%s\'. Did you provide all' + . ' required values?'; + + yield[ + 'An empty string', + '', + 3, + 1, + sprintf($template, 3, 1, ''), + ]; + + yield[ + 'Simple string', + 'Lorem ipsum', + 1, + 4, + sprintf($template, 1, 4, 'Lorem ipsum'), + ]; + + yield[ + 'One sentence', + 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.', + 5, + 0, + sprintf($template, 5, 0, 'Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.'), + ]; + } +} diff --git a/tests/ValueObject/TemplateTest.php b/tests/ValueObject/TemplateTest.php new file mode 100644 index 0000000..6de3828 --- /dev/null +++ b/tests/ValueObject/TemplateTest.php @@ -0,0 +1,215 @@ + + * @copyright Meritoo + * + * @internal + * @covers \Meritoo\Common\ValueObject\Template + */ +class TemplateTest extends BaseTestCase +{ + public function testConstructor(): void + { + static::assertConstructorVisibilityAndArguments( + Template::class, + OopVisibilityType::IS_PUBLIC, + 1, + 1 + ); + } + + /** + * @param string $content Raw string with placeholders (content of the template) + * @param string $exceptionMessage Expected message of exception + * + * @dataProvider provideInvalidContent + */ + public function testIsValidUsingInvalidContent(string $content, string $exceptionMessage): void + { + $this->expectException(InvalidContentException::class); + $this->expectExceptionMessage($exceptionMessage); + + new Template($content); + } + + /** + * @param Template $template Template to fill + * @param array $values Pairs of key-value where: key - name of placeholder, value - value of the + * placeholder + * @param string $exceptionMessage Expected message of exception + * + * @dataProvider provideTemplateToFillUsingNotEnoughValues + */ + public function testFillUsingNotEnoughValues(Template $template, array $values, string $exceptionMessage): void + { + $this->expectException(NotEnoughValuesException::class); + $this->expectExceptionMessage($exceptionMessage); + + $template->fill($values); + } + + /** + * @param string $description Description of test + * @param Template $template Template to fill + * @param array $values Pairs of key-value where: key - name of placeholder, value - value of the + * placeholder + * @param string $expected Expected result + * + * @dataProvider provideTemplateToFill + */ + public function testFill(string $description, Template $template, array $values, string $expected): void + { + static::assertSame($expected, $template->fill($values), $description); + } + + public function provideInvalidContent(): ?Generator + { + $template = 'Content of template \'%s\' is invalid. Did you use string with 1 placeholder at least?'; + + yield[ + 'An empty string' => '', + sprintf($template, ''), + ]; + + yield[ + 'Without placeholders' => 'test', + sprintf($template, 'test'), + ]; + + yield[ + 'With starting tag only (invalid placeholder)' => 'This is %test', + sprintf($template, 'This is %test'), + ]; + + yield[ + 'With ending tag only (invalid placeholder)' => 'This is test%', + sprintf($template, 'This is test%'), + ]; + } + + public function provideTemplateToFillUsingNotEnoughValues(): ?Generator + { + $template = 'Not enough values (%d) to fill all placeholders (%d) in template \'%s\'. Did you provide all' + . ' required values?'; + + yield[ + new Template('%test%'), + [], + sprintf( + $template, + 0, + 1, + '%test%' + ), + ]; + + yield[ + new Template('%test1% - %test2%'), + [ + 'test1' => 123, + ], + sprintf( + $template, + 1, + 2, + '%test1% - %test2%' + ), + ]; + } + + public function provideTemplateToFill(): ?Generator + { + yield[ + 'Template with 1 placeholder, but incorrect values', + new Template('%test%'), + [ + 'something' => 123, + ], + '%test%', + ]; + + yield[ + 'Template with 1 placeholder', + new Template('%test%'), + [ + 'test' => 123, + ], + '123', + ]; + + yield[ + 'Template with 1 placeholder, but more values', + new Template('%test%'), + [ + 'test' => 123, + 'anotherTest' => 456, + ], + '123', + ]; + + yield[ + 'Template with 2 placeholders', + new Template('My name is %name% and I am %profession%'), + [ + 'name' => 'Jane', + 'profession' => 'photographer', + ], + 'My name is Jane and I am photographer', + ]; + + yield[ + 'Template with 2 placeholders, but more values', + new Template('My name is %name% and I am %profession%'), + [ + 'name' => 'Jane', + 'test-test' => 123, + 'profession' => 'photographer', + 'anotherTest' => 456, + ], + 'My name is Jane and I am photographer', + ]; + + yield[ + 'Template with 2 placeholders that contains space', + new Template('My name is %first name% %last name% and I live in %current location%'), + [ + 'first name' => 'Jane', + 'last name' => 'Brown', + 'current location' => 'NY, USA', + ], + 'My name is Jane Brown and I live in NY, USA', + ]; + + yield[ + 'Template with 2 placeholders that contains space, but more values', + new Template('My name is %first name% %last name% and I live in %current location%'), + [ + 'first name' => 'Jane', + 'profession' => 'photographer', + 'last name' => 'Brown', + 'test-test' => 123, + 'anotherTest' => 456, + 'current location' => 'NY, USA', + ], + 'My name is Jane Brown and I live in NY, USA', + ]; + } +}