Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.56% covered (success)
97.56%
80 / 82
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SnowflakeIdentifier
97.56% covered (success)
97.56%
80 / 82
60.00% covered (warning)
60.00%
3 / 5
25
0.00% covered (danger)
0.00%
0 / 1
 getSeparator
n/a
0 / 0
n/a
0 / 0
0
 toNative
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromNative
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
8
 getRegularExpression
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
9
1<?php
2namespace Apie\Core\ValueObjects;
3
4use Apie\Core\Exceptions\InvalidTypeException;
5use Apie\Core\RegexUtils;
6use Apie\Core\Utils\ConverterUtils;
7use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
8use Apie\Core\ValueObjects\Interfaces\HasRegexValueObjectInterface;
9use Apie\Core\ValueObjects\Interfaces\ValueObjectInterface;
10use Apie\RegexTools\CompiledRegularExpression;
11use ReflectionClass;
12use ReflectionNamedType;
13
14abstract class SnowflakeIdentifier implements ValueObjectInterface, HasRegexValueObjectInterface
15{
16    private string $calculated;
17
18    abstract protected static function getSeparator(): string;
19
20    final public function toNative(): string
21    {
22        if (!isset($this->calculated)) {
23            $prefix = '';
24            if (is_callable([static::class, 'getPrefix'])) {
25                $prefix = static::getPrefix() . '-';
26            }
27            $refl = new ReflectionClass($this);
28            $separator = static::getSeparator();
29            $result = [];
30            foreach ($refl->getConstructor()->getParameters() as $parameter) {
31                $propertyName = $parameter->getName();
32                $propertyValue = $refl->getProperty($propertyName)->getValue($this);
33                $stringPropertyValue = get_debug_type($propertyValue) === 'float'
34                    ? number_format($propertyValue, 6, '.', '')
35                    : Utils::toString($propertyValue);
36                if (strpos($stringPropertyValue, $separator) !== false) {
37                    throw new InvalidStringForValueObjectException($stringPropertyValue, $propertyValue);
38                }
39                $result[] = $stringPropertyValue;
40            }
41
42            $this->calculated = $prefix . implode($separator, $result);
43        }
44        return $this->calculated;
45    }
46
47    final public function __toString(): string
48    {
49        return $this->toNative();
50    }
51
52    final public function jsonSerialize(): string
53    {
54        return $this->toNative();
55    }
56
57    public static function fromNative(mixed $input): self
58    {
59        $input = Utils::toString($input);
60        $prefix = '';
61        if (is_callable([static::class, 'getPrefix'])) {
62            $prefix = static::getPrefix() . '-';
63        }
64        if (strpos($input, $prefix) === 0) {
65            $input = substr($input, strlen($prefix));
66        } else {
67            throw new InvalidStringForValueObjectException($input, new ReflectionClass(static::class));
68        }
69        $refl = new ReflectionClass(static::class);
70        $parameters = $refl->getConstructor()->getParameters();
71        $separator = static::getSeparator();
72        $split = explode($separator, $input, count($parameters));
73        if (count($split) !== count($parameters)) {
74            throw new InvalidStringForValueObjectException($input, new ReflectionClass(static::class));
75        }
76        $constructorArguments = [];
77        foreach ($parameters as $key => $parameter) {
78            $parameterType = $parameter->getType();
79            if (!($parameterType instanceof ReflectionNamedType)) {
80                throw new InvalidTypeException($parameterType, 'ReflectionNamedType');
81            }
82            if ($parameterType->allowsNull() && $split[$key] === '') {
83                $constructorArguments[] = null;
84            } else {
85                $constructorArguments[] = Utils::toTypehint($parameterType, $split[$key]);
86            }
87        }
88        return $refl->newInstanceArgs($constructorArguments);
89    }
90
91    final public static function getRegularExpression(): string
92    {
93        $refl = new ReflectionClass(static::class);
94        $parameters = $refl->getConstructor()->getParameters();
95        $separator = preg_quote(static::getSeparator());
96
97        $expressions = [];
98        $prefix = '';
99        if (is_callable([static::class, 'getPrefix'])) {
100            $prefix = static::getPrefix() . '-';
101        }
102        if ($prefix !== '') {
103            $expressions[] = preg_quote($prefix, static::getSeparator());
104        }
105        foreach ($parameters as $parameter) {
106            $parameterType = $parameter->getType();
107            if (!($parameterType instanceof ReflectionNamedType)) {
108                throw new InvalidTypeException($parameterType, 'ReflectionNamedType');
109            }
110            $regex = '[^' . $separator . ']+';
111            $class = ConverterUtils::toReflectionClass($parameterType);
112            if (in_array(HasRegexValueObjectInterface::class, $class?->getInterfaceNames() ?? [])) {
113                $foundRegex = '(' . RegexUtils::removeDelimiters($class->getMethod('getRegularExpression')->invoke(null)) . ')';
114                if (strpos($foundRegex, '?=') === false) {
115                    $regex = $foundRegex;
116                }
117            } else {
118                switch ($parameterType->getName()) {
119                    case 'int':
120                        $regex = '-?(0|[1-9]\d*)';
121                        break;
122                    case 'float':
123                        $regex = '-?(0|[1-9]\d*)(\.\d+)?';
124                        break;
125                }
126            }
127            $expressions[] = $regex;
128            $expressions[] = $separator;
129        }
130        array_pop($expressions);
131
132        $expressions = array_map(
133            function (string $expression) {
134                return CompiledRegularExpression::createFromRegexWithoutDelimiters($expression)
135                            ->removeStartAndEndMarkers();
136            },
137            $expressions
138        );
139        array_unshift($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('^'));
140        array_push($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('$'));
141
142        $tmp = CompiledRegularExpression::createFromRegexWithoutDelimiters('');
143
144        return $tmp->merge(...$expressions)->__toString();
145    }
146}