Passed
Branch master (9e4d0d)
by Fran
03:08
created

Router   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 511
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 51.98%

Importance

Changes 0
Metric Value
dl 0
loc 511
ccs 131
cts 252
cp 0.5198
rs 2.6956
c 0
b 0
f 0
wmc 93
lcom 1
cbo 14

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A init() 0 10 3
A httpNotFound() 0 22 3
A getSlugs() 0 4 1
A getAllRoutes() 0 9 3
C execute() 0 32 7
C searchAction() 0 24 8
A sentAuthHeader() 0 4 1
A generateRouting() 0 15 3
B hydrateRouting() 0 17 6
A inspectDir() 0 11 2
A exists() 0 3 3
C addRouting() 0 24 9
A extractDomain() 0 10 2
A simpatize() 0 10 1
B slugify() 0 25 3
B getRoute() 0 17 10
C getAdminRoutes() 0 31 11
A getAdmin() 0 4 1
A getDomains() 0 4 2
B executeCachedRoute() 0 21 5
B generateSlugs() 0 18 6
A generateTranslationsFile() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PSFS\base;
4
5
use PSFS\base\config\Config;
6
use PSFS\base\exception\AccessDeniedException;
7
use PSFS\base\exception\ConfigException;
8
use PSFS\base\exception\RouterException;
9
use PSFS\base\types\helpers\RequestHelper;
10
use PSFS\base\types\helpers\RouterHelper;
11
use PSFS\base\types\helpers\SecurityHelper;
12
use PSFS\base\types\SingletonTrait;
13
use PSFS\controller\base\Admin;
14
use PSFS\services\AdminServices;
15
use Symfony\Component\Finder\Finder;
16
17
18
/**
19
 * Class Router
20
 * @package PSFS
21
 */
22
class Router
23
{
24
25
    use SingletonTrait;
26
27
    protected $routing;
28
    protected $slugs;
29
    private $domains;
30
    /**
31
     * @var Finder $finder
32
     */
33
    private $finder;
34
    /**
35
     * @var \PSFS\base\Cache $cache
36
     */
37
    private $cache;
38
    /**
39
     * @var bool headersSent
40
     */
41
    protected $headersSent = false;
42
43
    /**
44
     * Constructor Router
45
     * @throws ConfigException
46
     */
47 6
    public function __construct()
48
    {
49 6
        $this->finder = new Finder();
50 6
        $this->cache = Cache::getInstance();
51 6
        $this->init();
52 6
    }
53
54
    /**
55
     * Inicializador Router
56
     * @throws ConfigException
57
     */
58 1
    public function init()
59
    {
60 1
        if (!file_exists(CONFIG_DIR . DIRECTORY_SEPARATOR . "urls.json") || Config::getInstance()->getDebugMode()) {
61 1
            $this->hydrateRouting();
62 1
            $this->simpatize();
63 1
        } else {
64 1
            list($this->routing, $this->slugs) = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . "urls.json", Cache::JSON, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 146 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
65 1
            $this->domains = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . "domains.json", Cache::JSON, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
66
        }
67 1
    }
68
69
    /**
70
     * Método que deriva un error HTTP de página no encontrada
71
     *
72
     * @param \Exception $e
73
     *
74
     * @return string HTML
75
     */
76
    public function httpNotFound(\Exception $e = NULL)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $e. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
77
    {
78
        Logger::log('Throw not found exception');
79
        $template = Template::getInstance()->setStatus($e->getCode());
80
        if (preg_match('/json/i', Request::getInstance()->getServer('CONTENT_TYPE'))) {
81
            return $template->output(json_encode(array(
82
                "success" => FALSE,
83
                "error" => $e->getMessage(),
84
            )), 'application/json');
85
        } else {
86
            if (NULL === $e) {
87
                Logger::log('Not found page throwed without previus exception');
88
                $e = new \Exception(_('Page not found'), 404);
89
            }
90
91
            return $template->render('error.html.twig', array(
92
                'exception' => $e,
93
                'trace' => $e->getTraceAsString(),
94
                'error_page' => TRUE,
95
            ));
96
        }
97
    }
98
99
    /**
100
     * Método que devuelve las rutas
101
     * @return string|null
102
     */
103
    public function getSlugs()
104
    {
105
        return $this->slugs;
106
    }
107
108
    /**
109
     * Method that extract all routes in the platform
110
     * @return array
111
     */
112
    public function getAllRoutes() {
113
        $routes = [];
114
        foreach($this->routing as $path => $route) {
115
            if(array_key_exists('slug', $route)) {
116
                $routes[$route['slug']] = $path;
117
            }
118
        }
119
        return $routes;
120
    }
121
122
    /**
123
     * Método que calcula el objeto a enrutar
124
     *
125
     * @param string|null $route
126
     *
127
     * @throws \Exception
128
     * @return string HTML
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
129
     */
130
    public function execute($route)
131
    {
132
        Logger::log('Executing the request');
133
        try {
134
            //Check CORS for requests
135
            RequestHelper::checkCORS();
136
            // Checks restricted access
137
            SecurityHelper::checkRestrictedAccess($route);
138
            //Search action and execute
139
            $this->searchAction($route);
140
        } catch (AccessDeniedException $e) {
141
            Logger::log(_('Solicitamos credenciales de acceso a zona restringida'));
142
            return Admin::staticAdminLogon($route);
143
        } catch (RouterException $r) {
144
            if (FALSE !== preg_match('/\/$/', $route)) {
145
                if (preg_match('/admin/', $route)) {
146
                    $default = Config::getInstance()->get('admin_action') ?: 'admin-login';
147
                } else {
148
                    $default = Config::getInstance()->get('home_action');
149
                }
150
151
                return $this->execute($this->getRoute($default));
152
            }
153
            Logger::log($r->getMessage(), LOG_WARNING);
154
            throw $r;
155
        } catch (\Exception $e) {
156
            Logger::log($e->getMessage(), LOG_ERR);
157
            throw $e;
158
        }
159
160
        return $this->httpNotFound();
161
    }
162
163
    /**
164
     * Método que busca el componente que ejecuta la ruta
165
     *
166
     * @param string $route
167
     *
168
     * @throws \PSFS\base\exception\RouterException
169
     */
170
    protected function searchAction($route)
171
    {
172
        Logger::log('Searching action to execute: ' . $route, LOG_INFO);
173
        //Revisamos si tenemos la ruta registrada
174
        $parts = parse_url($route);
175
        $path = (array_key_exists('path', $parts)) ? $parts['path'] : $route;
176
        $httpRequest = Request::getInstance()->getMethod();
177
        foreach ($this->routing as $pattern => $action) {
178
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
179
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
180
            if ($matched && ($httpMethod === "ALL" || $httpRequest === $httpMethod) && RouterHelper::compareSlashes($routePattern, $path)) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 140 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
181
                $get = RouterHelper::extractComponents($route, $routePattern);
182
                /** @var $class \PSFS\base\types\Controller */
183
                $class = RouterHelper::getClassToCall($action);
184
                try {
185
                    $this->executeCachedRoute($route, $action, $class, $get);
186
                } catch (\Exception $e) {
187
                    Logger::log($e->getMessage(), LOG_ERR);
188
                    throw new RouterException($e->getMessage(), 404, $e);
189
                }
190
            }
191
        }
192
        throw new RouterException(_("Ruta no encontrada"));
193
    }
194
195
    /**
196
     * Método que manda las cabeceras de autenticación
197
     * @return string HTML
198
     */
199
    protected function sentAuthHeader()
200
    {
201
        return AdminServices::getInstance()->setAdminHeaders();
202
    }
203
204
    /**
205
     * Method that gather all the routes in the project
206
     */
207 1
    private function generateRouting()
208
    {
209 1
        $base = SOURCE_DIR;
210 1
        $modules = realpath(CORE_DIR);
211 1
        $this->routing = $this->inspectDir($base, "PSFS", array());
212 1
        if (file_exists($modules)) {
213
            $module = "";
214
            if(file_exists($modules . DIRECTORY_SEPARATOR . 'module.json')) {
215
                $mod_cfg = json_decode(file_get_contents($modules . DIRECTORY_SEPARATOR . 'module.json'), true);
216
                $module = $mod_cfg['module'];
217
            }
218
            $this->routing = $this->inspectDir($modules, $module, $this->routing);
219
        }
220 1
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . "domains.json", $this->domains, Cache::JSON, TRUE);
221 1
    }
222
223
    /**
224
     * Método que regenera el fichero de rutas
225
     * @throws ConfigException
226
     */
227 1
    public function hydrateRouting()
228
    {
229 1
        $this->generateRouting();
230 1
        $home = Config::getInstance()->get('home_action');
231 1
        if (NULL !== $home || $home !== '') {
232 1
            $home_params = NULL;
233 1
            foreach ($this->routing as $pattern => $params) {
234 1
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
235 1
                if (preg_match("/" . preg_quote($route, "/") . "$/i", "/" . $home)) {
236
                    $home_params = $params;
237
                }
238 1
            }
239 1
            if (NULL !== $home_params) {
240
                $this->routing['/'] = $home_params;
241
            }
242 1
        }
243 1
    }
244
245
    /**
246
     * Método que inspecciona los directorios en busca de clases que registren rutas
247
     *
248
     * @param string $origen
249
     * @param string $namespace
250
     * @param array $routing
251
     *
252
     * @return array
253
     * @throws ConfigException
254
     */
255 1
    private function inspectDir($origen, $namespace = 'PSFS', $routing)
256
    {
257 1
        $files = $this->finder->files()->in($origen)->path('/(controller|api)/i')->name("*.php");
258 1
        foreach ($files as $file) {
259 1
            $filename = str_replace("/", '\\', str_replace($origen, '', $file->getPathname()));
260 1
            $routing = $this->addRouting($namespace . str_replace('.php', '', $filename), $routing);
261 1
        }
262 1
        $this->finder = new Finder();
263
264 1
        return $routing;
265
    }
266
267
    /**
268
     * Checks that a namespace exists
269
     * @param string $namespace
270
     * @return bool
271
     */
272 1
    public static function exists($namespace) {
273 1
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
274
    }
275
276
    /**
277
     * Método que añade nuevas rutas al array de referencia
278
     *
279
     * @param string $namespace
280
     * @param array $routing
281
     *
282
     * @return array
283
     * @throws ConfigException
284
     */
285 1
    private function addRouting($namespace, &$routing)
286
    {
287 1
        if (self::exists($namespace)) {
288 1
            $reflection = new \ReflectionClass($namespace);
289 1
            if (FALSE === $reflection->isAbstract() && FALSE === $reflection->isInterface()) {
290 1
                $this->extractDomain($reflection);
291 1
                $classComments = $reflection->getDocComment();
292 1
                preg_match('/@api\ (.*)\n/im', $classComments, $apiPath);
293 1
                $api = '';
294 1
                if (count($apiPath)) {
295
                    $api = array_key_exists(1, $apiPath) ? $apiPath[1] : $api;
296
                }
297 1
                foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
298 1
                    list($route, $info) = RouterHelper::extractRouteInfo($method, $api);
299 1
                    if(null !== $route && null !== $info) {
300 1
                        $info['class'] = $namespace;
301 1
                        $routing[$route] = $info;
302 1
                    }
303 1
                }
304 1
            }
305 1
        }
306
307 1
        return $routing;
308
    }
309
310
    /**
311
     * Método que extrae de la ReflectionClass los datos necesarios para componer los dominios en los templates
312
     *
313
     * @param \ReflectionClass $class
314
     *
315
     * @return Router
316
     * @throws ConfigException
317
     */
318 1
    protected function extractDomain(\ReflectionClass $class)
319
    {
320
        //Calculamos los dominios para las plantillas
321 1
        if ($class->hasConstant("DOMAIN")) {
322 1
            $domain = "@" . $class->getConstant("DOMAIN") . "/";
323 1
            $this->domains[$domain] = RouterHelper::extractDomainInfo($class, $domain);
324 1
        }
325
326 1
        return $this;
327
    }
328
329
    /**
330
     * Método que genera las urls amigables para usar dentro del framework
331
     * @return Router
332
     */
333 1
    public function simpatize()
334
    {
335 1
        $translationFileName = "translations" . DIRECTORY_SEPARATOR . "routes_translations.php";
336 1
        $absoluteTranslationFileName = CACHE_DIR . DIRECTORY_SEPARATOR . $translationFileName;
337 1
        $this->generateSlugs($absoluteTranslationFileName);
338 1
        Config::createDir(CONFIG_DIR);
339 1
        Cache::getInstance()->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . "urls.json", array($this->routing, $this->slugs), Cache::JSON, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 144 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
340
341 1
        return $this;
342
    }
343
344
    /**
345
     * Método que devuelve el slug de un string dado
346
     *
347
     * @param string $text
348
     *
349
     * @return string
350
     */
351 1
    private function slugify($text)
352
    {
353
        // replace non letter or digits by -
354 1
        $text = preg_replace('~[^\\pL\d]+~u', '-', $text);
355
356
        // trim
357 1
        $text = trim($text, '-');
358
359
        // transliterate
360 1
        if (function_exists('iconv')) {
361 1
            $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
362 1
        }
363
364
        // lowercase
365 1
        $text = strtolower($text);
366
367
        // remove unwanted characters
368 1
        $text = preg_replace('~[^-\w]+~', '', $text);
369
370 1
        if (empty($text)) {
371
            return 'n-a';
372
        }
373
374 1
        return $text;
375
    }
376
377
    /**
378
     * Método que devuelve una ruta del framework
379
     *
380
     * @param string $slug
381
     * @param boolean $absolute
382
     * @param array $params
383
     *
384
     * @return string|null
385
     * @throws RouterException
386
     */
387
    public function getRoute($slug = '', $absolute = FALSE, $params = [])
388
    {
389
        if (strlen($slug) === 0) {
390
            return ($absolute) ? Request::getInstance()->getRootUrl() . '/' : '/';
391
        }
392
        if (NULL === $slug || !array_key_exists($slug, $this->slugs)) {
393
            throw new RouterException(_("No existe la ruta especificada"));
394
        }
395
        $url = ($absolute) ? Request::getInstance()->getRootUrl() . $this->slugs[$slug] : $this->slugs[$slug];
396
        if (!empty($params)) foreach ($params as $key => $value) {
397
            $url = str_replace("{" . $key . "}", $value, $url);
398
        } elseif (!empty($this->routing[$this->slugs[$slug]]["default"])) {
399
            $url = ($absolute) ? Request::getInstance()->getRootUrl() . $this->routing[$this->slugs[$slug]]["default"] : $this->routing[$this->slugs[$slug]]["default"];
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 168 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
400
        }
401
402
        return preg_replace('/(GET|POST|PUT|DELETE|ALL)\#\|\#/', '', $url);
403
    }
404
405
    /**
406
     * Método que devuelve las rutas de administración
407
     * @return array
408
     */
409 1
    public function getAdminRoutes()
410
    {
411 1
        $routes = array();
412 1
        foreach ($this->routing as $route => $params) {
413 1
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($route);
414 1
            if (preg_match('/^\/admin(\/|$)/', $routePattern)) {
415 1
                if (preg_match('/^\\\?PSFS/', $params["class"])) {
416 1
                    $profile = "superadmin";
417 1
                } else {
418
                    $profile = "admin";
419
                }
420 1
                if (!empty($params["default"]) && preg_match('/(GET|ALL)/i', $httpMethod)) {
421 1
                    $_profile = ($params["visible"]) ? $profile : 'adminhidden';
422 1
                    if (!array_key_exists($_profile, $routes)) {
423 1
                        $routes[$_profile] = array();
424 1
                    }
425 1
                    $routes[$_profile][] = $params["slug"];
426 1
                }
427 1
            }
428 1
        }
429 1
        if (array_key_exists("superadmin", $routes)) {
430 1
            asort($routes["superadmin"]);
431 1
        }
432 1
        if (array_key_exists("adminhidden", $routes)) {
433 1
            asort($routes["adminhidden"]);
434 1
        }
435 1
        if (array_key_exists('admin', $routes)) {
436
            asort($routes["admin"]);
437
        }
438 1
        return $routes;
439
    }
440
441
    /**
442
     * Método que devuelve le controlador del admin
443
     * @return Admin
444
     */
445
    public function getAdmin()
446
    {
447
        return Admin::getInstance();
448
    }
449
450
    /**
451
     * Método que extrae los dominios
452
     * @return array
453
     */
454
    public function getDomains()
455
    {
456
        return $this->domains ?: [];
457
    }
458
459
    /**
460
     * Método que ejecuta una acción del framework y revisa si lo tenemos cacheado ya o no
461
     *
462
     * @param string $route
463
     * @param array|null $action
464
     * @param types\Controller $class
465
     * @param array $params
0 ignored issues
show
Documentation introduced by
Should the type for parameter $params not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
466
     */
467
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
468
    {
469
        Logger::log('Executing route ' . $route, LOG_INFO);
470
        Security::getInstance()->setSessionKey("__CACHE__", $action);
471
        $cache = Cache::needCache();
472
        $execute = TRUE;
473
        if (FALSE !== $cache && Config::getInstance()->getDebugMode() === FALSE) {
474
            $cacheDataName = $this->cache->getRequestCacheHash();
475
            $cachedData = $this->cache->readFromCache("templates" . DIRECTORY_SEPARATOR . $cacheDataName,
476
                $cache, function () {});
0 ignored issues
show
Bug introduced by
It seems like $cache defined by \PSFS\base\Cache::needCache() on line 471 can also be of type boolean; however, PSFS\base\Cache::readFromCache() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
477
            if (NULL !== $cachedData) {
478
                $headers = $this->cache->readFromCache("templates" . DIRECTORY_SEPARATOR . $cacheDataName . ".headers",
479
                    $cache, function () {}, Cache::JSON);
0 ignored issues
show
Bug introduced by
It seems like $cache defined by \PSFS\base\Cache::needCache() on line 471 can also be of type boolean; however, PSFS\base\Cache::readFromCache() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
480
                Template::getInstance()->renderCache($cachedData, $headers);
481
                $execute = FALSE;
482
            }
483
        }
484
        if ($execute) {
485
            call_user_func_array(array($class, $action['method']), $params);
486
        }
487
    }
488
489
    /**
490
     * Parse slugs to create translations
491
     *
492
     * @param string $absoluteTranslationFileName
493
     */
494 1
    private function generateSlugs($absoluteTranslationFileName)
495
    {
496 1
        $translations = $this->generateTranslationsFile($absoluteTranslationFileName);
497 1
        foreach ($this->routing as $key => &$info) {
498 1
            $keyParts = $key;
499 1
            if (FALSE === strstr("#|#", $key)) {
500 1
                $keyParts = explode("#|#", $key);
501 1
                $keyParts = array_key_exists(1, $keyParts) ? $keyParts[1] : '';
502 1
            }
503 1
            $slug = $this->slugify($keyParts);
504 1
            if (NULL !== $slug && !array_key_exists($slug, $translations)) {
505 1
                $translations[$slug] = $key;
506 1
                file_put_contents($absoluteTranslationFileName, "\$translations[\"{$slug}\"] = _(\"{$slug}\");\n", FILE_APPEND);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
507 1
            }
508 1
            $this->slugs[$slug] = $key;
509 1
            $info["slug"] = $slug;
510 1
        }
511 1
    }
512
513
    /**
514
     * Create translation file if not exists
515
     *
516
     * @param string $absoluteTranslationFileName
517
     *
518
     * @return array
519
     */
520 1
    private function generateTranslationsFile($absoluteTranslationFileName)
521
    {
522 1
        $translations = array();
523 1
        if (file_exists($absoluteTranslationFileName)) {
524
            include($absoluteTranslationFileName);
525
        } else {
526 1
            Cache::getInstance()->storeData($absoluteTranslationFileName, "<?php \$translations = array();\n", Cache::TEXT, TRUE);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
527
        }
528
529 1
        return $translations;
530
    }
531
532
}
533