I18nExtension   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 129
c 1
b 0
f 0
dl 0
loc 251
rs 10
wmc 20

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getConfigSchema() 0 51 3
A registerTranslationBridgeFeatures() 0 29 3
A __construct() 0 8 2
A resolveVendorDir() 0 19 4
B loadConfiguration() 0 77 5
A beforeCompile() 0 7 2
A createProfile() 0 9 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SixtyEightPublishers\i18n\DI;
6
7
use Tracy\Bar;
8
use ReflectionClass;
9
use Tracy\IBarPanel;
10
use Nette\Schema\Expect;
11
use Nette\Schema\Schema;
12
use ReflectionException;
13
use Nette\DI\CompilerExtension;
14
use Composer\Autoload\ClassLoader;
15
use Nette\PhpGenerator\PhpLiteral;
16
use Nette\DI\Definitions\Statement;
17
use SixtyEightPublishers\i18n\Profile\Profile;
18
use SixtyEightPublishers\i18n\ProfileProvider;
19
use SixtyEightPublishers\i18n\Diagnostics\Panel;
20
use SixtyEightPublishers\i18n\Lists\ListOptions;
21
use SixtyEightPublishers\i18n\Lists\LanguageList;
22
use SixtyEightPublishers\i18n\Profile\ActiveProfile;
23
use SixtyEightPublishers\i18n\ProfileProviderInterface;
24
use SixtyEightPublishers\i18n\Detector\DetectorInterface;
25
use SixtyEightPublishers\i18n\Exception\RuntimeException;
26
use SixtyEightPublishers\i18n\Detector\NetteRequestDetector;
27
use SixtyEightPublishers\i18n\Storage\SessionProfileStorage;
28
use SixtyEightPublishers\i18n\Storage\ProfileStorageInterface;
29
use SixtyEightPublishers\i18n\ProfileContainer\ProfileContainer;
30
use SixtyEightPublishers\i18n\Exception\InvalidArgumentException;
31
use SixtyEightPublishers\i18n\Profile\ActiveProfileChangeNotifier;
32
use SixtyEightPublishers\i18n\Translation\TranslatorLocaleResolver;
33
use SixtyEightPublishers\i18n\ProfileContainer\ProfileContainerInterface;
34
use SixtyEightPublishers\TranslationBridge\DI\AbstractTranslationBridgeExtension;
35
use SixtyEightPublishers\TranslationBridge\Localization\TranslatorLocalizerInterface;
36
use SixtyEightPublishers\TranslationBridge\Localization\TranslatorLocaleResolverInterface;
37
38
final class I18nExtension extends CompilerExtension
39
{
40
	public const DEFAULT_PROFILE_NAME = 'default';
41
42
	/** @var bool  */
43
	private $debugMode;
44
45
	/** @var string  */
46
	private $vendorDir;
47
48
	/**
49
	 * @param bool        $debugMode
50
	 * @param string|NULL $vendorDir
51
	 */
52
	public function __construct(bool $debugMode = FALSE, ?string $vendorDir = NULL)
53
	{
54
		if (0 >= func_num_args()) {
55
			throw new InvalidArgumentException(sprintf('Provide Debug mode, e.q. %s(%%consoleMode%%).', static::class));
56
		}
57
58
		$this->debugMode = $debugMode;
59
		$this->vendorDir = $this->resolveVendorDir($vendorDir);
60
	}
61
62
	/**
63
	 * @return \Nette\Schema\Schema
64
	 */
65
	public function getConfigSchema(): Schema
66
	{
67
		$profileAttributeExpectationFactory = static function (string $attribute) {
68
			return Expect::anyOf(Expect::string(), Expect::arrayOf('string'))
69
				->required()
70
				->castTo('array')
71
				->assert(static function (array $value) {
72
					return !empty($value);
73
				}, sprintf('Almost one %s must be defined.', $attribute));
74
		};
75
76
		$schema = Expect::structure([
77
			'profiles' => Expect::arrayOf(Expect::structure([
78
				'language' => $profileAttributeExpectationFactory('language'),
79
				'currency' => $profileAttributeExpectationFactory('currency'),
80
				'country' => $profileAttributeExpectationFactory('country'),
81
				'domain' => Expect::anyOf(Expect::string(), Expect::arrayOf('string'))->default([])->castTo('array'),
82
				'enabled' => Expect::bool(TRUE),
83
			])),
84
85
			'lists' => Expect::structure([
86
				'fallback_language' => Expect::string('en'),
87
				'default_language' => Expect::string()->nullable(),
88
			]),
89
90
			'storage' => Expect::anyOf(Expect::string(), Expect::type(Statement::class))
91
				->default(SessionProfileStorage::class)
92
				->before(static function ($def) {
93
					return $def instanceof Statement ? $def : new Statement($def);
94
				}),
95
96
			'detector' => Expect::anyOf(Expect::string(), Expect::type(Statement::class))
97
				->default(NetteRequestDetector::class)
98
				->before(static function ($def) {
99
					return $def instanceof Statement ? $def : new Statement($def);
100
				}),
101
102
			'translation_bridge' => Expect::structure([
103
				'locale_resolver' => Expect::structure([
104
					'enabled' => Expect::bool(FALSE),
105
					'use_default' => Expect::bool(FALSE),
106
					'priority' => Expect::int(15),
107
				]),
108
			]),
109
		]);
110
111
		$schema->assert(static function ($schema) {
112
			return !empty($schema->profiles);
113
		}, 'You must define almost one profile in your configuration.');
114
115
		return $schema;
116
	}
117
118
	/**
119
	 * {@inheritdoc}
120
	 */
121
	public function loadConfiguration(): void
122
	{
123
		$builder = $this->getContainerBuilder();
124
125
		# ActiveProfile Change Notifier
126
		$builder->addDefinition($this->prefix('active_profile_change_notifier'))
127
			->setType(ActiveProfileChangeNotifier::class);
128
129
		# Register profile's storage
130
		$builder->addDefinition($this->prefix('storage'))
131
			->setType(ProfileStorageInterface::class)
132
			->setFactory($this->config->storage)
133
			->setAutowired(FALSE);
134
135
		# Register profile's detector
136
		$builder->addDefinition($this->prefix('detector'))
137
			->setType(DetectorInterface::class)
138
			->setFactory($this->config->detector)
139
			->setAutowired(FALSE);
140
141
		# Create default Profile
142
		$profiles = $this->config->profiles;
143
144
		if (isset($profiles[self::DEFAULT_PROFILE_NAME])) {
145
			$defaultProfile = $this->createProfile(self::DEFAULT_PROFILE_NAME, $profiles[self::DEFAULT_PROFILE_NAME]);
146
			unset($profiles[self::DEFAULT_PROFILE_NAME]);
147
		} else {
148
			$defaultProfile = $this->createProfile((string) key($profiles), array_shift($profiles));
149
		}
150
151
		# Register container
152
		$builder->addDefinition($this->prefix('profile_container'))
153
			->setType(ProfileContainerInterface::class)
154
			->setFactory(ProfileContainer::class, [
155
				'defaultProfile' => $defaultProfile,
156
				'profiles' => array_map(function ($config, $key) {
157
					return $this->createProfile((string) $key, $config);
158
				}, $profiles, array_keys($profiles)),
159
			])
160
			->setAutowired(FALSE);
161
162
		# Register profile provider
163
		$builder->addDefinition($this->prefix('profile_provider'))
164
			->setType(ProfileProviderInterface::class)
165
			->setFactory(ProfileProvider::class, [
166
				$this->prefix('@detector'),
167
				$this->prefix('@storage'),
168
				$this->prefix('@profile_container'),
169
			]);
170
171
		# Register lists
172
		$builder->addDefinition($this->prefix('list_options'))
173
			->setType(ListOptions::class)
174
			->setArguments([
175
				'vendorDir' => $this->vendorDir,
176
				'fallbackLanguage' => $this->config->lists->fallback_language,
177
				'defaultLanguage' => $this->config->lists->default_language,
178
			])
179
			->setAutowired(FALSE);
180
181
		$builder->addDefinition($this->prefix('list.language'))
182
			->setType(LanguageList::class)
183
			->setArguments([
184
				'options' => $this->prefix('@list_options'),
185
			]);
186
187
		# Register tracy panel
188
		if ($this->debugMode && interface_exists(IBarPanel::class) && class_exists(Bar::class)) {
189
			$builder->addDefinition($this->prefix('tracy_panel'))
190
				->setType(Panel::class)
191
				->setArguments([
192
					'profileContainer' => $this->prefix('@profile_container'),
193
				])
194
				->setAutowired(FALSE);
195
		}
196
197
		$this->registerTranslationBridgeFeatures();
198
	}
199
200
	/**
201
	 * {@inheritdoc}
202
	 */
203
	public function beforeCompile(): void
204
	{
205
		$builder = $this->getContainerBuilder();
206
207
		if (TRUE === $builder->hasDefinition($this->prefix('tracy_panel'))) {
208
			$builder->getDefinitionByType(Bar::class)
209
				->addSetup('addPanel', [$this->prefix('@tracy_panel')]);
210
		}
211
	}
212
213
	/**
214
	 * @return void
215
	 */
216
	private function registerTranslationBridgeFeatures(): void
217
	{
218
		if (empty($this->compiler->getExtensions(AbstractTranslationBridgeExtension::class))) {
219
			return;
220
		}
221
222
		$builder = $this->getContainerBuilder();
223
224
		# Automatically change the Translator's locale when the ActiveProfile's locale is changed
225
		$builder->getDefinition($this->prefix('active_profile_change_notifier'))
226
			->addSetup('$service->addOnLanguageChangeListener(function (? $profile) { $this->getByType(?::class)->setLocale($profile->language); })', [
227
				new PhpLiteral(ActiveProfile::class),
228
				new PhpLiteral(TranslatorLocalizerInterface::class),
229
			]);
230
231
		$localeResolverConfig = $this->config->translation_bridge->locale_resolver;
232
233
		if (!$localeResolverConfig->enabled) {
234
			return;
235
		}
236
237
		# Add Translator Locale Resolver
238
		$builder->addDefinition($this->prefix('translator_locale_resolver'))
239
			->setType(TranslatorLocaleResolverInterface::class)
240
			->setFactory(TranslatorLocaleResolver::class, [
241
				'useDefault' => $localeResolverConfig->use_default,
242
			])
243
			->setAutowired(FALSE)
244
			->addTag(AbstractTranslationBridgeExtension::TAG_TRANSLATOR_LOCALE_RESOLVER, $localeResolverConfig->priority);
245
	}
246
247
	/**
248
	 * @param string $name
249
	 * @param object $config
250
	 *
251
	 * @return \Nette\DI\Definitions\Statement
252
	 */
253
	private function createProfile(string $name, object $config): Statement
254
	{
255
		return new Statement(Profile::class, [
256
			'name' => $name,
257
			'languages' => $config->language,
258
			'countries' => $config->country,
259
			'currencies' => $config->currency,
260
			'domains' => $config->domain,
261
			'enabled' => $config->enabled,
262
		]);
263
	}
264
265
	/**
266
	 * @param string|NULL $vendorDir
267
	 *
268
	 * @return string
269
	 */
270
	private function resolveVendorDir(?string $vendorDir = NULL): string
271
	{
272
		if (NULL !== $vendorDir) {
273
			return $vendorDir;
274
		}
275
276
		if (!class_exists(ClassLoader::class)) {
277
			throw new RuntimeException(sprintf(
278
				'Vendor directory can\'t be detected because the class %s can\'t be found. Please provide the vendor directory manually.',
279
				ClassLoader::class
280
			));
281
		}
282
283
		try {
284
			$reflection = new ReflectionClass(ClassLoader::class);
285
286
			return dirname($reflection->getFileName(), 2);
287
		} catch (ReflectionException $e) {
288
			throw new RuntimeException('Vendor directory can\'t be detected. Please provide the vendor directory manually.', 0, $e);
289
		}
290
	}
291
}
292