From 9c063cd63a92a4f3936a20ec3cb92d0a2445225c Mon Sep 17 00:00:00 2001
From: Daniel Opitz <d.opitz@outlook.com>
Date: Tue, 26 Jan 2021 19:55:42 +0100
Subject: [PATCH] Init

---
 README.md                          |  88 ++++++++++++++++--
 src/ArrayTransformer.php           |  93 ++++++++++++++-----
 src/ArrayTransformerFilter.php     |  44 ---------
 src/ArrayTransformerFilterItem.php |  51 +++++++++++
 src/ArrayTransformerRule.php       | 138 ++++++++++++++++++++++++-----
 src/ArrayValueConverter.php        |  38 ++++----
 src/Filter/ArrayFilter.php         |   2 +-
 src/Filter/BlankToNullFilter.php   |   4 +-
 src/Filter/BooleanFilter.php       |   4 +-
 src/Filter/CallbackFilter.php      |   2 +-
 src/Filter/DateTimeFilter.php      |  49 ++++++----
 src/Filter/FloatFilter.php         |   4 +-
 src/Filter/IntegerFilter.php       |   4 +-
 src/Filter/NumberFormatFilter.php  |   8 +-
 src/Filter/SprintfFilter.php       |   2 +-
 src/Filter/StringFilter.php        |   2 +-
 tests/ArrayTransformerTest.php     | 109 ++++++++++++++++++++++-
 17 files changed, 499 insertions(+), 143 deletions(-)
 delete mode 100644 src/ArrayTransformerFilter.php
 create mode 100644 src/ArrayTransformerFilterItem.php

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
 <?php
 
@@ -57,10 +72,65 @@ The result:
 ];
 ```
 
-### Dot access
+### Transforming list of arrays
+
+You can use a method "toArrays" to transform a list of arrays.
+
+This can be useful if you want to transform a resultset from a database query, or a response payload from an API.
+
+**Example:**
+
+```php
+$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',
+];
+
+$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
 <?php
@@ -70,6 +140,8 @@ $transformer->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<mixed> $source
-     * @param array<mixed> $target
+     * Transform array to array.
+     *
+     * @param array<mixed> $source The source
+     * @param array<mixed> $target The target (optional)
      *
-     * @return array<mixed>
+     * @return array<mixed> 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<mixed> $source The source
+     * @param array<mixed> $target The target (optional)
+     *
+     * @return array<mixed> 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 @@
-<?php
-
-namespace Selective\Transformer;
-
-final class ArrayTransformerFilter
-{
-    /**
-     * @var string
-     */
-    private $name;
-
-    /**
-     * @var array<mixed>
-     */
-    private $params;
-
-    /**
-     * The constructor.
-     *
-     * @param string $name
-     * @param array<mixed> $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<mixed>
-     */
-    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 @@
+<?php
+
+namespace Selective\Transformer;
+
+/**
+ * Filter.
+ */
+final class ArrayTransformerFilterItem
+{
+    /**
+     * @var string
+     */
+    private $name;
+
+    /**
+     * @var array<mixed>
+     */
+    private $arguments;
+
+    /**
+     * The constructor.
+     *
+     * @param string $name The filter to apply
+     * @param array<mixed> $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<mixed> 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<mixed> $parameters
+     * Invoke filter callback.
+     *
+     * @param string $name The filter name
+     * @param mixed $value The value for the filter
+     * @param array<mixed> $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<mixed> 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
+        );
+    }
 }