Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.22% covered (warning)
89.22%
149 / 167
38.46% covered (danger)
38.46%
5 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Utils
89.22% covered (warning)
89.22%
149 / 167
38.46% covered (danger)
38.46%
5 / 13
97.70
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
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 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
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
12.49
 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%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 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 ($input instanceof ReflectionClass) {
56            return $input->name;
57        }
58        if (is_array($input)) {
59            throw new InvalidTypeException($input, 'string');
60        }
61        if (! $input instanceof Stringable && $input instanceof BackedEnum) {
62            return (string) $input->value;
63        }
64        if (! $input instanceof Stringable && $input instanceof UnitEnum) {
65            return (string) $input->name;
66        }
67        if ($input instanceof DateTimeInterface) {
68            return $input->format(DateTimeInterface::ATOM);
69        }
70        if (is_resource($input)) {
71            $offset = ftell($input);
72            @rewind($input);
73            $result = stream_get_contents($input);
74            @fseek($input, $offset);
75            if ($result !== false) {
76                return $result;
77            }
78        }
79        return (string) $input;
80    }
81
82    public static function toInt(mixed $input): int
83    {
84        if ($input instanceof ValueObjectInterface) {
85            $input = $input->toNative();
86        }
87        if (is_array($input)) {
88            throw new InvalidTypeException($input, 'int');
89        }
90        $iInput = (int) $input;
91        $sInput = (string) $input;
92        if ($sInput !== ((string) $iInput)) {
93            throw new InvalidTypeException(
94                $input,
95                'int'
96            );
97        }
98        return (int) $input;
99    }
100
101    public static function toFloat(mixed $input): float
102    {
103        if ($input instanceof ValueObjectInterface) {
104            $input = $input->toNative();
105        }
106        $inputString = trim(self::toString($input));
107        if (!preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/', $inputString)) {
108            throw new InvalidTypeException(
109                $inputString,
110                'float'
111            );
112        }
113        return (float) $input;
114    }
115
116    public static function toBoolean(mixed $input): bool
117    {
118        if ($input instanceof ValueObjectInterface) {
119            $input = $input->toNative();
120        }
121        return (bool) $input;
122    }
123    
124    public static function toMixed(mixed $input): mixed
125    {
126        return $input instanceof ValueObjectInterface ? $input->toNative() : $input;
127    }
128
129    public static function toDate(mixed $input, string $class = DateTimeImmutable::class): DateTimeInterface
130    {
131        if ($class === DateTimeInterface::class) {
132            $class = DateTimeImmutable::class;
133        }
134        if ($input instanceof DateTimeInterface) {
135            return $class::createFromInterface($input);
136        }
137        if ($input instanceof TimeRelatedValueObjectInterface) {
138            return $input->toDate();
139        }
140        $result = $class::createFromFormat(DateTime::ATOM, self::toString($input));
141        if (!$result) {
142            throw new InvalidTypeException($input, $class);
143        }
144        return $result;
145    }
146
147    /**
148     * @return mixed[]|string|int|float|bool|UnitEnum|null
149     */
150    public static function toNative(mixed $input): array|string|int|float|bool|UnitEnum|null
151    {
152        if ($input instanceof ValueObjectInterface) {
153            $input = $input->toNative();
154        }
155        if ($input instanceof stdClass) {
156            $input = json_decode(json_encode($input), true);
157        }
158        if (is_iterable($input)) {
159            $result = [];
160            foreach ($input as $key => $value) {
161                $result[$key] = self::toNative($value);
162            }
163            return $result;
164        }
165        if ($input instanceof UnitEnum) {
166            return $input;
167        }
168        if (is_object($input) && $input instanceof Stringable) {
169            return (string) $input;
170        }
171        if (is_bool($input)) {
172            return $input;
173        }
174        if (is_string($input) || is_numeric($input)) {
175            return $input;
176        }
177        if (null === $input) {
178            return null;
179        }
180        throw new InvalidTypeException($input, 'ValueObject|array|string|int|float|bool|UnitEnum|null');
181    }
182
183    public static function toEnum(string $className, mixed $input): UnitEnum
184    {
185        if ($input instanceof $className) {
186            return $input;
187        }
188
189        $input = self::toString($input);
190        foreach ($className::cases() as $enum) {
191            if ($enum->value === $input || $enum->name === $input) {
192                return $enum;
193            }
194        }
195        throw new InvalidTypeException($input, self::getDisplayNameForValueObject(new ReflectionClass($className)));
196    }
197
198    /**
199     * Converts native value in typehint.
200     */
201    public static function toTypehint(
202        ReflectionUnionType|ReflectionNamedType $typehint,
203        mixed $input
204    ): mixed {
205        if ($input === null) {
206            if ($typehint->allowsNull()) {
207                return null;
208            }
209            throw InvalidTypeException::fromTypehint($input, $typehint);
210        }
211        $types = $typehint instanceof ReflectionUnionType ? self::sortTypes($input, ...$typehint->getTypes()) : [$typehint];
212        $lastError = new InvalidTypeException($input, '(unknown)');
213        foreach ($types as $type) {
214            try {
215                if ($type->isBuiltin()) {
216                    switch ($type->getName()) {
217                        case 'string':
218                            return self::toString($input);
219                        case 'int':
220                            return self::toInt($input);
221                        case 'float':
222                            return self::toFloat($input);
223                        case 'array':
224                        case 'iterable':
225                            return self::toArray($input);
226                        case 'bool':
227                            return self::toBoolean($input);
228                        case 'mixed':
229                            return self::toMixed($input);
230                        default:
231                            throw new InvalidTypeException($input, $type->getName());
232                    }
233                }
234                $className = $type->getName();
235                switch ($className) {
236                    case stdClass::class:
237                        return json_decode(json_encode(self::toArray($input)), false);
238                    case DateTimeInterface::class:
239                    case DateTimeImmutable::class:
240                    case DateTime::class:
241                        return self::toDate($input, $className);
242                }
243                $refl = new ReflectionClass($className);
244                if ($refl->implementsInterface(ValueObjectInterface::class)) {
245                    return $className::fromNative($input);
246                }
247                if ($refl->implementsInterface(UnitEnum::class)) {
248                    return self::toEnum($className, $input);
249                }
250                throw new InvalidTypeException($className, 'ValueObjectInterface');
251            } catch (InvalidTypeException $error) {
252                $lastError = $error;
253            }
254        }
255        throw InvalidTypeException::chainException($lastError);
256    }
257
258    /**
259     * Sort typehints in a specific order.
260     *
261     * @return array<int, ReflectionNamedType>
262     */
263    public static function sortTypes(mixed $input, ReflectionNamedType... $types): array
264    {
265        $prio = [
266            'string' => gettype($input) === 'string' ?  -3 : 1,
267            'int' => 0,
268            'float' => -1,
269        ];
270        $callback = function (ReflectionNamedType $type1, ReflectionNamedType $type2) use (&$prio) : int {
271            $name1 = $type1->getName();
272            $name2 = $type2->getName();
273            $prio1 = $prio[$name1] ?? -2;
274            $prio2 = $prio[$name2] ?? -2;
275            return $prio1 <=> $prio2;
276        };
277
278        usort($types, $callback);
279        return $types;
280    }
281
282    public static function displayMixedAsString(mixed $input): string
283    {
284        if ($input === null) {
285            return '(null)';
286        }
287        if (is_object($input)) {
288            if ($input instanceof Stringable) {
289                return '(object "' . $input->__toString() . '")';
290            }
291            if ($input instanceof DateTimeInterface) {
292                return $input->format(DateTime::ATOM);
293            }
294            return '(object ' . self::getDisplayNameForValueObject(new ReflectionClass($input)) . ')';
295        }
296
297        if (is_bool($input)) {
298            return json_encode($input);
299        }
300
301        if (is_string($input) || is_numeric($input)) {
302            return (string) '"' . $input . '"';
303        }
304        if (is_resource($input)) {
305            return get_debug_type($input);
306        }
307
308        return gettype($input);
309    }
310
311    /**
312     * @param ValueObjectInterface|ReflectionClass<covariant object> $class
313     */
314    public static function getDisplayNameForValueObject(ValueObjectInterface|ReflectionClass $class): string
315    {
316        if ($class instanceof ReflectionClass) {
317            $className = $class->getShortName();
318            $refl = $class;
319        } else {
320            $refl = new ReflectionClass($class);
321            $className = $refl->getShortName();
322        }
323        foreach ($refl->getAttributes(ResourceName::class) as $attribute) {
324            return $attribute->newInstance()->name;
325        }
326        if (strcasecmp($className, 'Abstract') === 0 || strcasecmp($className, 'AbstractInterface') === 0) {
327            return 'Abstract';
328        }
329        return preg_replace(
330            '/Interface/i',
331            '',
332            preg_replace('/^abstract/i', '', $className)
333        );
334    }
335}