Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.92% covered (success)
95.92%
94 / 98
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SnowflakeIdentifier
95.92% covered (success)
95.92%
94 / 98
50.00% covered (danger)
50.00%
3 / 6
33
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
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 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        $prefix = '';
124        if (is_callable([static::class, 'getPrefix'])) {
125            $prefix = static::getPrefix() . static::getSeparator();
126        }
127        if ($prefix !== '') {
128            $expressions[] = preg_quote($prefix, static::getSeparator());
129        }
130        foreach ($parameters as $parameter) {
131            $parameterType = $parameter->getType();
132            if (!($parameterType instanceof ReflectionNamedType)) {
133                throw new InvalidTypeException($parameterType, 'ReflectionNamedType');
134            }
135            $regex = '[^' . $separator . ']+';
136            $class = ConverterUtils::toReflectionClass($parameterType);
137            if (in_array(HasRegexValueObjectInterface::class, $class?->getInterfaceNames() ?? [])) {
138                $foundRegex = '(' . RegexUtils::removeDelimiters($class->getMethod('getRegularExpression')->invoke(null)) . ')';
139                if (strpos($foundRegex, '?=') === false) {
140                    $regex = $foundRegex;
141                }
142            } else {
143                switch ($parameterType->getName()) {
144                    case 'int':
145                        $regex = '-?(0|[1-9]\d*)';
146                        break;
147                    case 'float':
148                        $regex = '-?(0|[1-9]\d*)(\.\d+)?';
149                        break;
150                }
151            }
152            $expressions[] = $regex;
153            $expressions[] = $separator;
154        }
155        array_pop($expressions);
156
157        $expressions = array_map(
158            function (string $expression) {
159                return CompiledRegularExpression::createFromRegexWithoutDelimiters($expression)
160                            ->removeStartAndEndMarkers();
161            },
162            $expressions
163        );
164        array_unshift($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('^'));
165        array_push($expressions, CompiledRegularExpression::createFromRegexWithoutDelimiters('$'));
166
167        $tmp = CompiledRegularExpression::createFromRegexWithoutDelimiters('');
168
169        return $tmp->merge(...$expressions)->__toString();
170    }
171}