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