Completed
Push — master ( 01951f...468ae1 )
by Filip
02:11
created

TranslationExtension::loadResourcesFromDirs()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.439
c 0
b 0
f 0
cc 6
eloc 16
nc 4
nop 1
1
<?php
2
3
/**
4
 * This file is part of the Kdyby (http://www.kdyby.org)
5
 *
6
 * Copyright (c) 2008 Filip Procházka ([email protected])
7
 *
8
 * For the full copyright and license information, please view the file license.txt that was distributed with this source code.
9
 */
10
11
namespace Kdyby\Translation\DI;
12
13
use Kdyby;
14
use Kdyby\Translation\InvalidResourceException;
15
use Nette;
16
use Nette\DI\Statement;
17
use Nette\PhpGenerator as Code;
18
use Nette\Reflection;
19
use Nette\Utils\Callback;
20
use Nette\Utils\Finder;
21
use Nette\Utils\Strings;
22
use Nette\Utils\Validators;
23
use Symfony\Component\Translation\Loader\LoaderInterface;
24
use Tracy;
25
26
27
28
/**
29
 * @author Filip Procházka <[email protected]>
30
 */
31
class TranslationExtension extends Nette\DI\CompilerExtension
32
{
33
34
	/** @deprecated */
35
	const LOADER_TAG = self::TAG_LOADER;
36
	/** @deprecated */
37
	const DUMPER_TAG = self::TAG_DUMPER;
38
	/** @deprecated */
39
	const EXTRACTOR_TAG = self::TAG_EXTRACTOR;
40
41
	const TAG_LOADER = 'translation.loader';
42
	const TAG_DUMPER = 'translation.dumper';
43
	const TAG_EXTRACTOR = 'translation.extractor';
44
45
	const RESOLVER_REQUEST = 'request';
46
	const RESOLVER_HEADER = 'header';
47
	const RESOLVER_SESSION = 'session';
48
49
	/**
50
	 * @var array
51
	 */
52
	public $defaults = [
53
		'whitelist' => NULL, // array('cs', 'en'),
0 ignored issues
show
Unused Code Comprehensibility introduced by
78% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
54
		'default' => 'en',
55
		'logging' => NULL, //  TRUE for psr/log, or string for kdyby/monolog channel
56
		// 'fallback' => array('en_US', 'en'), // using custom merge strategy becase Nette's config merger appends lists of values
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
57
		'dirs' => ['%appDir%/lang', '%appDir%/locale'],
58
		'cache' => 'Kdyby\Translation\Caching\PhpFileStorage',
59
		'debugger' => '%debugMode%',
60
		'resolvers' => [
61
			self::RESOLVER_SESSION => FALSE,
62
			self::RESOLVER_REQUEST => TRUE,
63
			self::RESOLVER_HEADER => TRUE,
64
		],
65
		'loaders' => []
66
	];
67
68
	/**
69
	 * @var array
70
	 */
71
	private $loaders;
72
73
74
75
	public function __construct()
76
	{
77
		$this->defaults['cache'] = new Statement($this->defaults['cache'], ['%tempDir%/cache']);
78
	}
79
80
81
82
	public function loadConfiguration()
83
	{
84
		$this->loaders = [];
85
86
		$builder = $this->getContainerBuilder();
87
		$config = $this->getConfig();
88
89
		$translator = $builder->addDefinition($this->prefix('default'))
90
			->setClass('Kdyby\Translation\Translator', [$this->prefix('@userLocaleResolver')])
91
			->addSetup('?->setTranslator(?)', [$this->prefix('@userLocaleResolver.param'), '@self'])
92
			->addSetup('setDefaultLocale', [$config['default']])
93
			->addSetup('setLocaleWhitelist', [$config['whitelist']]);
94
95
		Validators::assertField($config, 'fallback', 'list');
96
		$translator->addSetup('setFallbackLocales', [$config['fallback']]);
97
98
		$catalogueCompiler = $builder->addDefinition($this->prefix('catalogueCompiler'))
99
			->setClass('Kdyby\Translation\CatalogueCompiler', self::filterArgs($config['cache']));
100
101
		if ($config['debugger'] && interface_exists('Tracy\IBarPanel')) {
102
			$builder->addDefinition($this->prefix('panel'))
103
				->setClass('Kdyby\Translation\Diagnostics\Panel', [dirname($builder->expand('%appDir%'))])
104
				->addSetup('setLocaleWhitelist', [$config['whitelist']]);
105
106
			$translator->addSetup('?->register(?)', [$this->prefix('@panel'), '@self']);
107
			$catalogueCompiler->addSetup('enableDebugMode');
108
		}
109
110
		$this->loadLocaleResolver($config);
111
112
		$builder->addDefinition($this->prefix('helpers'))
113
			->setClass('Kdyby\Translation\TemplateHelpers')
114
			->setFactory($this->prefix('@default') . '::createTemplateHelpers');
115
116
		$builder->addDefinition($this->prefix('fallbackResolver'))
117
			->setClass('Kdyby\Translation\FallbackResolver');
118
119
		$builder->addDefinition($this->prefix('catalogueFactory'))
120
			->setClass('Kdyby\Translation\CatalogueFactory');
121
122
		$builder->addDefinition($this->prefix('selector'))
123
			->setClass('Symfony\Component\Translation\MessageSelector');
124
125
		$builder->addDefinition($this->prefix('extractor'))
126
			->setClass('Symfony\Component\Translation\Extractor\ChainExtractor');
127
128
		$this->loadExtractors();
129
130
		$builder->addDefinition($this->prefix('writer'))
131
			->setClass('Symfony\Component\Translation\Writer\TranslationWriter');
132
133
		$this->loadDumpers();
134
135
		$builder->addDefinition($this->prefix('loader'))
136
			->setClass('Kdyby\Translation\TranslationLoader');
137
138
		$loaders = $this->loadFromFile(__DIR__ . '/config/loaders.neon');
139
		$this->loadLoaders($loaders, $config['loaders'] ? : array_keys($loaders));
140
141
		if ($this->isRegisteredConsoleExtension()) {
142
			$this->loadConsole($config);
143
		}
144
	}
145
146
147
148
	protected function loadLocaleResolver(array $config)
149
	{
150
		$builder = $this->getContainerBuilder();
151
152
		$builder->addDefinition($this->prefix('userLocaleResolver.param'))
153
			->setClass('Kdyby\Translation\LocaleResolver\LocaleParamResolver')
154
			->setAutowired(FALSE);
155
156
		$builder->addDefinition($this->prefix('userLocaleResolver.acceptHeader'))
157
			->setClass('Kdyby\Translation\LocaleResolver\AcceptHeaderResolver');
158
159
		$builder->addDefinition($this->prefix('userLocaleResolver.session'))
160
			->setClass('Kdyby\Translation\LocaleResolver\SessionResolver');
161
162
		$chain = $builder->addDefinition($this->prefix('userLocaleResolver'))
163
			->setClass('Kdyby\Translation\IUserLocaleResolver')
164
			->setFactory('Kdyby\Translation\LocaleResolver\ChainResolver');
165
166
		$resolvers = [];
167 View Code Duplication
		if ($config['resolvers'][self::RESOLVER_HEADER]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
168
			$resolvers[] = $this->prefix('@userLocaleResolver.acceptHeader');
169
			$chain->addSetup('addResolver', [$this->prefix('@userLocaleResolver.acceptHeader')]);
170
		}
171
172 View Code Duplication
		if ($config['resolvers'][self::RESOLVER_REQUEST]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
			$resolvers[] = $this->prefix('@userLocaleResolver.param');
174
			$chain->addSetup('addResolver', [$this->prefix('@userLocaleResolver.param')]);
175
		}
176
177 View Code Duplication
		if ($config['resolvers'][self::RESOLVER_SESSION]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
178
			$resolvers[] = $this->prefix('@userLocaleResolver.session');
179
			$chain->addSetup('addResolver', [$this->prefix('@userLocaleResolver.session')]);
180
		}
181
182
		if ($config['debugger'] && interface_exists('Tracy\IBarPanel')) {
183
			$builder->getDefinition($this->prefix('panel'))
184
				->addSetup('setLocaleResolvers', [array_reverse($resolvers)]);
185
		}
186
	}
187
188
189
190
	protected function loadConsole(array $config)
191
	{
192
		$builder = $this->getContainerBuilder();
193
194
		Validators::assertField($config, 'dirs', 'list');
195
		$builder->addDefinition($this->prefix('console.extract'))
196
			->setClass('Kdyby\Translation\Console\ExtractCommand')
197
			->addSetup('$defaultOutputDir', [reset($config['dirs'])])
198
			->addTag('kdyby.console.command', 'latte');
199
	}
200
201
202
203 View Code Duplication
	protected function loadDumpers()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
	{
205
		$builder = $this->getContainerBuilder();
206
207
		foreach ($this->loadFromFile(__DIR__ . '/config/dumpers.neon') as $format => $class) {
208
			$builder->addDefinition($this->prefix('dumper.' . $format))
209
				->setClass($class)
210
				->addTag(self::TAG_DUMPER, $format);
211
		}
212
	}
213
214
215
216
	protected function loadLoaders(array $loaders, array $allowed)
217
	{
218
		$builder = $this->getContainerBuilder();
219
220
		foreach ($loaders as $format => $class) {
221
			if (array_search($format, $allowed) === FALSE) {
222
				continue;
223
			}
224
			$builder->addDefinition($this->prefix('loader.' . $format))
225
				->setClass($class)
226
				->addTag(self::TAG_LOADER, $format);
227
		}
228
	}
229
230
231
232 View Code Duplication
	protected function loadExtractors()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
233
	{
234
		$builder = $this->getContainerBuilder();
235
236
		foreach ($this->loadFromFile(__DIR__ . '/config/extractors.neon') as $format => $class) {
237
			$builder->addDefinition($this->prefix('extractor.' . $format))
238
				->setClass($class)
239
				->addTag(self::TAG_EXTRACTOR, $format);
240
		}
241
	}
242
243
244
245
	public function beforeCompile()
246
	{
247
		$builder = $this->getContainerBuilder();
248
		$config = $this->getConfig();
249
250
		$this->beforeCompileLogging($config);
251
252
		$registerToLatte = function (Nette\DI\ServiceDefinition $def) {
253
			$def->addSetup('?->onCompile[] = function($engine) { Kdyby\Translation\Latte\TranslateMacros::install($engine->getCompiler()); }', ['@self']);
254
255
			$def->addSetup('addProvider', ['translator', $this->prefix('@default')])
256
				->addSetup('addFilter', ['translate', [$this->prefix('@helpers'), 'translateFilterAware']]);
257
		};
258
259
		$latteFactoryService = $builder->getByType('Nette\Bridges\ApplicationLatte\ILatteFactory');
260
		if (!$latteFactoryService || !self::isOfType($builder->getDefinition($latteFactoryService)->getClass(), 'Latte\engine')) {
261
			$latteFactoryService = 'nette.latteFactory';
262
		}
263
264
		if ($builder->hasDefinition($latteFactoryService) && self::isOfType($builder->getDefinition($latteFactoryService)->getClass(), 'Latte\Engine')) {
265
			$registerToLatte($builder->getDefinition($latteFactoryService));
266
		}
267
268
		if ($builder->hasDefinition('nette.latte')) {
269
			$registerToLatte($builder->getDefinition('nette.latte'));
270
		}
271
272
		$applicationService = $builder->getByType('Nette\Application\Application') ?: 'application';
273
		if ($builder->hasDefinition($applicationService)) {
274
			$builder->getDefinition($applicationService)
275
				->addSetup('$service->onRequest[] = ?', [[$this->prefix('@userLocaleResolver.param'), 'onRequest']]);
276
277
			if ($config['debugger'] && interface_exists('Tracy\IBarPanel')) {
278
				$builder->getDefinition($applicationService)
279
					->addSetup('$self = $this; $service->onStartup[] = function () use ($self) { $self->getService(?); }', [$this->prefix('default')])
280
					->addSetup('$service->onRequest[] = ?', [[$this->prefix('@panel'), 'onRequest']]);
281
			}
282
		}
283
284
		if (class_exists('Tracy\Debugger')) {
285
			Kdyby\Translation\Diagnostics\Panel::registerBluescreen();
286
		}
287
288
		$extractor = $builder->getDefinition($this->prefix('extractor'));
289 View Code Duplication
		foreach ($builder->findByTag(self::TAG_EXTRACTOR) as $extractorId => $meta) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
290
			Validators::assert($meta, 'string:2..');
291
292
			$extractor->addSetup('addExtractor', [$meta, '@' . $extractorId]);
293
294
			$builder->getDefinition($extractorId)->setAutowired(FALSE);
295
		}
296
297
		$writer = $builder->getDefinition($this->prefix('writer'));
298 View Code Duplication
		foreach ($builder->findByTag(self::TAG_DUMPER) as $dumperId => $meta) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
			Validators::assert($meta, 'string:2..');
300
301
			$writer->addSetup('addDumper', [$meta, '@' . $dumperId]);
302
303
			$builder->getDefinition($dumperId)->setAutowired(FALSE);
304
		}
305
306
		$this->loaders = [];
307
		foreach ($builder->findByTag(self::TAG_LOADER) as $loaderId => $meta) {
308
			Validators::assert($meta, 'string:2..');
309
			$builder->getDefinition($loaderId)->setAutowired(FALSE);
310
			$this->loaders[$meta] = $loaderId;
311
		}
312
313
		$builder->getDefinition($this->prefix('loader'))
314
			->addSetup('injectServiceIds', [$this->loaders]);
315
316
		foreach ($this->compiler->getExtensions() as $extension) {
317
			if (!$extension instanceof ITranslationProvider) {
318
				continue;
319
			}
320
321
			$config['dirs'] = array_merge($config['dirs'], array_values($extension->getTranslationResources()));
322
		}
323
324
		if ($dirs = array_values(array_filter($config['dirs'], Callback::closure('is_dir')))) {
325
			foreach ($dirs as $dir) {
326
				$builder->addDependency($dir);
327
			}
328
329
			$this->loadResourcesFromDirs($dirs);
330
		}
331
	}
332
333
334
335
	protected function beforeCompileLogging(array $config)
336
	{
337
		$builder = $this->getContainerBuilder();
338
		$translator = $builder->getDefinition($this->prefix('default'));
339
340
		if ($config['logging'] === TRUE) {
341
			$translator->addSetup('injectPsrLogger');
342
343
		} elseif (is_string($config['logging'])) { // channel for kdyby/monolog
344
			$translator->addSetup('injectPsrLogger', [
345
				new Statement('@Kdyby\Monolog\Logger::channel', [$config['logging']]),
346
			]);
347
348
		} elseif ($config['logging'] !== NULL) {
349
			throw new Kdyby\Translation\InvalidArgumentException(sprintf(
350
				"Invalid config option for logger. Valid are TRUE for general psr/log or string for kdyby/monolog channel, but %s was given",
351
				$config['logging']
352
			));
353
		}
354
	}
355
356
357
358
	protected function loadResourcesFromDirs($dirs)
359
	{
360
		$builder = $this->getContainerBuilder();
361
		$config = $this->getConfig();
362
363
		$whitelistRegexp = Kdyby\Translation\Translator::buildWhitelistRegexp($config['whitelist']);
364
		$translator = $builder->getDefinition($this->prefix('default'));
365
366
		$mask = array_map(function ($value) {
367
			return '*.*.' . $value;
368
		}, array_keys($this->loaders));
369
370
		foreach (Finder::findFiles($mask)->from($dirs) as $file) {
371
			/** @var \SplFileInfo $file */
372
			if (!$m = Strings::match($file->getFilename(), '~^(?P<domain>.*?)\.(?P<locale>[^\.]+)\.(?P<format>[^\.]+)$~')) {
373
				continue;
374
			}
375
376
			if ($whitelistRegexp && !preg_match($whitelistRegexp, $m['locale']) && $builder->parameters['productionMode']) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $whitelistRegexp of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
377
				continue; // ignore in production mode, there is no need to pass the ignored resources
378
			}
379
380
			$this->validateResource($m['format'], $file->getPathname(), $m['locale'], $m['domain']);
381
			$translator->addSetup('addResource', [$m['format'], $file->getPathname(), $m['locale'], $m['domain']]);
382
			$builder->addDependency($file->getPathname());
383
		}
384
	}
385
386
387
388
	/**
389
	 * @param string $format
390
	 * @param string $file
391
	 * @param string $locale
392
	 * @param string $domain
393
	 */
394
	protected function validateResource($format, $file, $locale, $domain)
395
	{
396
		$builder = $this->getContainerBuilder();
397
398
		if (!isset($this->loaders[$format])) {
399
			return;
400
		}
401
402
		try {
403
			$def = $builder->getDefinition($this->loaders[$format]);
404
			$refl = Reflection\ClassType::from($def->getEntity() ?: $def->getClass());
405
			if (($method = $refl->getConstructor()) && $method->getNumberOfRequiredParameters() > 1) {
406
				return;
407
			}
408
409
			$loader = $refl->newInstance();
410
			if (!$loader instanceof LoaderInterface) {
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Transl...\Loader\LoaderInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
411
				return;
412
			}
413
414
		} catch (\ReflectionException $e) {
415
			return;
416
		}
417
418
		try {
419
			$loader->load($file, $locale, $domain);
420
421
		} catch (\Exception $e) {
422
			throw new InvalidResourceException("Resource $file is not valid and cannot be loaded.", 0, $e);
423
		}
424
	}
425
426
427
428
	public function afterCompile(Code\ClassType $class)
429
	{
430
		$initialize = $class->getMethod('initialize');
431
		if (class_exists('Tracy\Debugger')) {
432
			$initialize->addBody('Kdyby\Translation\Diagnostics\Panel::registerBluescreen();');
433
		}
434
	}
435
436
437
438
	/**
439
	 * {@inheritdoc}
440
	 */
441
	public function getConfig(array $defaults = NULL, $expand = TRUE)
442
	{
443
		return parent::getConfig($this->defaults) + ['fallback' => ['en_US']];
444
	}
445
446
447
448
	private function isRegisteredConsoleExtension()
449
	{
450
		foreach ($this->compiler->getExtensions() as $extension) {
451
			if ($extension instanceof Kdyby\Console\DI\ConsoleExtension) {
0 ignored issues
show
Bug introduced by
The class Kdyby\Console\DI\ConsoleExtension does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
452
				return TRUE;
453
			}
454
		}
455
456
		return FALSE;
457
	}
458
459
460
461
	/**
462
	 * @param \Nette\Configurator $configurator
463
	 */
464
	public static function register(Nette\Configurator $configurator)
465
	{
466
		$configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) {
467
			$compiler->addExtension('translation', new TranslationExtension());
468
		};
469
	}
470
471
472
473
	/**
474
	 * @param string|\stdClass $statement
475
	 * @return Nette\DI\Statement[]
476
	 */
477
	protected static function filterArgs($statement)
478
	{
479
		return \Nette\DI\Helpers::filterArguments([is_string($statement) ? new Nette\DI\Statement($statement) : $statement]);
480
	}
481
482
483
484
	/**
485
	 * @param string $class
486
	 * @param string $type
487
	 * @return bool
488
	 */
489
	private static function isOfType($class, $type)
490
	{
491
		return $class === $type || is_subclass_of($class, $type);
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $type can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
492
	}
493
494
}
495