diff --git a/README.md b/README.md index e05292a..726a87e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # selective/transformer -A strictly typed array transformer with dot access and fluent interface. -The mapped result can be used for JSON responses and many other things. +A strictly typed array transformer with dot access and fluent interface. The mapped result can be used for JSON +responses and many other things. [![Latest Version on Packagist](https://img.shields.io/github/release/selective-php/transformer.svg)](https://packagist.org/packages/selective/transformer) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) @@ -10,6 +10,21 @@ The mapped result can be used for JSON responses and many other things. [![Quality Score](https://img.shields.io/scrutinizer/quality/g/selective-php/transformer.svg)](https://scrutinizer-ci.com/g/selective-php/transformer/?branch=master) [![Total Downloads](https://img.shields.io/packagist/dt/selective/transformer.svg)](https://packagist.org/packages/selective/transformer/stats) +## Table of Contents + +* [Requirements](#requirements) +* [Installation](#installation) +* [Usage](#usage) + * [Transforming](#transforming) + * [Transforming list of arrays](#transforming-list-of-arrays) +* [Dot access](#dot-access) +* [Mapping rules](#mapping-rules) + * [Simple mapping rules](#simple-mapping-rules) + * [Complex mapping rules](#complex-mapping-rules) +* [Filter](#filter) + * [Custom Filter](#custom-filter) +* [License](#license) + ## Requirements * PHP 7.2+ or 8.0+ @@ -22,6 +37,8 @@ composer require selective/transformer ## Usage +### Transforming + Sample data: ```php @@ -32,8 +49,6 @@ $data = [ ]; ``` -### Minimal example - ```php map('id', 'id', $transformer->rule()->integer()) + ->map('first_name', 'first_name', $transformer->rule()->string()) + ->map('last_name', 'last_name', $transformer->rule()->string()) + ->map('phone', 'phone', $transformer->rule()->string()) + ->map('enabled', 'enabled', $transformer->rule()->boolean()); + +$rows = []; +$rows[] = [ + 'id' => '100', + 'first_name' => 'Sally', + 'last_name' => '', + 'phone' => null, + 'enabled' => '1', +]; + +$rows[] = [ + 'id' => '101', + 'first_name' => 'Max', + 'last_name' => 'Doe', + 'phone' => '+123456789', + 'enabled' => '0', +]; + +$result = $transformer->toArrays($rows); +``` + +The result: -You can copy any data from the source array to any sub-element of the destination array -using the dot-syntax. +```php +[ + [ + 'id' => 100, + 'first_name' => 'Sally', + 'enabled' => true, + ], + [ + 'id' => 101, + 'first_name' => 'Max', + 'last_name' => 'Doe', + 'phone' => '+123456789', + 'enabled' => false, + ], +] +``` + +## Dot access + +You can copy any data from the source array to any sub-element of the destination array using the dot-syntax. ```php map('firstName', 'address.first_name') ->map('invoice.items', 'root.sub1.sub2.items'); ``` +## Mapping Rules + ### Simple mapping rules Using strings, separated by `|`, to define a filter chain: @@ -170,7 +242,7 @@ $transformer->rule()->callback( ); ``` -## Custom filters +### Custom Filter You can also add your own custom filter: diff --git a/src/ArrayTransformer.php b/src/ArrayTransformer.php index b51dff7..c5054c5 100644 --- a/src/ArrayTransformer.php +++ b/src/ArrayTransformer.php @@ -13,6 +13,9 @@ use Selective\Transformer\Filter\NumberFormatFilter; use Selective\Transformer\Filter\StringFilter; +/** + * Transformer. + */ final class ArrayTransformer { /** @@ -25,35 +28,52 @@ final class ArrayTransformer */ private $converter; + /** + * @var string[] + */ + private $internalFilters = [ + 'string' => StringFilter::class, + 'blank-to-null' => BlankToNullFilter::class, + 'boolean' => BooleanFilter::class, + 'integer' => IntegerFilter::class, + 'float' => FloatFilter::class, + 'number' => NumberFormatFilter::class, + 'date' => DateTimeFilter::class, + 'array' => ArrayFilter::class, + 'callback' => CallbackFilter::class, + ]; + + /** + * The constructor. + */ public function __construct() { $this->converter = new ArrayValueConverter(); - $this->registerFilter('string', StringFilter::class); - $this->registerFilter('blank-to-null', BlankToNullFilter::class); - $this->registerFilter('boolean', BooleanFilter::class); - $this->registerFilter('integer', IntegerFilter::class); - $this->registerFilter('float', FloatFilter::class); - $this->registerFilter('number', NumberFormatFilter::class); - $this->registerFilter('date', DateTimeFilter::class); - $this->registerFilter('array', ArrayFilter::class); - $this->registerFilter('callback', CallbackFilter::class); + + foreach ($this->internalFilters as $name => $class) { + $this->registerFilter($name, new $class()); + } } /** - * @param string $string - * @param callable|string $filter + * Register custom filter. + * + * @param string $name The name + * @param callable $filter The filter callback */ - public function registerFilter(string $string, $filter): void + public function registerFilter(string $name, callable $filter): void { - $this->converter->registerFilter($string, $filter); + $this->converter->registerFilter($name, $filter); } /** - * @param string $destination - * @param string $source - * @param ArrayTransformerRule|string|null $rule + * Add mapping rule. + * + * @param string $destination The destination element + * @param string $source The source element + * @param ArrayTransformerRule|string|null $rule The rule * - * @return $this + * @return $this The transformer */ public function map(string $destination, string $source, $rule = null): self { @@ -68,11 +88,23 @@ public function map(string $destination, string $source, $rule = null): self return $this; } + /** + * Create transformer rule. + * + * @return ArrayTransformerRule The rule + */ public function rule(): ArrayTransformerRule { return new ArrayTransformerRule(); } + /** + * Convert rule string to rule object. + * + * @param string $rules The rules, separated by '|' + * + * @return ArrayTransformerRule The rule object + */ private function ruleFromString(string $rules): ArrayTransformerRule { $rule = $this->rule(); @@ -90,10 +122,12 @@ private function ruleFromString(string $rules): ArrayTransformerRule } /** - * @param array $source - * @param array $target + * Transform array to array. + * + * @param array $source The source + * @param array $target The target (optional) * - * @return array + * @return array The result */ public function toArray(array $source, array $target = []): array { @@ -105,7 +139,7 @@ public function toArray(array $source, array $target = []): array $value = $this->converter->convert($value, $rule); if ($value === null && !$rule->isRequired()) { - // Don't add item to result + // Skip item continue; } @@ -114,4 +148,21 @@ public function toArray(array $source, array $target = []): array return $targetData->export(); } + + /** + * Transform list of arrays to list of arrays. + * + * @param array $source The source + * @param array $target The target (optional) + * + * @return array The result + */ + public function toArrays(array $source, array $target = []): array + { + foreach ($source as $item) { + $target[] = $this->toArray($item); + } + + return $target; + } } diff --git a/src/ArrayTransformerFilter.php b/src/ArrayTransformerFilter.php deleted file mode 100644 index 94cd561..0000000 --- a/src/ArrayTransformerFilter.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ - private $params; - - /** - * The constructor. - * - * @param string $name - * @param array $params - */ - public function __construct(string $name, array $params = []) - { - $this->name = $name; - $this->params = $params; - } - - /** - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * @return array - */ - public function getParams(): array - { - return $this->params; - } -} diff --git a/src/ArrayTransformerFilterItem.php b/src/ArrayTransformerFilterItem.php new file mode 100644 index 0000000..f9c049c --- /dev/null +++ b/src/ArrayTransformerFilterItem.php @@ -0,0 +1,51 @@ + + */ + private $arguments; + + /** + * The constructor. + * + * @param string $name The filter to apply + * @param array $arguments The parameters for the filter + */ + public function __construct(string $name, array $arguments = []) + { + $this->name = $name; + $this->arguments = $arguments; + } + + /** + * The filter to apply. + * + * @return string The name + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get filter parameters. + * + * @return array The params + */ + public function getArguments(): array + { + return $this->arguments; + } +} diff --git a/src/ArrayTransformerRule.php b/src/ArrayTransformerRule.php index 93ca9b9..125c931 100644 --- a/src/ArrayTransformerRule.php +++ b/src/ArrayTransformerRule.php @@ -4,6 +4,9 @@ use DateTimeZone; +/** + * Rule. + */ final class ArrayTransformerRule { /** @@ -27,10 +30,17 @@ final class ArrayTransformerRule private $required = false; /** - * @var ArrayTransformerFilter[] + * @var ArrayTransformerFilterItem[] */ private $filters = []; + /** + * Add destination. + * + * @param string $destination The destination element name + * + * @return $this The rule + */ public function destination(string $destination): self { $this->destination = $destination; @@ -38,6 +48,13 @@ public function destination(string $destination): self return $this; } + /** + * Set source name. + * + * @param string $source The element name + * + * @return $this Self + */ public function source(string $source): self { $this->source = $source; @@ -45,16 +62,31 @@ public function source(string $source): self return $this; } + /** + * Get source. + * + * @return string The source name + */ public function getSource(): string { return $this->source; } + /** + * Get destination. + * + * @return string The destination name + */ public function getDestination(): string { return $this->destination; } + /** + * Set required. + * + * @return $this Self + */ public function required(): self { $this->required = true; @@ -63,9 +95,21 @@ public function required(): self } /** - * @param mixed $default + * Get required status. * - * @return $this + * @return bool The status + */ + public function isRequired(): bool + { + return $this->required; + } + + /** + * Set default value. + * + * @param mixed $default The value + * + * @return $this Self */ public function default($default = null): self { @@ -74,13 +118,10 @@ public function default($default = null): self return $this; } - public function isRequired(): bool - { - return $this->required; - } - /** - * @return mixed|null + * Get default value. + * + * @return mixed|null The value */ public function getDefault() { @@ -88,63 +129,118 @@ public function getDefault() } /** - * @param string $name - * @param mixed ...$parameters + * Get filters. * - * @return $this + * @return ArrayTransformerFilterItem[] The filters */ - public function filter(string $name, ...$parameters): self + public function getFiltersItems(): array { - $this->filters[] = new ArrayTransformerFilter($name, $parameters); - - return $this; + return $this->filters; } /** - * @return ArrayTransformerFilter[] + * Add string filter. + * + * @param bool $blankToNull Convert blank string to null (default) + * + * @return $this Self */ - public function getFilters(): array + public function string(bool $blankToNull = true): self { - return $this->filters; + $this->filter($blankToNull ? 'blank-to-null' : 'string'); + + return $this; } - public function string(bool $blankToNull = true): self + /** + * Add filter. + * + * @param string $name The filter name + * @param mixed ...$parameters The filter arguments + * + * @return $this Self + */ + public function filter(string $name, ...$parameters): self { - $this->filter($blankToNull ? 'blank-to-null' : 'string'); + $this->filters[] = new ArrayTransformerFilterItem($name, $parameters); return $this; } + /** + * Add boolean filter. + * + * @return $this Self + */ public function boolean(): self { return $this->filter('boolean'); } + /** + * Add float filter. + * + * @return $this Self + */ public function float(): self { return $this->filter('float'); } + /** + * Add integer filter. + * + * @return $this Self + */ public function integer(): self { return $this->filter('integer'); } + /** + * Add number format filter. + * + * @param int $decimals The number of decimals + * @param string $decimalSeparator The decimal separator + * @param string $thousandsSeparator The Thousands separator + * + * @return $this Self + */ public function number(int $decimals = 0, string $decimalSeparator = '.', string $thousandsSeparator = ','): self { return $this->filter('number', $decimals, $decimalSeparator, $thousandsSeparator); } + /** + * Add date filter. + * + * @param string $format The date format + * @param DateTimeZone|null $dateTimeZone The time zone + * + * @return $this Self + */ public function date(string $format = 'Y-m-d H:i:s', DateTimeZone $dateTimeZone = null): self { return $this->filter('date', $format, $dateTimeZone); } + /** + * Add array filter. + * + * @return $this Self + */ public function array(): self { return $this->filter('array'); } + /** + * Add callback filter. + * + * @param callable $callback The callback + * + * @return $this Self + */ public function callback(callable $callback): self { return $this->filter('callback', $callback); diff --git a/src/ArrayValueConverter.php b/src/ArrayValueConverter.php index 4ecf076..db18e33 100644 --- a/src/ArrayValueConverter.php +++ b/src/ArrayValueConverter.php @@ -4,6 +4,9 @@ use Selective\Transformer\Exceptions\ArrayTransformerException; +/** + * Converter. + */ final class ArrayValueConverter { /** @@ -12,35 +15,40 @@ final class ArrayValueConverter private $filters = []; /** - * @param mixed $value - * @param ArrayTransformerRule $rule + * Convert the values by the given filter rules. * - * @return mixed + * @param mixed $value The source value + * @param ArrayTransformerRule $rule The rule + * + * @return mixed The new value */ public function convert($value, ArrayTransformerRule $rule) { if ($value === null) { return null; } - foreach ($rule->getFilters() as $filter) { + + foreach ($rule->getFiltersItems() as $filter) { $name = $filter->getName(); if (!isset($this->filters[$name])) { throw new ArrayTransformerException(sprintf('Filter not found: %s', $name)); } - $value = $this->invokeCallback($name, $value, $filter->getParams()); + $value = $this->invokeCallback($name, $value, $filter->getArguments()); } return $value; } /** - * @param string $name - * @param mixed $value - * @param array $parameters + * Invoke filter callback. + * + * @param string $name The filter name + * @param mixed $value The value for the filter + * @param array $parameters The filter arguments (optional) * - * @return mixed + * @return mixed The filter result */ private function invokeCallback(string $name, $value, array $parameters = []) { @@ -54,15 +62,13 @@ private function invokeCallback(string $name, $value, array $parameters = []) } /** - * @param string $name - * @param callable|string $callback + * Register a filter callback. + * + * @param string $name The filter name + * @param callable $callback The callback */ - public function registerFilter(string $name, $callback): void + public function registerFilter(string $name, callable $callback): void { - if (is_string($callback) && class_exists($callback)) { - $callback = new $callback(); - } - $this->filters[$name] = $callback; } } diff --git a/src/Filter/ArrayFilter.php b/src/Filter/ArrayFilter.php index 60e13b9..bb1fba3 100644 --- a/src/Filter/ArrayFilter.php +++ b/src/Filter/ArrayFilter.php @@ -12,7 +12,7 @@ final class ArrayFilter * * @param mixed $value The value * - * @return mixed The value + * @return array The value */ public function __invoke($value) { diff --git a/src/Filter/BlankToNullFilter.php b/src/Filter/BlankToNullFilter.php index bc5de2a..8717e5c 100644 --- a/src/Filter/BlankToNullFilter.php +++ b/src/Filter/BlankToNullFilter.php @@ -10,9 +10,9 @@ final class BlankToNullFilter /** * Invoke. * - * @param mixed $value + * @param mixed $value The value * - * @return mixed The value + * @return string|null The value */ public function __invoke($value) { diff --git a/src/Filter/BooleanFilter.php b/src/Filter/BooleanFilter.php index 5648757..05395dc 100644 --- a/src/Filter/BooleanFilter.php +++ b/src/Filter/BooleanFilter.php @@ -10,9 +10,9 @@ final class BooleanFilter /** * Invoke. * - * @param mixed $value + * @param mixed $value The value * - * @return mixed The value + * @return bool The value */ public function __invoke($value) { diff --git a/src/Filter/CallbackFilter.php b/src/Filter/CallbackFilter.php index 5c99396..193557a 100644 --- a/src/Filter/CallbackFilter.php +++ b/src/Filter/CallbackFilter.php @@ -11,7 +11,7 @@ final class CallbackFilter * Invoke. * * @param mixed $value The value - * @param callable $callback + * @param callable $callback The callback * * @return mixed The value */ diff --git a/src/Filter/DateTimeFilter.php b/src/Filter/DateTimeFilter.php index c297ca7..46cd40d 100644 --- a/src/Filter/DateTimeFilter.php +++ b/src/Filter/DateTimeFilter.php @@ -15,13 +15,13 @@ final class DateTimeFilter /** * Invoke. * - * @param mixed $value - * @param string|null $format - * @param DateTimeZone|null $timezone + * @param mixed $value The value + * @param string|null $format The date time format + * @param DateTimeZone|null $timezone The time zone * - * @throws Exception + * @throws ArrayTransformerException * - * @return mixed The value + * @return string The value */ public function __invoke($value, string $format = null, DateTimeZone $timezone = null) { @@ -29,20 +29,39 @@ public function __invoke($value, string $format = null, DateTimeZone $timezone = $format = $format ?? 'Y-m-d H:i:s'; if ($value instanceof DateTimeImmutable) { - if ($timezone) { - // This would only with only work with UTC as default time zone. - // https://3v4l.org/YlGWY - throw new ArrayTransformerException( - 'Changing the DateTimeZone of an existing DateTimeImmutable object is not supported.' - ); - } - - return (string)$value->format($format); + return $this->formatDateTime($value, $format, $timezone); } - return (new DateTimeImmutable($value, $timezone))->format($format); + return (string)(new DateTimeImmutable($value, $timezone))->format($format); } catch (Exception $exception) { throw new ArrayTransformerException($exception->getMessage(), $exception->getCode(), $exception); } } + + /** + * Format date time. + * + * @param DateTimeImmutable $value The date time + * @param string $format The format + * @param DateTimeZone|null $timezone The timezone + * + * @throws ArrayTransformerException + * + * @return string The result + */ + private function formatDateTime( + DateTimeImmutable $value, + string $format, + DateTimeZone $timezone = null + ): string { + if ($timezone) { + // This would only with only work with UTC as default time zone. + // https://3v4l.org/YlGWY + throw new ArrayTransformerException( + 'Changing the DateTimeZone of an existing DateTimeImmutable object is not supported.' + ); + } + + return (string)$value->format($format); + } } diff --git a/src/Filter/FloatFilter.php b/src/Filter/FloatFilter.php index 205dff0..78e0ccb 100644 --- a/src/Filter/FloatFilter.php +++ b/src/Filter/FloatFilter.php @@ -10,9 +10,9 @@ final class FloatFilter /** * Invoke. * - * @param mixed $value + * @param mixed $value The value * - * @return mixed The value + * @return float The value */ public function __invoke($value) { diff --git a/src/Filter/IntegerFilter.php b/src/Filter/IntegerFilter.php index 3b9c573..7b4c57e 100644 --- a/src/Filter/IntegerFilter.php +++ b/src/Filter/IntegerFilter.php @@ -10,9 +10,9 @@ final class IntegerFilter /** * Invoke. * - * @param mixed $value + * @param mixed $value The value * - * @return mixed The result + * @return int The result */ public function __invoke($value) { diff --git a/src/Filter/NumberFormatFilter.php b/src/Filter/NumberFormatFilter.php index ecf00bb..1bbca8c 100644 --- a/src/Filter/NumberFormatFilter.php +++ b/src/Filter/NumberFormatFilter.php @@ -11,11 +11,11 @@ final class NumberFormatFilter * Invoke. * * @param mixed $value The value - * @param int $decimals - * @param string $decimalSeparator - * @param string $thousandsSeparator + * @param int $decimals The decimals + * @param string $decimalSeparator The decimal separator + * @param string $thousandsSeparator The thousand separator * - * @return mixed The value + * @return string The value */ public function __invoke( $value, diff --git a/src/Filter/SprintfFilter.php b/src/Filter/SprintfFilter.php index 9233a98..bcd907f 100644 --- a/src/Filter/SprintfFilter.php +++ b/src/Filter/SprintfFilter.php @@ -13,7 +13,7 @@ final class SprintfFilter * @param mixed $value The value * @param string $format The format * - * @return mixed The value + * @return string The value */ public function __invoke($value, string $format) { diff --git a/src/Filter/StringFilter.php b/src/Filter/StringFilter.php index f1a8b8d..cfda094 100644 --- a/src/Filter/StringFilter.php +++ b/src/Filter/StringFilter.php @@ -12,7 +12,7 @@ final class StringFilter * * @param mixed $value The value * - * @return mixed The value + * @return string The value */ public function __invoke($value) { diff --git a/tests/ArrayTransformerTest.php b/tests/ArrayTransformerTest.php index 019a04d..16a5503 100644 --- a/tests/ArrayTransformerTest.php +++ b/tests/ArrayTransformerTest.php @@ -81,7 +81,7 @@ public function testComplexMapping(): void $transformer->registerFilter('trim', 'trim'); - $transformer->registerFilter('sprintf', SprintfFilter::class); + $transformer->registerFilter('sprintf', new SprintfFilter()); $transformer->registerFilter( 'custom1', @@ -120,7 +120,6 @@ function ($value) { ); $actual = $transformer->toArray($data); - //$actual = $transformer->toArrays($data); $expected = [ 'username' => 'admin', @@ -271,4 +270,110 @@ public function testUndefinedFilterException(): void ] ); } + + /** + * Test. + * + * @return void + */ + public function testToArrays(): void + { + $transformer = new ArrayTransformer(); + + $transformer->map('id', 'id', $transformer->rule()->integer()) + ->map('first_name', 'first_name', $transformer->rule()->string()) + ->map('last_name', 'last_name', $transformer->rule()->string()) + ->map('phone', 'phone', $transformer->rule()->string()) + ->map('enabled', 'enabled', $transformer->rule()->boolean()); + + $rows = []; + $rows[] = [ + 'id' => '100', + 'first_name' => 'Sally', + 'last_name' => '', + 'phone' => null, + 'enabled' => '1', + ]; + + $rows[] = [ + 'id' => '101', + 'first_name' => 'Max', + 'last_name' => 'Doe', + 'phone' => '+123456789', + 'enabled' => '0', + ]; + + $actual = $transformer->toArrays($rows); + + $this->assertSame( + [ + [ + 'id' => 100, + 'first_name' => 'Sally', + 'enabled' => true, + ], + [ + 'id' => 101, + 'first_name' => 'Max', + 'last_name' => 'Doe', + 'phone' => '+123456789', + 'enabled' => false, + ], + ], + $actual + ); + } + + /** + * Test. + * + * @return void + */ + public function testToArraysWithStrings(): void + { + $transformer = new ArrayTransformer(); + + $transformer->map('id', 'id', 'integer') + ->map('first_name', 'first_name', 'string') + ->map('last_name', 'last_name', 'blank-to-null') + ->map('phone', 'phone', 'string') + ->map('enabled', 'enabled', 'boolean'); + + $rows = []; + $rows[] = [ + 'id' => '100', + 'first_name' => 'Sally', + 'last_name' => '', + 'phone' => null, + 'enabled' => '1', + ]; + + $rows[] = [ + 'id' => '101', + 'first_name' => 'Max', + 'last_name' => 'Doe', + 'phone' => '+123456789', + 'enabled' => '0', + ]; + + $actual = $transformer->toArrays($rows); + + $this->assertSame( + [ + [ + 'id' => 100, + 'first_name' => 'Sally', + 'enabled' => true, + ], + [ + 'id' => 101, + 'first_name' => 'Max', + 'last_name' => 'Doe', + 'phone' => '+123456789', + 'enabled' => false, + ], + ], + $actual + ); + } }