Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.27% covered (success)
93.27%
97 / 104
88.24% covered (warning)
88.24%
15 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractTranslation
93.27% covered (success)
93.27%
97 / 104
88.24% covered (warning)
88.24%
15 / 17
25.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getPlaceholders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRegularExpression
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 toNative
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __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.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
4
 createRandom
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 withoutBoundedContextId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 withBoundedContextId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 withoutResourceIdentifier
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 withResourceIdentifier
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 withoutAuthenticated
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 withAuthenticated
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getSpecifity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSimplifications
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
4
 toPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFallbackText
n/a
0 / 0
n/a
0 / 0
0
1<?php
2namespace Apie\Core\Translator\ValueObjects;
3
4use Apie\Core\ApieLib;
5use Apie\Core\Attributes\Description;
6use Apie\Core\Attributes\ExampleValue;
7use Apie\Core\Attributes\FakeMethod;
8use Apie\Core\BoundedContext\BoundedContextId;
9use Apie\Core\Identifiers\SnakeCaseSlug;
10use Apie\Core\Lists\ItemHashmap;
11use Apie\Core\RegexUtils;
12use Apie\Core\Translator\Lists\TranslationStringSet;
13use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
14use Apie\Core\ValueObjects\Interfaces\HasRegexValueObjectInterface;
15use Apie\Core\ValueObjects\Utils;
16use Faker\Generator as FakerGenerator;
17use ReflectionClass;
18use RegRev\RegRev;
19
20#[Description('A translation string for an Apie translation following a rigid, predictable structure.')]
21#[ExampleValue('apie.bounded.example.resource.user.example.test.singular.authenticated')]
22#[FakeMethod('createRandom')]
23abstract class AbstractTranslation implements HasRegexValueObjectInterface
24{
25    protected const PREFIX_REGEX = 'apie\.(bounded\.([a-z][a-z0-9]*)\.)?(resource\.([a-z0-9]+(_[a-z0-9]+)*)\.)?';
26    /**
27     * Override this to define the translation namespace.
28     * Must not include prefix or suffix separators.
29     */
30    protected const MIDDLE_REGEX = '[^.]+(\.[^.]+)*';
31    protected const SUFFIX_REGEX = '(\.(singular|plural))?(\.(authenticated|unauthenticated))?';
32
33    final protected function __construct(
34        protected TranslationStringPrefix $prefix,
35        protected string $middleSection,
36        protected TranslationStringSuffix $suffix,
37        protected ItemHashmap $placeholderValue
38    ) {
39        $regex = RegexUtils::fromPlaceholderToRegularExpression(static::MIDDLE_REGEX, includeAsCaptureGroup: false);
40        if (!preg_match($regex, $middleSection)) {
41            throw new InvalidStringForValueObjectException(
42                $prefix . $middleSection . $suffix,
43                new ReflectionClass(static::class)
44            );
45        }
46    }
47
48    public function getPlaceholders(): ItemHashmap
49    {
50        return $this->placeholderValue;
51    }
52
53    public static function getRegularExpression(): string
54    {
55        $regex = RegexUtils::fromPlaceholderToRegularExpression(static::MIDDLE_REGEX, false, false);
56        // the str_replace is a temporary fix because RegRev seems to read [^.] incorrectly.
57        return str_replace(
58            '[^.]+',
59            '[a-zA-Z0-9]+',
60            '/^' . self::PREFIX_REGEX . $regex . self::SUFFIX_REGEX . '$/'
61        );
62    }
63
64    final public function toNative(): string
65    {
66        return $this->prefix . $this->middleSection . $this->suffix;
67    }
68
69    final public function __toString(): string
70    {
71        return $this->toNative();
72    }
73
74    final public function jsonSerialize(): string
75    {
76        return $this->toNative();
77    }
78
79    final public static function fromNative(mixed $input): static
80    {
81        if (static::class === AbstractTranslation::class) {
82            $alias = explode('|', ApieLib::getAlias(static::class));
83            foreach ($alias as $class) {
84                try {
85                    return $class::fromNative($input);
86                } catch (InvalidStringForValueObjectException) {
87                }
88            }
89            throw new InvalidStringForValueObjectException($input, new ReflectionClass(static::class));
90        }
91        $input = Utils::toString($input);
92        $prefix = TranslationStringPrefix::createFromTranslation($input);
93        $inputSection = substr($input, strlen($prefix->toNative()));
94        $suffix = TranslationStringSuffix::createFromTranslation($inputSection);
95    
96        $middleSection = substr($inputSection, 0, strlen($inputSection) - strlen($suffix->toNative()));
97
98        $regex = RegexUtils::fromPlaceholderToRegularExpression(static::MIDDLE_REGEX);
99        preg_match($regex, $middleSection, $placeholders);
100        // @phpstan-ignore-next-line nullCoalesce.variable
101        $placeholders = array_filter($placeholders ?? [], fn ($k) => !is_numeric($k), ARRAY_FILTER_USE_KEY);
102        
103        return new static(
104            $prefix,
105            $middleSection,
106            $suffix,
107            (new ItemHashmap($placeholders))
108        );
109    }
110
111    public static function createRandom(FakerGenerator $factory): static
112    {
113        if (static::class === AbstractTranslation::class) {
114            $aliases = explode('|', ApieLib::getAlias(AbstractTranslation::class));
115            $alias = $factory->randomElement($aliases);
116
117            return $alias::createRandom($factory);
118        }
119
120        $regularExpressionWithDelimiter = static::getRegularExpression();
121        $regex = RegexUtils::removeDelimiters($regularExpressionWithDelimiter);
122        return static::fromNative(RegRev::generate($regex));
123    }
124
125    public function withoutBoundedContextId(): static
126    {
127        return new static(
128            $this->prefix->withoutBoundedContextId(),
129            $this->middleSection,
130            $this->suffix,
131            $this->placeholderValue
132        );
133    }
134
135    public function withBoundedContextId(BoundedContextId $boundedContextId): static
136    {
137        return new static(
138            $this->prefix->withBoundedContextId($boundedContextId),
139            $this->middleSection,
140            $this->suffix,
141            $this->placeholderValue
142        );
143    }
144
145    public function withoutResourceIdentifier(): static
146    {
147        return new static(
148            $this->prefix->withoutResourceIdentifier(),
149            $this->middleSection,
150            $this->suffix,
151            $this->placeholderValue
152        );
153    }
154
155    public function withResourceIdentifier(SnakeCaseSlug $resourceIdentifier): static
156    {
157        return new static(
158            $this->prefix->withResourceIdentifier($resourceIdentifier),
159            $this->middleSection,
160            $this->suffix,
161            $this->placeholderValue
162        );
163    }
164
165    public function withoutAuthenticated(): static
166    {
167        return new static(
168            $this->prefix,
169            $this->middleSection,
170            $this->suffix->withoutAuthenticated(),
171            $this->placeholderValue
172        );
173    }
174
175    public function withAuthenticated(bool $authenticated): static
176    {
177        return new static(
178            $this->prefix,
179            $this->middleSection,
180            $this->suffix->withAuthenticated($authenticated),
181            $this->placeholderValue
182        );
183    }
184
185    final public function getSpecifity(): int
186    {
187        return $this->prefix->getSpecifity() + $this->suffix->getSpecifity();
188    }
189
190    public function getSimplifications(): TranslationStringSet
191    {
192        $list = [];
193        foreach ($this->prefix->getSimplifications() as $prefixSimplification) {
194            $list[] = new static(
195                $prefixSimplification,
196                $this->middleSection,
197                $this->suffix,
198                $this->placeholderValue
199            );
200            foreach ($this->suffix->getSimplifications() as $suffixSimplification) {
201                $list[] = new static(
202                    $prefixSimplification,
203                    $this->middleSection,
204                    $suffixSimplification,
205                    $this->placeholderValue
206                );
207            }
208        }
209        foreach ($this->suffix->getSimplifications() as $suffixSimplification) {
210            $list[] = new static(
211                $this->prefix,
212                $this->middleSection,
213                $suffixSimplification,
214                $this->placeholderValue
215            );
216        }
217        return new TranslationStringSet($list);
218    }
219
220    final public function toPath(string $rootPath): string
221    {
222        return rtrim($rootPath, '/') . '/' . str_replace('.', '/', $this->toNative());
223    }
224
225    abstract public function getFallbackText(): string;
226}