diff --git a/README.md b/README.md index 9038cca..83ee8f3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,20 @@ A powerful and flexible PHP library for formatting and transforming strings. composer require respect/string-formatter ``` +## Usage + +You can use individual formatters directly or chain multiple formatters together using the `FormatterBuilder`: + +```php +use Respect\StringFormatter\FormatterBuilder as f; + +echo f::create() + ->mask('7-12') + ->pattern('#### #### #### ####') + ->format('1234123412341234'); +// Output: 1234 12** **** 1234 +``` + ## Formatters | Formatter | Description | diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ca3a183..fc67f9e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,6 +3,9 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" cacheDirectory=".phpunit.cache"> + + tests/Integration/ + tests/Unit/ diff --git a/src/FormatterBuilder.php b/src/FormatterBuilder.php new file mode 100644 index 0000000..6fa1f2a --- /dev/null +++ b/src/FormatterBuilder.php @@ -0,0 +1,63 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter; + +use ReflectionClass; +use Respect\StringFormatter\Mixin\Builder; + +use function array_reduce; +use function ucfirst; + +/** @mixin Builder */ +final readonly class FormatterBuilder implements Formatter +{ + /** @var array */ + private array $formatters; + + public function __construct(Formatter ...$formatters) + { + $this->formatters = $formatters; + } + + public static function create(Formatter ...$formatters): self + { + return new self(...$formatters); + } + + public function format(string $input): string + { + if ($this->formatters === []) { + throw new InvalidFormatterException('No formatters have been added to the builder'); + } + + return array_reduce( + $this->formatters, + static fn(string $carry, Formatter $formatter) => $formatter->format($carry), + $input, + ); + } + + /** @param array $arguments */ + public function __call(string $name, array $arguments): self + { + /** @var class-string $class */ + $class = __NAMESPACE__ . '\\' . ucfirst($name) . 'Formatter'; + $reflection = new ReflectionClass($class); + + return clone($this, ['formatters' => [...$this->formatters, $reflection->newInstanceArgs($arguments)]]); + } + + /** @param array $arguments */ + public static function __callStatic(string $name, array $arguments): self + { + return self::create()->__call($name, $arguments); + } +} diff --git a/src/Mixin/Builder.php b/src/Mixin/Builder.php new file mode 100644 index 0000000..e2ff60f --- /dev/null +++ b/src/Mixin/Builder.php @@ -0,0 +1,24 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Mixin; + +use Respect\StringFormatter\FormatterBuilder; + +/** @mixin FormatterBuilder */ +interface Builder +{ + public static function mask(string $range, string $replacement = '*'): Chain; + + public static function pattern(string $pattern): Chain; + + /** @param array $parameters */ + public static function placeholder(array $parameters): Chain; +} diff --git a/src/Mixin/Chain.php b/src/Mixin/Chain.php new file mode 100644 index 0000000..3223a04 --- /dev/null +++ b/src/Mixin/Chain.php @@ -0,0 +1,24 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Mixin; + +use Respect\StringFormatter\Formatter; +use Respect\StringFormatter\FormatterBuilder; + +interface Chain extends Formatter +{ + public function mask(string $range, string $replacement = '*'): FormatterBuilder; + + public function pattern(string $pattern): FormatterBuilder; + + /** @param array $parameters */ + public function placeholder(array $parameters): FormatterBuilder; +} diff --git a/tests/Integration/FormatterBuilderTest.php b/tests/Integration/FormatterBuilderTest.php new file mode 100644 index 0000000..3e02480 --- /dev/null +++ b/tests/Integration/FormatterBuilderTest.php @@ -0,0 +1,198 @@ + + */ + +declare(strict_types=1); + +namespace Respect\StringFormatter\Test\Integration; + +use ArgumentCountError; +use Error; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use ReflectionException; +use Respect\StringFormatter\FormatterBuilder; +use Respect\StringFormatter\InvalidFormatterException; +use Respect\StringFormatter\MaskFormatter; +use Respect\StringFormatter\PatternFormatter; +use Respect\StringFormatter\PlaceholderFormatter; + +use function sprintf; + +#[CoversClass(FormatterBuilder::class)] +final class FormatterBuilderTest extends TestCase +{ + #[Test] + public function itShouldFormatWithSingleFormatter(): void + { + $input = '1234123412341234'; + $range = '1-3,8-12'; + $maskFormatter = new MaskFormatter($range); + $expected = $maskFormatter->format($input); + + $builder = new FormatterBuilder(); + + $actual = $builder->mask($range)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldFormatWithMultipleFormatters(): void + { + $input = '1234123412341234'; + $range = '1-3,8-12'; + $pattern = '#### #### #### ####'; + $maskFormatter = new MaskFormatter($range); + $patternFormatter = new PatternFormatter($pattern); + $expected = $patternFormatter->format($maskFormatter->format($input)); + + $builder = new FormatterBuilder(); + + $actual = $builder->mask($range)->pattern($pattern)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldThrowExceptionWhenFormattingWithoutFormatters(): void + { + $builder = new FormatterBuilder(); + + $this->expectException(InvalidFormatterException::class); + $this->expectExceptionMessage('No formatters have been added to the builder'); + + $builder->format('test'); + } + + #[Test] + public function itShouldAllowCallingSameFormatterMultipleTimes(): void + { + $input = '1234567890'; + $firstRange = '1-3'; + $secondRange = '5-7'; + $firstMaskFormatter = new MaskFormatter($firstRange); + $secondMaskFormatter = new MaskFormatter($secondRange); + $expected = $secondMaskFormatter->format($firstMaskFormatter->format($input)); + + $builder = new FormatterBuilder(); + $builder = $builder->mask($firstRange)->mask($secondRange); + + $actual = $builder->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldCreateMaskFormatterUsingStaticFactory(): void + { + $input = '1234567890'; + $range = '1-3'; + $maskFormatter = new MaskFormatter($range); + $expected = $maskFormatter->format($input); + + $actual = FormatterBuilder::mask($range)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldCreatePatternFormatterUsingStaticFactory(): void + { + $input = '1234567890'; + $pattern = '###-###-####'; + $patternFormatter = new PatternFormatter($pattern); + $expected = $patternFormatter->format($input); + + $actual = FormatterBuilder::pattern($pattern)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldCreatePlaceholderFormatterUsingStaticFactory(): void + { + $input = 'Hello, {{name}}!'; + $parameters = ['name' => 'World']; + $placeholderFormatter = new PlaceholderFormatter($parameters); + $expected = $placeholderFormatter->format($input); + + $actual = FormatterBuilder::placeholder($parameters)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldUsePlaceholderFormatter(): void + { + $input = 'Hello, {{name}}! Your balance is {{amount}}.'; + $parameters = [ + 'name' => 'John', + 'amount' => 100.5, + ]; + $expected = (new PlaceholderFormatter($parameters))->format($input); + + $builder = new FormatterBuilder(); + $actual = $builder->placeholder($parameters)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldBuildFormatterWithMultipleArguments(): void + { + $input = '1234567890'; + $range = '1-3,7-9'; + $replacement = 'X'; + $maskFormatter = new MaskFormatter($range, $replacement); + $expected = $maskFormatter->format($input); + + $builder = new FormatterBuilder(); + $actual = $builder->mask($range, $replacement)->format($input); + + self::assertSame($expected, $actual); + } + + #[Test] + public function itShouldThrowExceptionWhenFormatterIsNotInstantiable(): void + { + $builder = new FormatterBuilder(); + + $this->expectException(Error::class); + $this->expectExceptionMessage('Cannot instantiate interface Respect\StringFormatter\Formatter'); + + $builder->__call('', []); + } + + #[Test] + public function itShouldThrowExceptionWhenFormatterArgumentIsMissing(): void + { + $builder = new FormatterBuilder(); + + $this->expectException(ArgumentCountError::class); + $this->expectExceptionMessage(sprintf( + 'Too few arguments to function %s::__construct(), 0 passed and exactly 1 expected', + PatternFormatter::class, + )); + + /** @phpstan-ignore arguments.count */ + $builder->pattern(); + } + + #[Test] + public function itShouldThrowExceptionWhenFormatterDoesNotExist(): void + { + $builder = new FormatterBuilder(); + + $this->expectException(ReflectionException::class); + $this->expectExceptionMessage('Class "Respect\StringFormatter\NonexistentFormatter" does not exist'); + + /** @phpstan-ignore method.notFound */ + $builder->nonexistent(); + } +}