diff --git a/composer.json b/composer.json
index 1ab80e9..b8fbcd5 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,6 @@
{
"name": "tiny-blocks/mapper",
"type": "library",
- "version": "1.0.4",
"license": "MIT",
"homepage": "https://github.com/tiny-blocks/mapper",
"description": "Allows mapping data between different formats, such as JSON, arrays, and DTOs, providing flexibility in transforming and serializing information.",
@@ -44,7 +43,7 @@
},
"require": {
"php": "^8.3",
- "tiny-blocks/collection": "1.9.0"
+ "tiny-blocks/collection": "^1"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
diff --git a/phpmd.xml b/phpmd.xml
index be1b346..44cb832 100644
--- a/phpmd.xml
+++ b/phpmd.xml
@@ -36,8 +36,12 @@
-
+
+
+
+
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index d081be5..e42932c 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -7,6 +7,7 @@ parameters:
- '#method#'
- '#expects#'
- '#should return#'
+ - '#type mixed supplied#'
- '#not specify its types#'
- '#no value type specified#'
reportUnmatchedIgnoredErrors: false
diff --git a/src/Internal/Mappers/Object/Casters/ArrayIteratorCaster.php b/src/Internal/Mappers/Object/Casters/ArrayIteratorCaster.php
new file mode 100644
index 0000000..1629c50
--- /dev/null
+++ b/src/Internal/Mappers/Object/Casters/ArrayIteratorCaster.php
@@ -0,0 +1,15 @@
+parameter->getType()->getName();
+
+ $caster = match (true) {
+ $class === Generator::class, => new GeneratorCaster(),
+ $class === ArrayIterator::class, => new ArrayIteratorCaster(),
+ is_subclass_of($class, UnitEnum::class) => new EnumCaster(class: $class),
+ is_subclass_of($class, Collectible::class) => new CollectionCaster(class: $class),
+ is_subclass_of($class, DateTimeInterface::class) => new DateTimeCaster(),
+ default => new DefaultCaster(class: $class)
+ };
+
+ return $caster->castValue(value: $value);
+ }
+}
diff --git a/src/Internal/Mappers/Object/Casters/CastingHandler.php b/src/Internal/Mappers/Object/Casters/CastingHandler.php
deleted file mode 100644
index 0efc27a..0000000
--- a/src/Internal/Mappers/Object/Casters/CastingHandler.php
+++ /dev/null
@@ -1,39 +0,0 @@
-class = $this->targetProperty->getType()->getName();
- }
-
- public function applyCast(): mixed
- {
- $caster = match (true) {
- $this->class === Generator::class, => new GeneratorCaster(),
- $this->class === ArrayIterator::class, => new ArrayIteratorCaster(),
- $this->class === DateTimeImmutable::class, => new DateTimeCaster(),
- is_subclass_of($this->class, UnitEnum::class) => new EnumCaster(class: $this->class),
- default => new DefaultCaster(property: $this->targetProperty)
- };
-
- return $caster->applyCast(value: $this->value);
- }
-}
diff --git a/src/Internal/Mappers/Object/Casters/CollectionCaster.php b/src/Internal/Mappers/Object/Casters/CollectionCaster.php
new file mode 100644
index 0000000..753d541
--- /dev/null
+++ b/src/Internal/Mappers/Object/Casters/CollectionCaster.php
@@ -0,0 +1,38 @@
+class);
+ /** @var IterableMapper & Collectible $instance */
+ $instance = $reflectionClass->newInstanceWithoutConstructor();
+
+ $type = $instance->getType();
+
+ if ($type === $this->class) {
+ return $instance->createFrom(elements: $value);
+ }
+
+ $mapped = [];
+
+ foreach ($value as $item) {
+ $mapped[] = (new ObjectMapper())->map(iterable: $item, class: $type);
+ }
+
+ return $instance->createFrom(elements: $mapped);
+ }
+}
diff --git a/src/Internal/Mappers/Object/Casters/DateTimeCaster.php b/src/Internal/Mappers/Object/Casters/DateTimeCaster.php
new file mode 100644
index 0000000..077d91e
--- /dev/null
+++ b/src/Internal/Mappers/Object/Casters/DateTimeCaster.php
@@ -0,0 +1,15 @@
+class)) {
+ return $value;
+ }
+
+ return (new ObjectMapper())->map(iterable: $value, class: $this->class);
+ }
+}
diff --git a/src/Internal/Mappers/Object/Casters/Types/EnumCaster.php b/src/Internal/Mappers/Object/Casters/EnumCaster.php
similarity index 74%
rename from src/Internal/Mappers/Object/Casters/Types/EnumCaster.php
rename to src/Internal/Mappers/Object/Casters/EnumCaster.php
index 23fb829..05c8606 100644
--- a/src/Internal/Mappers/Object/Casters/Types/EnumCaster.php
+++ b/src/Internal/Mappers/Object/Casters/EnumCaster.php
@@ -2,20 +2,19 @@
declare(strict_types=1);
-namespace TinyBlocks\Mapper\Internal\Mappers\Object\Casters\Types;
+namespace TinyBlocks\Mapper\Internal\Mappers\Object\Casters;
use ReflectionEnum;
use TinyBlocks\Mapper\Internal\Exceptions\InvalidCast;
-use TinyBlocks\Mapper\Internal\Mappers\Object\Casters\TypeCaster;
use UnitEnum;
-final readonly class EnumCaster implements TypeCaster
+final readonly class EnumCaster implements Caster
{
public function __construct(public string $class)
{
}
- public function applyCast(mixed $value): UnitEnum
+ public function castValue(mixed $value): UnitEnum
{
$reflectionEnum = new ReflectionEnum($this->class);
diff --git a/src/Internal/Mappers/Object/Casters/GeneratorCaster.php b/src/Internal/Mappers/Object/Casters/GeneratorCaster.php
new file mode 100644
index 0000000..6d0b7e2
--- /dev/null
+++ b/src/Internal/Mappers/Object/Casters/GeneratorCaster.php
@@ -0,0 +1,23 @@
+property->getDocComment();
- $objectMapper = new ObjectMapper();
-
- if ($comment !== false) {
- $mapped = [];
- $pattern = sprintf(self::DOCBLOCK_PROPERTY_TYPE_PATTERN, $this->property->getName());
-
- preg_match($pattern, $comment, $matches);
-
- if (isset($matches[self::DOCBLOCK_PROPERTY_TYPE])) {
- $class = rtrim($matches[self::DOCBLOCK_PROPERTY_TYPE], '[]');
-
- foreach ($value as $item) {
- $mapped[] = $objectMapper->map(iterable: $item, class: $class);
- }
- }
-
- return $mapped;
- }
-
- return $objectMapper->map(iterable: $value, class: $this->property->getType()->getName());
- }
-}
diff --git a/src/Internal/Mappers/Object/Casters/Types/GeneratorCaster.php b/src/Internal/Mappers/Object/Casters/Types/GeneratorCaster.php
deleted file mode 100644
index b3a347a..0000000
--- a/src/Internal/Mappers/Object/Casters/Types/GeneratorCaster.php
+++ /dev/null
@@ -1,24 +0,0 @@
-getProperties();
- $instance = $reflectionClass->newInstanceWithoutConstructor();
+ $reflectionClass = Reflector::reflectFrom(class: $class);
- $data = iterator_to_array($iterable);
+ $parameters = $reflectionClass->getParameters();
+ $inputProperties = iterator_to_array($iterable);
+ $constructorArguments = [];
- foreach ($properties as $property) {
- $value = $data[$property->getName()] ?? $data;
+ foreach ($parameters as $parameter) {
+ $name = $parameter->getName();
+ $value = $inputProperties[$name] ?? null;
- $caster = new CastingHandler(value: $value, targetProperty: $property);
- $castedValue = $caster->applyCast();
+ if ($value !== null) {
+ $caster = new CasterHandler(parameter: $parameter);
+ $castedValue = $caster->castValue(value: $value);
- $property->setValue($instance, $castedValue);
+ $constructorArguments[] = $castedValue;
+ continue;
+ }
+
+ $constructorArguments[] = $parameter->getDefaultValue();
}
- return $instance;
+ return $reflectionClass->newInstance(constructorArguments: $constructorArguments);
}
}
diff --git a/src/Internal/Mappers/Object/Reflector.php b/src/Internal/Mappers/Object/Reflector.php
new file mode 100644
index 0000000..1faec36
--- /dev/null
+++ b/src/Internal/Mappers/Object/Reflector.php
@@ -0,0 +1,49 @@
+constructor = $reflectionClass->getConstructor();
+ $this->parameters = $this->constructor ? $this->constructor->getParameters() : [];
+ }
+
+ public static function reflectFrom(string $class): Reflector
+ {
+ return new Reflector(reflectionClass: new ReflectionClass($class));
+ }
+
+ public function getParameters(): array
+ {
+ return $this->parameters;
+ }
+
+ public function newInstance(array $constructorArguments): mixed
+ {
+ $instance = $this->constructor && $this->constructor->isPrivate()
+ ? $this->newInstanceWithoutConstructor()
+ : $this->reflectionClass->newInstanceArgs($constructorArguments);
+
+ if ($this->constructor && $this->constructor->isPrivate()) {
+ $this->constructor->invokeArgs($instance, $constructorArguments);
+ }
+
+ return $instance;
+ }
+
+ public function newInstanceWithoutConstructor(): mixed
+ {
+ return $this->reflectionClass->newInstanceWithoutConstructor();
+ }
+}
diff --git a/src/IterableMappability.php b/src/IterableMappability.php
index a785629..cfbe7e9 100644
--- a/src/IterableMappability.php
+++ b/src/IterableMappability.php
@@ -22,4 +22,9 @@ public function toArray(KeyPreservation $keyPreservation = KeyPreservation::PRES
return $mapper->map(value: $this, keyPreservation: $keyPreservation);
}
+
+ public function getType(): string
+ {
+ return static::class;
+ }
}
diff --git a/src/IterableMapper.php b/src/IterableMapper.php
index 39bb6d4..3fd5161 100644
--- a/src/IterableMapper.php
+++ b/src/IterableMapper.php
@@ -9,4 +9,10 @@
*/
interface IterableMapper extends Mapper
{
+ /**
+ * Get the type of the iterable collection of objects.
+ *
+ * @return string The type of the objects in the collection.
+ */
+ public function getType(): string;
}
diff --git a/tests/IterableMapperTest.php b/tests/IterableMapperTest.php
index 1c44fff..1c379a2 100644
--- a/tests/IterableMapperTest.php
+++ b/tests/IterableMapperTest.php
@@ -190,7 +190,7 @@ public static function dataProviderForToJson(): iterable
],
'Shipping object with no addresses' => [
'elements' => [
- new Shipping(id: PHP_INT_MAX, addresses: new ShippingAddresses())
+ new Shipping(id: PHP_INT_MAX, addresses: ShippingAddresses::createFromEmpty())
],
'expected' => '[{"id":9223372036854775807,"addresses":[]}]'
],
@@ -198,7 +198,7 @@ public static function dataProviderForToJson(): iterable
'elements' => [
new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',
@@ -217,7 +217,7 @@ public static function dataProviderForToJson(): iterable
'elements' => [
new Shipping(
id: 100000,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'New York',
@@ -327,7 +327,7 @@ public static function dataProviderForToArray(): iterable
],
'Shipping object with no addresses' => [
'elements' => [
- new Shipping(id: PHP_INT_MAX, addresses: new ShippingAddresses())
+ new Shipping(id: PHP_INT_MAX, addresses: ShippingAddresses::createFromEmpty())
],
'expected' => [
[
@@ -340,7 +340,7 @@ public static function dataProviderForToArray(): iterable
'elements' => [
new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',
@@ -372,7 +372,7 @@ public static function dataProviderForToArray(): iterable
'elements' => [
new Shipping(
id: 100000,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'New York',
diff --git a/tests/Models/Collection.php b/tests/Models/Collection.php
index 53b85f9..3795180 100644
--- a/tests/Models/Collection.php
+++ b/tests/Models/Collection.php
@@ -6,6 +6,7 @@
use Closure;
use TinyBlocks\Collection\Collectible;
+use TinyBlocks\Collection\Order;
use TinyBlocks\Mapper\IterableMappability;
use TinyBlocks\Mapper\IterableMapper;
use Traversable;
@@ -126,10 +127,8 @@ public function reduce(Closure $aggregator, mixed $initial): mixed
// TODO: Implement reduce() method.
}
- public function sort(
- \TinyBlocks\Collection\Order $order = \TinyBlocks\Collection\Order::ASCENDING_KEY,
- ?Closure $predicate = null
- ): Collectible {
+ public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = null): Collectible
+ {
// TODO: Implement sort() method.
}
diff --git a/tests/Models/Customer.php b/tests/Models/Customer.php
new file mode 100644
index 0000000..b4b5203
--- /dev/null
+++ b/tests/Models/Customer.php
@@ -0,0 +1,17 @@
+elements = is_array($elements) ? $elements : iterator_to_array($elements);
- }
-
- public function getIterator(): Traversable
+ public function getType(): string
{
- return new ArrayIterator($this->elements);
+ return ShippingAddress::class;
}
}
diff --git a/tests/Models/Stores.php b/tests/Models/Stores.php
new file mode 100644
index 0000000..f2a38f8
--- /dev/null
+++ b/tests/Models/Stores.php
@@ -0,0 +1,139 @@
+iterator = $iterator;
+ }
+
+ public static function createFrom(iterable $elements): Stores
+ {
+ return new Stores(iterator: $elements);
+ }
+
+ public static function createFromEmpty(): Collectible
+ {
+ // TODO: Implement createFromEmpty() method.
+ }
+
+ public function add(...$elements): Collectible
+ {
+ // TODO: Implement add() method.
+ }
+
+ public function contains(mixed $element): bool
+ {
+ // TODO: Implement contains() method.
+ }
+
+ public function count(): int
+ {
+ return iterator_count($this->iterator);
+ }
+
+ public function each(Closure ...$actions): Collectible
+ {
+ // TODO: Implement each() method.
+ }
+
+ public function equals(Collectible $other): bool
+ {
+ // TODO: Implement equals() method.
+ }
+
+ public function filter(?Closure ...$predicates): Collectible
+ {
+ // TODO: Implement filter() method.
+ }
+
+ public function findBy(Closure ...$predicates): mixed
+ {
+ // TODO: Implement findBy() method.
+ }
+
+ public function first(mixed $defaultValueIfNotFound = null): mixed
+ {
+ // TODO: Implement first() method.
+ }
+
+ public function flatten(): Collectible
+ {
+ // TODO: Implement flatten() method.
+ }
+
+ public function getBy(int $index, mixed $defaultValueIfNotFound = null): mixed
+ {
+ // TODO: Implement getBy() method.
+ }
+
+ public function getIterator(): Traversable
+ {
+ yield from $this->iterator;
+ }
+
+ public function groupBy(Closure $grouping): Collectible
+ {
+ // TODO: Implement groupBy() method.
+ }
+
+ public function isEmpty(): bool
+ {
+ // TODO: Implement isEmpty() method.
+ }
+
+ public function joinToString(string $separator): string
+ {
+ // TODO: Implement joinToString() method.
+ }
+
+ public function last(mixed $defaultValueIfNotFound = null): mixed
+ {
+ // TODO: Implement last() method.
+ }
+
+ public function map(Closure ...$transformations): Collectible
+ {
+ // TODO: Implement map() method.
+ }
+
+ public function remove(mixed $element): Collectible
+ {
+ // TODO: Implement remove() method.
+ }
+
+ public function removeAll(?Closure $filter = null): Collectible
+ {
+ // TODO: Implement removeAll() method.
+ }
+
+ public function reduce(Closure $aggregator, mixed $initial): mixed
+ {
+ // TODO: Implement reduce() method.
+ }
+
+ public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = null): Collectible
+ {
+ // TODO: Implement sort() method.
+ }
+
+ public function slice(int $index, int $length = -1): Collectible
+ {
+ // TODO: Implement slice() method.
+ }
+}
diff --git a/tests/ObjectMapperTest.php b/tests/ObjectMapperTest.php
index 82c7759..d64498f 100644
--- a/tests/ObjectMapperTest.php
+++ b/tests/ObjectMapperTest.php
@@ -14,11 +14,13 @@
use TinyBlocks\Mapper\Models\Amount;
use TinyBlocks\Mapper\Models\Configuration;
use TinyBlocks\Mapper\Models\Currency;
+use TinyBlocks\Mapper\Models\Customer;
use TinyBlocks\Mapper\Models\Decimal;
use TinyBlocks\Mapper\Models\Dragon;
use TinyBlocks\Mapper\Models\DragonSkills;
use TinyBlocks\Mapper\Models\DragonType;
use TinyBlocks\Mapper\Models\ExpirationDate;
+use TinyBlocks\Mapper\Models\Merchant;
use TinyBlocks\Mapper\Models\Order;
use TinyBlocks\Mapper\Models\Product;
use TinyBlocks\Mapper\Models\Service;
@@ -27,6 +29,7 @@
use TinyBlocks\Mapper\Models\ShippingAddresses;
use TinyBlocks\Mapper\Models\ShippingCountry;
use TinyBlocks\Mapper\Models\ShippingState;
+use TinyBlocks\Mapper\Models\Stores;
final class ObjectMapperTest extends TestCase
{
@@ -87,6 +90,20 @@ public function testIterableToObject(iterable $iterable, ObjectMapper $expected)
self::assertNotSame($expected, $actual);
}
+ public function testIterableToObjectWithDefaultParameterValue(): void
+ {
+ /** @Given an iterable with values */
+ $iterable = ['name' => 'Zephyrax the Tempest'];
+
+ /** @When converting the iterable to an object */
+ $actual = Customer::fromIterable(iterable: $iterable);
+
+ /** @Then the result should have the default value for the missing parameter */
+ self::assertSame('Zephyrax the Tempest', $actual->name);
+ self::assertSame(0, $actual->score);
+ self::assertNull($actual->gender);
+ }
+
public function testInvalidObjectValueToArrayReturnsEmptyArray(): void
{
/** @Given an object with an invalid item (e.g., a function that cannot be serialized) */
@@ -161,13 +178,13 @@ public static function dataProviderForToJson(): iterable
'expected' => '{"value":"2000-01-01 00:00:00"}'
],
'Shipping object with no addresses' => [
- 'object' => new Shipping(id: PHP_INT_MAX, addresses: new ShippingAddresses()),
+ 'object' => new Shipping(id: PHP_INT_MAX, addresses: ShippingAddresses::createFromEmpty()),
'expected' => '{"id": 9223372036854775807,"addresses":[]}'
],
'Shipping object with a single address' => [
'object' => new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',
@@ -184,7 +201,7 @@ public static function dataProviderForToJson(): iterable
'Shipping object with multiple addresses' => [
'object' => new Shipping(
id: 100000,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'New York',
@@ -270,7 +287,7 @@ public static function dataProviderForToArray(): iterable
'expected' => ['value' => '2000-01-01 00:00:00']
],
'Shipping object with no addresses' => [
- 'object' => new Shipping(id: PHP_INT_MAX, addresses: new ShippingAddresses()),
+ 'object' => new Shipping(id: PHP_INT_MAX, addresses: ShippingAddresses::createFromEmpty()),
'expected' => [
'id' => PHP_INT_MAX,
'addresses' => []
@@ -279,7 +296,7 @@ public static function dataProviderForToArray(): iterable
'Shipping object with a single address' => [
'object' => new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',
@@ -307,7 +324,7 @@ public static function dataProviderForToArray(): iterable
'Shipping object with multiple addresses' => [
'object' => new Shipping(
id: 100000,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'New York',
@@ -391,6 +408,26 @@ public static function dataProviderForIterableToObject(): iterable
stockBatch: new ArrayIterator([1000, 2000, 3000])
)
],
+ 'Customer object' => [
+ 'iterable' => ['name' => 'Zephyrax the Tempest'],
+ 'expected' => new Customer(name: 'Zephyrax the Tempest', score: 0, gender: null)
+ ],
+ 'Merchant object' => [
+ 'iterable' => [
+ 'id' => '1dc6ca7a-e5f9-4c04-8fdf-16630c3009e3',
+ 'stores' => [
+ ['name' => 'Store A'],
+ ['name' => 'Store B']
+ ]
+ ],
+ 'expected' => new Merchant(
+ id: '1dc6ca7a-e5f9-4c04-8fdf-16630c3009e3',
+ stores: Stores::createFrom(elements: [
+ ['name' => 'Store A'],
+ ['name' => 'Store B']
+ ])
+ )
+ ],
'Configuration object' => [
'iterable' => [
'id' => PHP_INT_MAX,
@@ -408,7 +445,7 @@ public static function dataProviderForIterableToObject(): iterable
],
'Shipping object with no addresses' => [
'iterable' => ['id' => PHP_INT_MAX, 'addresses' => []],
- 'expected' => new Shipping(id: PHP_INT_MAX, addresses: new ShippingAddresses())
+ 'expected' => new Shipping(id: PHP_INT_MAX, addresses: ShippingAddresses::createFromEmpty())
],
'Shipping object with a single address' => [
'iterable' => [
@@ -425,7 +462,7 @@ public static function dataProviderForIterableToObject(): iterable
],
'expected' => new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',
@@ -460,7 +497,7 @@ public static function dataProviderForIterableToObject(): iterable
],
'expected' => new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'New York',
@@ -493,7 +530,7 @@ public static function dataProviderForToJsonDiscardKeys(): iterable
'Shipping object with a single address' => [
'object' => new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',
@@ -520,7 +557,7 @@ public static function dataProviderForToArrayDiscardKeys(): iterable
'Shipping object with a single address' => [
'object' => new Shipping(
id: PHP_INT_MIN,
- addresses: new ShippingAddresses(
+ addresses: ShippingAddresses::createFrom(
elements: [
new ShippingAddress(
city: 'São Paulo',