Completed
Branch 09branch (0b333e)
by Anton
02:57
created

Indexer::resolveDomain()   D

Complexity

Conditions 9
Paths 28

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 17
nc 28
nop 1
dl 0
loc 32
rs 4.909
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Translator;
10
11
use Spiral\Core\Component;
12
use Spiral\Debug\Traits\LoggerTrait;
13
use Spiral\Tokenizer\ClassesInterface;
14
use Spiral\Tokenizer\InvocationsInterface;
15
use Spiral\Tokenizer\Reflections\ReflectionArgument;
16
use Spiral\Tokenizer\Reflections\ReflectionInvocation;
17
use Spiral\Translator\Configs\TranslatorConfig;
18
use Spiral\Translator\Traits\TranslatorTrait;
19
20
21
/**
22
 * Index available classes and function calls to fetch every used string translation. Can
23
 * understand l, p and translate (trait) function.
24
 *
25
 * In addition Indexer will find every string specified in default value of model or class which
26
 * uses TranslatorTrait. String has to be embraced with [[ ]] in order to be indexed, you can
27
 * disable property indexation using @do-not-index doc comment. Translator can merge strings with
28
 * parent data, set class constant INHERIT_TRANSLATIONS to true.
29
 */
30
class Indexer extends Component
31
{
32
    use LoggerTrait;
33
34
    /**
35
     * @var Translator
36
     */
37
    protected $translator;
38
39
    /**
40
     * @var TranslatorConfig
41
     */
42
    protected $config;
43
44
    /**
45
     * @var Catalogue
46
     */
47
    protected $catalogue;
48
49
    /**
50
     * @param Translator       $translator
51
     * @param TranslatorConfig $config
52
     */
53
    public function __construct(Translator $translator, TranslatorConfig $config)
54
    {
55
        $this->translator = $translator;
56
        $this->config = $config;
57
58
        //Indexation into fallback (root) locale
59
        $this->catalogue = $translator->getCatalogue($config->fallbackLocale());
60
    }
61
62
    /**
63
     * Indexing available method and function invocations, target: l, p, $this->translate()
64
     * functions.
65
     *
66
     * @param InvocationsInterface $locator
67
     */
68
    public function indexInvocations(InvocationsInterface $locator)
69
    {
70
        $this->logger()->info("Indexing usages of 'l' function.");
71
        $this->registerInvocations(
72
            $locator->getInvocations(new \ReflectionFunction('l'))
73
        );
74
75
        $this->logger()->info("Indexing usages of 'p' function.");
76
        $this->registerInvocations(
77
            $locator->getInvocations(new \ReflectionFunction('p'))
78
        );
79
80
        $this->logger()->info("Indexing usages of 'say' method (TranslatorTrait).");
81
        $this->registerInvocations(
82
            $locator->getInvocations(new \ReflectionMethod(TranslatorTrait::class, 'say'))
83
        );
84
85
        $this->translator->getCatalogue()->saveDomains();
86
    }
87
88
    /**
89
     * Index and register i18n string located in default properties which belongs to TranslatorTrait
90
     * classes.
91
     *
92
     * @param ClassesInterface $locator
93
     */
94
    public function indexClasses(ClassesInterface $locator)
95
    {
96
        foreach ($locator->getClasses(TranslatorTrait::class) as $class => $options) {
97
            $reflection = new \ReflectionClass($class);
98
99
            $strings = $this->fetchStrings(
100
                $reflection,
101
                $reflection->getConstant('INHERIT_TRANSLATIONS')
102
            );
103
104
            if (!empty($strings)) {
105
                $this->logger()->info(
106
                    "Found translation string(s) in class '{class}'.",
107
                    ['class' => $reflection->getName()]
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
108
                );
109
            }
110
111
            foreach ($strings as $string) {
112
                $this->register(
113
                    $this->translator->resolveDomain($reflection->getName()),
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
114
                    $string
115
                );
116
            }
117
        }
118
119
        $this->catalogue->saveDomains();
120
    }
121
122
    /**
123
     * Register found invocations in translator bundles.
124
     *
125
     * @param ReflectionInvocation[] $invocations
126
     */
127
    protected function registerInvocations(array $invocations)
128
    {
129
        foreach ($invocations as $invocation) {
130
            if ($invocation->getArgument(0)->getType() != ReflectionArgument::STRING) {
131
                //We can only index invocations with constant string arguments
132
                continue;
133
            }
134
135
            $this->logger()->debug(
136
                "Found invocation of '{invocation}' in '{file}' at line {line}.",
137
                [
138
                    'invocation' => $invocation->getName(),
139
                    'file'       => $invocation->getFilename(),
140
                    'line'       => $invocation->getLine()
141
                ]
142
            );
143
144
            $string = $invocation->getArgument(0)->stringValue();
145
            $string = $this->removeBraces($string);
146
147
            $this->register($this->resolveDomain($invocation), $string);
148
        }
149
    }
150
151
    /**
152
     * Register string in active translator.
153
     *
154
     * @param string $domain
155
     * @param string $string
156
     */
157
    protected function register($domain, $string)
158
    {
159
        //Automatically registering
160
        $this->catalogue->set($domain, $string, $string);
161
162
        $this->logger()->debug("Found [{domain}]: '{string}'", compact('domain', 'string'));
163
    }
164
165
    /**
166
     * Get associated domain.
167
     *
168
     * @param \Spiral\Tokenizer\Reflections\ReflectionInvocation $invocation
169
     *
170
     * @return string
171
     */
172
    private function resolveDomain(ReflectionInvocation $invocation): string
173
    {
174
        //Translation using default bundle
175
        $domain = $this->config->defaultDomain();
176
177
        if ($invocation->getName() == 'say') {
178
            //Let's try to confirm domain
179
            $domain = $this->translator->resolveDomain($invocation->getClass());
180
        }
181
182
        //L and SAY functions
183
        $argument = null;
184
        switch (strtolower($invocation->getName())) {
185
            case 'say':
186
            case 'l':
187
                if ($invocation->countArguments() >= 3) {
188
                    $argument = $invocation->getArgument(2);
189
                }
190
                break;
191
            case 'p':
192
                if ($invocation->countArguments() >= 4) {
193
                    $argument = $invocation->getArgument(3);
194
                }
195
        }
196
197
        if (!empty($argument) && $argument->getType() == ReflectionArgument::STRING) {
198
            //Domain specified in arguments
199
            $domain = $this->translator->resolveDomain($argument->stringValue());
200
        }
201
202
        return $domain;
203
    }
204
205
    /**
206
     * Fetch default string values from class and merge it with parent strings if requested.
207
     *
208
     * @param \ReflectionClass $reflection
209
     * @param bool             $recursively
210
     *
211
     * @return array
212
     */
213
    private function fetchStrings(\ReflectionClass $reflection, $recursively = false)
214
    {
215
        $target = $reflection->getDefaultProperties() + $reflection->getConstants();
216
217
        foreach ($reflection->getProperties() as $property) {
218
            if (strpos($property->getDocComment(), "@do-not-index")) {
219
                unset($target[$property->getName()]);
220
            }
221
        }
222
223
        $strings = [];
224
        array_walk_recursive($target, function ($value) use (&$strings) {
225
            if (is_string($value) && $this->hasBraces($value)) {
226
                $strings[] = $this->removeBraces($value);
227
            }
228
        });
229
230
        if ($recursively && $reflection->getParentClass()) {
231
            //Joining strings data with parent class values (inheritance ON) - resolved into same
232
            //domain on export
233
            $strings = array_merge(
234
                $strings,
235
                $this->fetchStrings($reflection->getParentClass(), true)
236
            );
237
        }
238
239
        return $strings;
240
    }
241
242
    /**
243
     * Remove [[ and ]] braces from translated string.
244
     *
245
     * @param string $string
246
     *
247
     * @return string
248
     */
249
    private function removeBraces($string)
250
    {
251
        if ($this->hasBraces($string)) {
252
            //This string was defined in class attributes
253
            $string = substr($string, 2, -2);
254
        }
255
256
        return $string;
257
    }
258
259
    /**
260
     * Check if string has translation braces [[ and ]]/
261
     *
262
     * @param string $string
263
     *
264
     * @return bool
265
     */
266
    private function hasBraces($string)
267
    {
268
        return substr($string, 0, 2) == Translator::I18N_PREFIX
269
            && substr($string, -2) == Translator::I18N_POSTFIX;
270
    }
271
}