Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ permissions:

env:
PHP_VERSION: '8.5'
COMPOSER_ROOT_VERSION: '1.2.0'

jobs:
build:
Expand All @@ -23,7 +22,6 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: bcmath
tools: composer:2

- name: Validate composer.json
Expand Down Expand Up @@ -53,7 +51,6 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: bcmath
tools: composer:2

- name: Download vendor artifact from build
Expand All @@ -78,7 +75,6 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: bcmath
tools: composer:2

- name: Download vendor artifact from build
Expand Down
10 changes: 2 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@
"issues": "https://github.com/tiny-blocks/mapper/issues",
"source": "https://github.com/tiny-blocks/mapper"
},
"extra": {
"branch-alias": {
"dev-develop": "1.3.x-dev"
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
Expand All @@ -43,7 +38,7 @@
},
"autoload-dev": {
"psr-4": {
"TinyBlocks\\Mapper\\": "tests/"
"Test\\TinyBlocks\\Mapper\\": "tests/"
}
},
"require": {
Expand All @@ -53,8 +48,7 @@
"phpunit/phpunit": "^11.5",
"phpstan/phpstan": "^2.1",
"infection/infection": "^0.32",
"tiny-blocks/collection": "1.10.*",
"squizlabs/php_codesniffer": "^3.13"
"squizlabs/php_codesniffer": "^4.0"
},
"scripts": {
"test": "php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests",
Expand Down
12 changes: 6 additions & 6 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ parameters:
level: 9
tmpDir: report/phpstan
ignoreErrors:
- '#method#'
- '#expects#'
- '#should return#'
- '#T of#'
- '#mixed#'
- '#UnitEnum#'
- '#Reflection#'
- '#Traversable#'
- '#is used zero times#'
- '#type mixed supplied#'
- '#not specify its types#'
- '#no value type specified#'
- '#type specified in iterable type#'
reportUnmatchedIgnoredErrors: false
98 changes: 98 additions & 0 deletions src/Internal/Builders/ObjectBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Builders;

use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionParameter;
use TinyBlocks\Mapper\Internal\Extractors\ReflectionExtractor;
use TinyBlocks\Mapper\Internal\Mappers\Object\Casters\CasterHandler;

final readonly class ObjectBuilder
{
public function __construct(private ReflectionExtractor $extractor)
{
}

/**
* @template T of object
* @param class-string<T> $class
* @return T
* @throws ReflectionException
*/
public function build(iterable $iterable, string $class): object
{
$reflection = new ReflectionClass(objectOrClass: $class);
$parameters = $this->extractor->extractConstructorParameters(class: $class);
$inputProperties = iterator_to_array(iterator: $iterable);

$arguments = $this->buildArguments(
parameters: $parameters,
inputProperties: $inputProperties
);

return $this->instantiate(reflection: $reflection, arguments: $arguments);
}

protected function buildArguments(array $parameters, array $inputProperties): array
{
$arguments = [];

/** @var ReflectionParameter $parameter */
foreach ($parameters as $parameter) {
$name = $parameter->getName();
$value = $inputProperties[$name] ?? null;

$arguments[] = $value !== null
? $this->castValue(parameter: $parameter, value: $value)
: $this->getDefaultValue(parameter: $parameter);
}

return $arguments;
}

protected function castValue(ReflectionParameter $parameter, mixed $value): mixed
{
$caster = new CasterHandler(parameter: $parameter);
return $caster->castValue(value: $value);
}

protected function getDefaultValue(ReflectionParameter $parameter): mixed
{
return $parameter->isDefaultValueAvailable()
? $parameter->getDefaultValue()
: null;
}

protected function instantiate(ReflectionClass $reflection, array $arguments): object
{
$constructor = $reflection->getConstructor();

if ($constructor === null) {
return $reflection->newInstance();
}

if ($constructor->isPrivate()) {
return $this->instantiateWithPrivateConstructor(
reflection: $reflection,
constructor: $constructor,
arguments: $arguments
);
}

return $reflection->newInstanceArgs(args: $arguments);
}

protected function instantiateWithPrivateConstructor(
ReflectionClass $reflection,
ReflectionMethod $constructor,
array $arguments
): object {
$instance = $reflection->newInstanceWithoutConstructor();
$constructor->invokeArgs(object: $instance, args: $arguments);
return $instance;
}
}
15 changes: 15 additions & 0 deletions src/Internal/Detectors/DateTimeDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Detectors;

use DateTimeInterface;

final readonly class DateTimeDetector implements TypeDetector
{
public function matches(mixed $value): bool
{
return $value instanceof DateTimeInterface;
}
}
15 changes: 15 additions & 0 deletions src/Internal/Detectors/EnumDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Detectors;

use UnitEnum;

final readonly class EnumDetector implements TypeDetector
{
public function matches(mixed $value): bool
{
return $value instanceof UnitEnum;
}
}
19 changes: 19 additions & 0 deletions src/Internal/Detectors/TypeDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Detectors;

/**
* Defines the contract for type detection strategies.
*/
interface TypeDetector
{
/**
* Determines if the given value matches the detector's type criteria.
*
* @param mixed $value The value to detect.
* @return bool True if the value matches, false otherwise.
*/
public function matches(mixed $value): bool;
}
26 changes: 26 additions & 0 deletions src/Internal/Detectors/ValueObjectDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Detectors;

use ReflectionClass;
use ReflectionProperty;
use UnitEnum;

final readonly class ValueObjectDetector implements TypeDetector
{
private const int SINGLE_PROPERTY = 1;

public function matches(mixed $value): bool
{
$reflection = new ReflectionClass($value);
$properties = $reflection->getProperties(
ReflectionProperty::IS_PUBLIC
| ReflectionProperty::IS_PROTECTED
| ReflectionProperty::IS_PRIVATE
);

return !$value instanceof UnitEnum && count($properties) === self::SINGLE_PROPERTY;
}
}
1 change: 0 additions & 1 deletion src/Internal/Exceptions/InvalidCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ final class InvalidCast extends InvalidArgumentException
public static function forEnumValue(int|string $value, string $class): InvalidCast
{
$message = sprintf('Invalid value <%s> for enum <%s>.', $value, $class);

return new InvalidCast(message: $message);
}
}
28 changes: 28 additions & 0 deletions src/Internal/Extractors/IterableExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Extractors;

use Traversable;

final readonly class IterableExtractor implements PropertyExtractor
{
public function __construct(private ReflectionExtractor $extractor)
{
}

public function extract(object $object): array
{
$properties = $this->extractor->extractProperties(object: $object);

$candidates = array_filter(
$properties,
static fn(mixed $value): bool => is_array($value) || $value instanceof Traversable
);

$iterable = reset($candidates);

return is_array($iterable) ? $iterable : iterator_to_array($iterable);
}
}
19 changes: 19 additions & 0 deletions src/Internal/Extractors/PropertyExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Extractors;

/**
* Defines the contract for extracting values from objects.
*/
interface PropertyExtractor
{
/**
* Extracts a value from an object.
*
* @param object $object The object to extract from.
* @return mixed The extracted value.
*/
public function extract(object $object): mixed;
}
46 changes: 46 additions & 0 deletions src/Internal/Extractors/ReflectionExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Extractors;

use ReflectionClass;
use ReflectionProperty;

final readonly class ReflectionExtractor
{
public function extractProperties(object $object): array
{
$reflection = new ReflectionClass(objectOrClass: $object);
$properties = $reflection->getProperties(
ReflectionProperty::IS_PUBLIC
| ReflectionProperty::IS_PROTECTED
| ReflectionProperty::IS_PRIVATE
);

$extracted = [];

foreach ($properties as $property) {
if ($property->isStatic()) {
continue;
}

$name = $property->getName();
$extracted[$name] = $property->getValue(object: $object);
}

return $extracted;
}

public function extractConstructorParameters(string $class): array
{
$reflection = new ReflectionClass(objectOrClass: $class);
$constructor = $reflection->getConstructor();

if ($constructor === null) {
return [];
}

return $constructor->getParameters();
}
}
15 changes: 15 additions & 0 deletions src/Internal/Extractors/ValuePropertyExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Mapper\Internal\Extractors;

use ReflectionClass;

final readonly class ValuePropertyExtractor implements PropertyExtractor
{
public function extract(object $object): mixed
{
return new ReflectionClass($object)->getProperties()[0]->getValue($object);
}
}
Loading