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