Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.88% covered (success)
96.88%
124 / 128
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SnowflakeIdentifier
96.88% covered (success)
96.88%
124 / 128
57.14% covered (warning)
57.14%
4 / 7
40
0.00% covered (danger)
0.00%
0 / 1
 getSeparator
n/a
0 / 0
n/a
0 / 0
0
 getMinimumNumberOfSegments
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 toNative
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 __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
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
10.05
 getRegularExpression
98.33% covered (success)
98.33%
59 / 60
0.00% covered (danger)
0.00%
0 / 1
15
 createRegex
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
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 protected static function getMinimumNumberOfSegments(): int
21    {
22        $refl = new ReflectionClass(static::class);
23        $parameters = $refl->getConstructor()->getParameters();
24        $parameters = array_reverse($parameters);
25        $count = count($parameters) - 1;
26        foreach ($parameters as $parameter) {
27            if (!$parameter->isOptional() || !$parameter->getType()?->allowsNull()) {
28                return $count;
29            }
30            $count--;
31        }
32
33        return 0;
34    }
35
36    final public function toNative(): string
37    {
38        if (!isset($this->calculated)) {
39            $prefix = '';
40            if (is_callable([static::class, 'getPrefix'])) {
41                $prefix = static::getPrefix() . static::getSeparator();
42            }
43            $refl = new ReflectionClass($this);
44            $separator = static::getSeparator();
45            $result = [];
46            $minCount = static::getMinimumNumberOfSegments();
47            $count = 0;
48            foreach ($refl->getConstructor()->getParameters() as $parameter) {
49                $propertyName = $parameter->getName();
50                $propertyValue = $refl->getProperty($propertyName)->getValue($this);
51                $stringPropertyValue = get_debug_type($propertyValue) === 'float'
52                    ? number_format($propertyValue, 6, '.', '')
53                    : Utils::toString($propertyValue);
54                if (strpos($stringPropertyValue, $separator) !== false) {
55                    throw new InvalidStringForValueObjectException($stringPropertyValue, $propertyValue);
56                }
57                if ($propertyValue !== null || $count < $minCount) {
58                    $result[] = $stringPropertyValue;
59                }
60                $count++;
61            }
62
63            $this->calculated = $prefix . implode($separator, $result);
64        }
65        return $this->calculated;
66    }
67
68    final public function __toString(): string
69    {
70        return $this->toNative();
71    }
72
73    final public function jsonSerialize(): string
74    {
75        return $this->toNative();
76    }
77
78    public static function fromNative(mixed $input): self
79    {
80        $input = Utils::toString($input);
81        $prefix = '';
82        if (is_callable([static::class, 'getPrefix'])) {
83            $prefix = static::getPrefix() . static::getSeparator();
84        }
85        if (strpos($input, $prefix) === 0) {
86            $input = substr($input, strlen($prefix));
87        } else {
88            throw new InvalidStringForValueObjectException($input, new ReflectionClass(static::class));
89        }
90        $refl = new ReflectionClass(static::class);
91        $parameters = $refl->getConstructor()->getParameters();
92        $separator = static::getSeparator();
93        $split = explode($separator, $input, count($parameters));
94        $minCount = static::getMinimumNumberOfSegments();
95        $maxCount = count($parameters);
96        if (count($split) < $minCount || count($split) > $maxCount) {
97            throw new InvalidStringForValueObjectException($input, new ReflectionClass(static::class));
98        }
99        $constructorArguments = [];
100    
101        foreach ($parameters as $key => $parameter) {
102            $parameterType = $parameter->getType();
103            if (!($parameterType instanceof ReflectionNamedType)) {
104                throw new InvalidTypeException($parameterType, 'ReflectionNamedType');
105            }
106            $splitValue = $split[$key] ?? null;
107            if ($parameterType->allowsNull() && ($splitValue === '' || $splitValue === null)) {
108                $constructorArguments[] = null;
109            } else {
110                $constructorArguments[] = Utils::toTypehint($parameterType, $splitValue ?? '');
111            }
112        }
113        return $refl->newInstanceArgs($constructorArguments);
114    }
115
116    final public static function getRegularExpression(): string
117    {
118        $refl = new ReflectionClass(static::class);
119        $parameters = $refl->getConstructor()->getParameters();
120        $separator = preg_quote(static::getSeparator());
121
122        $expressions = [];
123        $optionalExpressions = [];
124        $prefix = '';
125        if (is_callable([static::class, 'getPrefix'])) {
126            $prefix = static::getPrefix() . static::getSeparator();
127        }
128        if ($prefix !== '') {
129            $expressions[] = preg_quote($prefix, static::getSeparator());
130        }
131        $addedOptionalMarker = false;
132        $addedSeparator = false;
133        $addedOptionalSeparator = false;
134        foreach ($parameters as $parameter) {
135            $parameterType = $parameter->getType();
136            if (!($parameterType instanceof ReflectionNamedType)) {
137                throw new InvalidTypeException($parameterType, 'ReflectionNamedType');
138            }
139            if ($parameter->isOptional()) {
140                $addedOptionalMarker = true;
141            }
142            $regex = '[^' . $separator . ']+';
143            $class = ConverterUtils::toReflectionClass($parameterType);
144            if (in_array(HasRegexValueObjectInterface::class, $class?->getInterfaceNames() ?? [])) {
145                $foundRegex = '(' . RegexUtils::removeDelimiters($class->getMethod('getRegularExpression')->invoke(null)) . ')';
146                if (strpos($foundRegex, '?=') === false) {
147                    $regex = $foundRegex;
148                }
149            } else {
150                switch ($parameterType->getName()) {
151                    case 'int':
152                        $regex = '-?(0|[1-9]\d*)';
153                        break;
154                    case 'float':
155                        $regex = '-?(0|[1-9]\d*)(\.\d+)?';
156                        break;
157                }
158            }
159            if ($addedOptionalMarker) {
160                $optionalExpressions[] = $regex;
161                $optionalExpressions[] = $separator;
162                $addedOptionalSeparator = true;
163            } else {
164                $expressions[] = $regex;
165                $expressions[] = $separator;
166                $addedSeparator = true;
167            }
168        }
169        if ($addedSeparator) {
170            $last = array_pop($expressions);
171            if (!empty($optionalExpressions)) {
172                array_unshift($optionalExpressions, $last);
173            }
174        }
175        if ($addedOptionalSeparator) {
176            array_pop($optionalExpressions);
177        }
178
179        $requiredExpression = self::createRegex($expressions);
180        $optionalExpression = self::createRegex($optionalExpressions);
181        if ($optionalExpressions) {
182            $optionalExpression = '(' . $optionalExpression . ')?';
183        }
184
185        $tmp = CompiledRegularExpression::createFromRegexWithoutDelimiters('');
186        $expressions = array_map(
187            function (string $expression) {
188                return CompiledRegularExpression::createFromRegexWithoutDelimiters($expression)
189                            ->removeStartAndEndMarkers();
190            },
191            [$requiredExpression, $optionalExpression]
192        );
193        array_unshift($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('^'));
194        array_push($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('$'));
195
196        return $tmp->merge(...$expressions)->__toString();
197    }
198
199    /**
200     * @param array<int, string> $expressions
201     */
202    private static function createRegex(array $expressions): string
203    {
204        $expressions = array_map(
205            function (string $expression) {
206                return CompiledRegularExpression::createFromRegexWithoutDelimiters($expression)
207                            ->removeStartAndEndMarkers();
208            },
209            $expressions
210        );
211        array_unshift($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('^'));
212        array_push($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('$'));
213
214        return implode('', $expressions);
215    }
216}