Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.82% covered (warning)
87.82%
137 / 156
38.46% covered (danger)
38.46%
5 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Utils
87.82% covered (warning)
87.82%
137 / 156
38.46% covered (danger)
38.46%
5 / 13
96.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 toArray
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 toString
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
9.06
 toInt
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 toFloat
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
4.54
 toBoolean
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 toMixed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 toDate
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 toNative
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
13.15
 toEnum
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 toTypehint
85.71% covered (warning)
85.71%
30 / 35
0.00% covered (danger)
0.00%
0 / 1
22.29
 sortTypes
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 displayMixedAsString
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 getDisplayNameForValueObject
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2namespace Apie\Core\ValueObjects;
3
4use Apie\Core\Attributes\ResourceName;
5use Apie\Core\Exceptions\InvalidTypeException;
6use Apie\Core\Lists\ItemHashmap;
7use Apie\Core\Lists\ItemList;
8use Apie\Core\ValueObjects\Interfaces\TimeRelatedValueObjectInterface;
9use Apie\Core\ValueObjects\Interfaces\ValueObjectInterface;
10use BackedEnum;
11use DateTime;
12use DateTimeImmutable;
13use DateTimeInterface;
14use ReflectionClass;
15use ReflectionNamedType;
16use ReflectionUnionType;
17use stdClass;
18use Stringable;
19use UnitEnum;
20
21/**
22 * Util classes used by or from value objects.
23 */
24final class Utils
25{
26    /**
27     * @codeCoverageIgnore
28     */
29    private function __construct()
30    {
31    }
32
33    /**
34     * @return mixed[]
35     */
36    public static function toArray(mixed $input): array
37    {
38        if (is_array($input)) {
39            return $input;
40        }
41        if ($input instanceof ItemList || $input instanceof ItemHashmap) {
42            return $input->toArray();
43        }
44        if (is_iterable($input)) {
45            return iterator_to_array($input);
46        }
47        throw new InvalidTypeException($input, 'array');
48    }
49
50    public static function toString(mixed $input): string
51    {
52        if ($input instanceof ValueObjectInterface) {
53            $input = $input instanceof Stringable ? $input->__toString() : $input->toNative();
54        }
55        if (is_array($input)) {
56            throw new InvalidTypeException($input, 'string');
57        }
58        if (! $input instanceof Stringable && $input instanceof BackedEnum) {
59            return (string) $input->value;
60        }
61        if (! $input instanceof Stringable && $input instanceof UnitEnum) {
62            return (string) $input->name;
63        }
64        if ($input instanceof DateTimeInterface) {
65            return $input->format(DateTimeInterface::ATOM);
66        }
67        return (string) $input;
68    }
69
70    public static function toInt(mixed $input): int
71    {
72        if ($input instanceof ValueObjectInterface) {
73            $input = $input->toNative();
74        }
75        if (is_array($input)) {
76            throw new InvalidTypeException($input, 'int');
77        }
78        $iInput = (int) $input;
79        $sInput = (string) $input;
80        if ($sInput !== ((string) $iInput)) {
81            throw new InvalidTypeException(
82                $input,
83                'int'
84            );
85        }
86        return (int) $input;
87    }
88
89    public static function toFloat(mixed $input): float
90    {
91        if ($input instanceof ValueObjectInterface) {
92            $input = $input->toNative();
93        }
94        $inputString = trim(self::toString($input));
95        if (!preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/', $inputString)) {
96            throw new InvalidTypeException(
97                $inputString,
98                'float'
99            );
100        }
101        return (float) $input;
102    }
103
104    public static function toBoolean(mixed $input): bool
105    {
106        if ($input instanceof ValueObjectInterface) {
107            $input = $input->toNative();
108        }
109        return (bool) $input;
110    }
111    
112    public static function toMixed(mixed $input): mixed
113    {
114        return $input instanceof ValueObjectInterface ? $input->toNative() : $input;
115    }
116
117    public static function toDate(mixed $input, string $class = DateTimeImmutable::class): DateTimeInterface
118    {
119        if ($class === DateTimeInterface::class) {
120            $class = DateTimeImmutable::class;
121        }
122        if ($input instanceof DateTimeInterface) {
123            return $class::createFromInterface($input);
124        }
125        if ($input instanceof TimeRelatedValueObjectInterface) {
126            return $input->toDate();
127        }
128        $result = $class::createFromFormat(DateTime::ATOM, self::toString($input));
129        if (!$result) {
130            throw new InvalidTypeException($input, $class);
131        }
132        return $result;
133    }
134
135    /**
136     * @return mixed[]|string|int|float|bool|UnitEnum|null
137     */
138    public static function toNative(mixed $input): array|string|int|float|bool|UnitEnum|null
139    {
140        if ($input instanceof ValueObjectInterface) {
141            $input = $input->toNative();
142        }
143        if ($input instanceof stdClass) {
144            $input = json_decode(json_encode($input), true);
145        }
146        if (is_iterable($input)) {
147            $result = [];
148            foreach ($input as $key => $value) {
149                $result[$key] = self::toNative($value);
150            }
151            return $result;
152        }
153        if ($input instanceof UnitEnum) {
154            return $input;
155        }
156        if (is_object($input) && $input instanceof Stringable) {
157            return (string) $input;
158        }
159        if (is_bool($input)) {
160            return $input;
161        }
162        if (is_string($input) || is_numeric($input)) {
163            return $input;
164        }
165        if (null === $input) {
166            return null;
167        }
168        throw new InvalidTypeException($input, 'ValueObject|array|string|int|float|bool|UnitEnum|null');
169    }
170
171    public static function toEnum(string $className, mixed $input): UnitEnum
172    {
173        if ($input instanceof $className) {
174            return $input;
175        }
176
177        $input = self::toString($input);
178        foreach ($className::cases() as $enum) {
179            if ($enum->value === $input || $enum->name === $input) {
180                return $enum;
181            }
182        }
183        throw new InvalidTypeException($input, self::getDisplayNameForValueObject(new ReflectionClass($className)));
184    }
185
186    /**
187     * Converts native value in typehint.
188     */
189    public static function toTypehint(
190        ReflectionUnionType|ReflectionNamedType $typehint,
191        mixed $input
192    ): mixed {
193        if ($input === null) {
194            if ($typehint->allowsNull()) {
195                return null;
196            }
197            throw InvalidTypeException::fromTypehint($input, $typehint);
198        }
199        $types = $typehint instanceof ReflectionUnionType ? self::sortTypes($input, ...$typehint->getTypes()) : [$typehint];
200        $lastError = new InvalidTypeException($input, '(unknown)');
201        foreach ($types as $type) {
202            try {
203                if ($type->isBuiltin()) {
204                    switch ($type->getName()) {
205                        case 'string':
206                            return self::toString($input);
207                        case 'int':
208                            return self::toInt($input);
209                        case 'float':
210                            return self::toFloat($input);
211                        case 'array':
212                        case 'iterable':
213                            return self::toArray($input);
214                        case 'bool':
215                            return self::toBoolean($input);
216                        case 'mixed':
217                            return self::toMixed($input);
218                        default:
219                            throw new InvalidTypeException($input, $type->getName());
220                    }
221                }
222                $className = $type->getName();
223                switch ($className) {
224                    case stdClass::class:
225                        return json_decode(json_encode(self::toArray($input)), false);
226                    case DateTimeInterface::class:
227                    case DateTimeImmutable::class:
228                    case DateTime::class:
229                        return self::toDate($input, $className);
230                }
231                $refl = new ReflectionClass($className);
232                if ($refl->implementsInterface(ValueObjectInterface::class)) {
233                    return $className::fromNative($input);
234                }
235                if ($refl->implementsInterface(UnitEnum::class)) {
236                    return self::toEnum($className, $input);
237                }
238                throw new InvalidTypeException($className, 'ValueObjectInterface');
239            } catch (InvalidTypeException $error) {
240                $lastError = $error;
241            }
242        }
243        throw InvalidTypeException::chainException($lastError);
244    }
245
246    /**
247     * Sort typehints in a specific order.
248     *
249     * @return array<int, ReflectionNamedType>
250     */
251    public static function sortTypes(mixed $input, ReflectionNamedType... $types): array
252    {
253        $prio = [
254            'string' => gettype($input) === 'string' ?  -3 : 1,
255            'int' => 0,
256            'float' => -1,
257        ];
258        $callback = function (ReflectionNamedType $type1, ReflectionNamedType $type2) use (&$prio) : int {
259            $name1 = $type1->getName();
260            $name2 = $type2->getName();
261            $prio1 = $prio[$name1] ?? -2;
262            $prio2 = $prio[$name2] ?? -2;
263            return $prio1 <=> $prio2;
264        };
265
266        usort($types, $callback);
267        return $types;
268    }
269
270    public static function displayMixedAsString(mixed $input): string
271    {
272        if ($input === null) {
273            return '(null)';
274        }
275        if (is_object($input)) {
276            if ($input instanceof Stringable) {
277                return '(object "' . $input->__toString() . '")';
278            }
279            if ($input instanceof DateTimeInterface) {
280                return $input->format(DateTime::ATOM);
281            }
282            return '(object ' . self::getDisplayNameForValueObject(new ReflectionClass($input)) . ')';
283        }
284
285        if (is_bool($input)) {
286            return json_encode($input);
287        }
288
289        if (is_string($input) || is_numeric($input)) {
290            return (string) '"' . $input . '"';
291        }
292
293        return gettype($input);
294    }
295
296    /**
297     * @param ValueObjectInterface|ReflectionClass<object> $class
298     */
299    public static function getDisplayNameForValueObject(ValueObjectInterface|ReflectionClass $class): string
300    {
301        if ($class instanceof ReflectionClass) {
302            $className = $class->getShortName();
303            $refl = $class;
304        } else {
305            $refl = new ReflectionClass($class);
306            $className = $refl->getShortName();
307        }
308        foreach ($refl->getAttributes(ResourceName::class) as $attribute) {
309            return $attribute->newInstance()->name;
310        }
311        if (strcasecmp($className, 'Abstract') === 0 || strcasecmp($className, 'AbstractInterface') === 0) {
312            return 'Abstract';
313        }
314        return preg_replace(
315            '/Interface/i',
316            '',
317            preg_replace('/^abstract/i', '', $className)
318        );
319    }
320}