Completed
Push — master ( 9f8cf8...431e4e )
by Tomáš
06:21
created

DoctrineExtension   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 67.44%

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 2
dl 0
loc 421
ccs 145
cts 215
cp 0.6744
rs 8.72
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A processSecondLevelCache() 0 44 3
A parseConfig() 0 26 5
B getCache() 0 46 6
D loadConfiguration() 0 100 10
A beforeCompile() 0 17 2
A afterCompile() 0 22 4
A registerCommandsIntoConsole() 0 29 4
A processDbalTypes() 0 12 2
A processDbalTypeOverrides() 0 9 2
A processEventSubscribers() 0 17 3
A processFilters() 0 9 2
A hasSymfonyConsole() 0 4 1
A hasEventManager() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like DoctrineExtension 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 DoctrineExtension, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Portiny\Doctrine\Adapter\Nette\DI;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\AnnotationRegistry;
9
use Doctrine\Common\Annotations\CachedReader;
10
use Doctrine\Common\Annotations\Reader;
11
use Doctrine\Common\Cache\ArrayCache;
12
use Doctrine\Common\Cache\RedisCache;
13
use Doctrine\Common\EventManager;
14
use Doctrine\Common\EventSubscriber;
15
use Doctrine\DBAL\Connection;
16
use Doctrine\DBAL\Tools\Console\Command\ImportCommand;
17
use Doctrine\ORM\Cache\CacheConfiguration;
18
use Doctrine\ORM\Cache\CacheFactory;
19
use Doctrine\ORM\Cache\DefaultCacheFactory;
20
use Doctrine\ORM\Cache\Logging\CacheLogger;
21
use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
22
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
23
use Doctrine\ORM\Cache\RegionsConfiguration;
24
use Doctrine\ORM\Configuration;
25
use Doctrine\ORM\EntityManager;
26
use Doctrine\ORM\EntityManagerInterface;
27
use Doctrine\ORM\EntityRepository;
28
use Doctrine\ORM\Events;
29
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
30
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
31
use Doctrine\ORM\Query\Filter\SQLFilter;
32
use Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand;
33
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand;
34
use Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand;
35
use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand;
36
use Doctrine\ORM\Tools\Console\Command\GenerateEntitiesCommand;
37
use Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand;
38
use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand;
39
use Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand;
40
use Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand;
41
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
42
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
43
use Doctrine\ORM\Tools\ResolveTargetEntityListener;
44
use Nette\DI\CompilerExtension;
45
use Nette\DI\ContainerBuilder;
46
use Nette\DI\ServiceDefinition;
47
use Nette\DI\Statement;
48
use Nette\PhpGenerator\ClassType;
49
use Nette\PhpGenerator\PhpLiteral;
50
use Nette\Utils\AssertionException;
51
use Nette\Utils\Validators;
52
use Portiny\Doctrine\Adapter\Nette\Tracy\DoctrineSQLPanel;
53
use Portiny\Doctrine\Cache\DefaultCache;
54
use Portiny\Doctrine\Contract\Provider\ClassMappingProviderInterface;
55
use Portiny\Doctrine\Contract\Provider\EntitySourceProviderInterface;
56
use Symfony\Component\Console\Application;
57
use Symfony\Component\Console\Helper\HelperSet;
58
59
class DoctrineExtension extends CompilerExtension
60
{
61
	/**
62
	 * @var string
63
	 */
64
	private const DOCTRINE_SQL_PANEL = DoctrineSQLPanel::class;
65
66
	private $classMappings = [];
67
68
	private $entitySources = [];
69
70
	/**
71
	 * @var array
72
	 */
73
	private static $defaults = [
74
		'debug' => '%debugMode%',
75
		'dbal' => [
76
			'type_overrides' => [],
77
			'types' => [],
78
			'schema_filter' => null,
79
		],
80
		'prefix' => 'doctrine.default',
81
		'proxyDir' => '%tempDir%/cache/proxies',
82
		'proxyNamespace' => 'DoctrineProxies',
83
		'sourceDir' => null,
84
		'entityManagerClassName' => EntityManager::class,
85
		'defaultRepositoryClassName' => EntityRepository::class,
86
		'repositoryFactory' => null,
87
		'namingStrategy' => UnderscoreNamingStrategy::class,
88
		'sqlLogger' => null,
89
		'targetEntityMappings' => [],
90
		'metadata' => [],
91
		'functions' => [],
92
		// caches
93
		'metadataCache' => 'default',
94
		'queryCache' => 'default',
95
		'resultCache' => 'default',
96
		'hydrationCache' => 'default',
97
		'secondLevelCache' => [
98
			'enabled' => false,
99
			'factoryClass' => DefaultCacheFactory::class,
100
			'driver' => 'default',
101
			'regions' => [
102
				'defaultLifetime' => 3600,
103
				'defaultLockLifetime' => 60,
104
			],
105
			'fileLockRegionDirectory' => '%tempDir%/cache/Doctrine.Cache.Locks',
106
			'logging' => '%debugMode%',
107
		],
108
		'cache' => [
109
			'redis' => [
110
				'class' => RedisCache::class,
111
			],
112
		],
113
	];
114
115
	/**
116
	 * {@inheritdoc}
117
	 */
118 1
	public function loadConfiguration(): void
119
	{
120 1
		$config = $this->parseConfig();
121
122 1
		$builder = $this->getContainerBuilder();
123 1
		$name = $config['prefix'];
124
125 1
		$builder->addDefinition($name . '.namingStrategy')
126 1
			->setType($config['namingStrategy']);
127
128 1
		$configurationDefinition = $builder->addDefinition($name . '.config')
129 1
			->setType(Configuration::class)
130 1
			->addSetup('setFilterSchemaAssetsExpression', [$config['dbal']['schema_filter']])
131 1
			->addSetup('setDefaultRepositoryClassName', [$config['defaultRepositoryClassName']])
132 1
			->addSetup('setProxyDir', [$config['proxyDir']])
133 1
			->addSetup('setProxyNamespace', [$config['proxyNamespace']])
134 1
			->addSetup('setAutoGenerateProxyClasses', [$config['debug']])
135 1
			->addSetup('setNamingStrategy', ['@' . $name . '.namingStrategy']);
136
137 1
		$builder->addDefinition($name . '.annotationReader')
138 1
			->setType(AnnotationReader::class)
139 1
			->setAutowired(false);
140
141 1
		$metadataCache = $this->getCache($name . '.metadata', $builder, $config['metadataCache'] ?: 'array');
142 1
		$builder->addDefinition($name . '.reader')
143 1
			->setType(Reader::class)
144 1
			->setFactory(CachedReader::class, ['@' . $name . '.annotationReader', $metadataCache, $config['debug']]);
145
146 1
		$builder->addDefinition($name . '.annotationDriver')
147 1
			->setFactory(AnnotationDriver::class, ['@' . $name . '.reader', array_values($this->entitySources)]);
148
149 1
		$configurationDefinition->addSetup('setMetadataDriverImpl', ['@' . $name . '.annotationDriver']);
150
151 1
		foreach ($config['functions'] as $functionName => $function) {
152 1
			$configurationDefinition->addSetup('addCustomStringFunction', [$functionName, $function]);
153
		}
154
155 1
		if ($config['repositoryFactory']) {
156
			$builder->addDefinition($name . '.repositoryFactory')
157
				->setType($config['repositoryFactory']);
158
			$configurationDefinition->addSetup('setRepositoryFactory', ['@' . $name . '.repositoryFactory']);
159
		}
160 1
		if ($config['sqlLogger']) {
161
			$builder->addDefinition($name . '.sqlLogger')
162
				->setType($config['sqlLogger']);
163
			$configurationDefinition->addSetup('setSQLLogger', ['@' . $name . '.sqlLogger']);
164
		}
165
166 1
		if ($config['metadataCache'] !== false) {
167 1
			$configurationDefinition->addSetup(
168 1
				'setMetadataCacheImpl',
169 1
				[$this->getCache($name . '.metadata', $builder, $config['metadataCache'])]
170
			);
171
		}
172
173 1
		if ($config['queryCache'] !== false) {
174 1
			$configurationDefinition->addSetup(
175 1
				'setQueryCacheImpl',
176 1
				[$this->getCache($name . '.query', $builder, $config['queryCache'])]
177
			);
178
		}
179
180 1
		if ($config['resultCache'] !== false) {
181 1
			$configurationDefinition->addSetup(
182 1
				'setResultCacheImpl',
183 1
				[$this->getCache($name . '.ormResult', $builder, $config['resultCache'])]
184
			);
185
		}
186
187 1
		if ($config['hydrationCache'] !== false) {
188 1
			$configurationDefinition->addSetup(
189 1
				'setHydrationCacheImpl',
190 1
				[$this->getCache($name . '.hydration', $builder, $config['hydrationCache'])]
191
			);
192
		}
193
194 1
		$this->processSecondLevelCache($name, $config['secondLevelCache']);
195
196 1
		$builder->addDefinition($name . '.connection')
197 1
			->setType(Connection::class)
198 1
			->setFactory('@' . $name . '.entityManager::getConnection');
199
200 1
		$builder->addDefinition($name . '.entityManager')
201 1
			->setType($config['entityManagerClassName'])
202 1
			->setFactory(
203 1
				$config['entityManagerClassName'] . '::create',
204 1
				[$config['connection'], '@' . $name . '.config', '@Doctrine\Common\EventManager']
205
			);
206
207 1
		$builder->addDefinition($name . '.resolver')
208 1
			->setType(ResolveTargetEntityListener::class);
209
210 1
		if ($config['debug'] === true) {
211
			$builder->addDefinition($this->prefix($name . '.diagnosticsPanel'))
212
				->setType(self::DOCTRINE_SQL_PANEL);
213
		}
214
215
		// import Doctrine commands into Symfony/Console if exists
216 1
		$this->registerCommandsIntoConsole($builder, $name);
217 1
	}
218
219
	/**
220
	 * {@inheritdoc}
221
	 */
222 1
	public function beforeCompile(): void
223
	{
224 1
		$config = $this->getConfig(self::$defaults);
225 1
		$name = $config['prefix'];
226
227 1
		$builder = $this->getContainerBuilder();
228
229 1
		foreach ($this->classMappings as $source => $target) {
230
			$builder->getDefinition($name . '.resolver')
231
				->addSetup('addResolveTargetEntity', [$source, $target, []]);
232
		}
233
234 1
		$this->processDbalTypes($name, $config['dbal']['types']);
235 1
		$this->processDbalTypeOverrides($name, $config['dbal']['type_overrides']);
236 1
		$this->processEventSubscribers($name);
237 1
		$this->processFilters();
238 1
	}
239
240
	/**
241
	 * {@inheritdoc}
242
	 */
243 1
	public function afterCompile(ClassType $classType): void
244
	{
245 1
		$config = $this->getConfig(self::$defaults);
246 1
		$initialize = $classType->methods['initialize'];
247
248 1
		$initialize->addBody('?::registerUniqueLoader("class_exists");', [new PhpLiteral(AnnotationRegistry::class)]);
249
250 1
		if ($config['debug'] === true) {
251
			$initialize->addBody('$this->getByType(\'' . self::DOCTRINE_SQL_PANEL . '\')->bindToBar();');
252
		}
253
254 1
		$builder = $this->getContainerBuilder();
255 1
		$filterDefinitions = $builder->findByType(SQLFilter::class);
256 1
		if ($filterDefinitions !== []) {
257
			$initialize->addBody(
258
				'$filterCollection = $this->getByType(\'' . EntityManagerInterface::class . '\')->getFilters();'
259
			);
260
			foreach (array_keys($filterDefinitions) as $name) {
261
				$initialize->addBody('$filterCollection->enable(\'' . $name . '\');');
262
			}
263
		}
264 1
	}
265
266 1
	protected function processSecondLevelCache($name, array $config): void
267
	{
268 1
		if (! $config['enabled']) {
269 1
			return;
270
		}
271
272
		$builder = $this->getContainerBuilder();
273
274
		$cacheService = $this->getCache($name . '.secondLevel', $builder, $config['driver']);
275
276
		$builder->addDefinition($this->prefix($name . '.cacheFactory'))
277
			->setType(CacheFactory::class)
278
			->setFactory($config['factoryClass'], [
279
				$this->prefix('@' . $name . '.cacheRegionsConfiguration'),
280
				$cacheService,
281
			])
282
			->addSetup('setFileLockRegionDirectory', [$config['fileLockRegionDirectory']]);
283
284
		$builder->addDefinition($this->prefix($name . '.cacheRegionsConfiguration'))
285
			->setFactory(RegionsConfiguration::class, [
286
				$config['regions']['defaultLifetime'],
287
				$config['regions']['defaultLockLifetime'],
288
			]);
289
290
		$logger = $builder->addDefinition($this->prefix($name . '.cacheLogger'))
291
			->setType(CacheLogger::class)
292
			->setFactory(CacheLoggerChain::class)
293
			->setAutowired(false);
294
295
		if ($config['logging']) {
296
			$logger->addSetup('setLogger', ['statistics', new Statement(StatisticsCacheLogger::class)]);
297
		}
298
299
		$cacheConfigName = $this->prefix($name . '.ormCacheConfiguration');
300
		$builder->addDefinition($cacheConfigName)
301
			->setType(CacheConfiguration::class)
302
			->addSetup('setCacheFactory', [$this->prefix('@' . $name . '.cacheFactory')])
303
			->addSetup('setCacheLogger', [$this->prefix('@' . $name . '.cacheLogger')])
304
			->setAutowired(false);
305
306
		$configuration = $builder->getDefinitionByType(Configuration::class);
307
		$configuration->addSetup('setSecondLevelCacheEnabled');
308
		$configuration->addSetup('setSecondLevelCacheConfiguration', ['@' . $cacheConfigName]);
309
	}
310
311
	/**
312
	 * @throws AssertionException
313
	 */
314 1
	private function parseConfig(): array
315
	{
316 1
		$config = $this->getConfig(self::$defaults);
317 1
		$this->classMappings = $config['targetEntityMappings'];
318 1
		$this->entitySources = $config['metadata'];
319
320 1
		foreach ($this->compiler->getExtensions() as $extension) {
321 1
			if ($extension instanceof ClassMappingProviderInterface) {
322
				$entityMapping = $extension->getClassMapping();
323
				Validators::assert($entityMapping, 'array');
324
				$this->classMappings = array_merge($this->classMappings, $entityMapping);
325
			}
326
327 1
			if ($extension instanceof EntitySourceProviderInterface) {
328
				$entitySource = $extension->getEntitySource();
329
				Validators::assert($entitySource, 'array');
330 1
				$this->entitySources = array_merge($this->entitySources, $entitySource);
331
			}
332
		}
333
334 1
		if ($config['sourceDir']) {
335
			$this->entitySources[] = $config['sourceDir'];
336
		}
337
338 1
		return $config;
339
	}
340
341 1
	private function getCache(string $prefix, ContainerBuilder $containerBuilder, string $cacheType): string
342
	{
343 1
		if ($containerBuilder->hasDefinition($prefix . '.cache')) {
344 1
			return '@' . $prefix . '.cache';
345
		}
346
347 1
		$config = $this->parseConfig();
348
349
		switch ($cacheType) {
350 1
			case 'redis':
351
				$cacheClass = $config['cache']['redis']['class'];
352
				break;
353
354 1
			case 'array':
355
				$cacheClass = ArrayCache::class;
356
				break;
357
358 1
			case 'default':
359
			default:
360 1
				$cacheClass = DefaultCache::class;
361 1
				break;
362
		}
363
364 1
		$cacheDefinition = $containerBuilder->addDefinition($prefix . '.cache')
365 1
			->setType($cacheClass);
366
367 1
		if ($cacheType === 'redis') {
368
			$redisConfig = $config['cache']['redis'];
369
370
			$containerBuilder->addDefinition($prefix . '.redis')
371
				->setType('\Redis')
372
				->setAutowired(false)
373
				->addSetup('connect', [
374
					$redisConfig['host'] ?? '127.0.0.1',
375
					$redisConfig['port'] ?? null,
376
					$redisConfig['timeout'] ?? 0.0,
377
					$redisConfig['reserved'] ?? null,
378
					$redisConfig['retryInterval'] ?? 0,
379
				])
380
				->addSetup('select', [$redisConfig['database'] ?? 1]);
381
382
			$cacheDefinition->addSetup('setRedis', ['@' . $prefix . '.redis']);
383
		}
384
385 1
		return '@' . $prefix . '.cache';
386
	}
387
388 1
	private function registerCommandsIntoConsole(ContainerBuilder $containerBuilder, string $name): void
389
	{
390 1
		if ($this->hasSymfonyConsole()) {
391
			$commands = [
392 1
				ConvertMappingCommand::class,
393
				CreateCommand::class,
394
				DropCommand::class,
395
				GenerateEntitiesCommand::class,
396
				GenerateProxiesCommand::class,
397
				ImportCommand::class,
398
				MetadataCommand::class,
399
				QueryCommand::class,
400
				ResultCommand::class,
401
				UpdateCommand::class,
402
				ValidateSchemaCommand::class,
403
			];
404 1
			foreach ($commands as $index => $command) {
405 1
				$containerBuilder->addDefinition($name . '.command.' . $index)
406 1
					->setType($command);
407
			}
408
409 1
			$helperSets = $containerBuilder->findByType(HelperSet::class);
410 1
			if (! empty($helperSets)) {
411
				/** @var ServiceDefinition $helperSet */
412
				$helperSet = reset($helperSets);
413
				$helperSet->addSetup('set', [new Statement(EntityManagerHelper::class), 'em']);
414
			}
415
		}
416 1
	}
417
418 1
	private function processDbalTypes(string $name, array $types): void
419
	{
420 1
		$builder = $this->getContainerBuilder();
421 1
		$entityManagerDefinition = $builder->getDefinition($name . '.entityManager');
422
423 1
		foreach ($types as $type => $className) {
424 1
			$entityManagerDefinition->addSetup(
425 1
				'if ( ! Doctrine\DBAL\Types\Type::hasType(?)) { Doctrine\DBAL\Types\Type::addType(?, ?); }',
426 1
				[$type, $type, $className]
427
			);
428
		}
429 1
	}
430
431 1
	private function processDbalTypeOverrides(string $name, array $types): void
432
	{
433 1
		$builder = $this->getContainerBuilder();
434 1
		$entityManagerDefinition = $builder->getDefinition($name . '.entityManager');
435
436 1
		foreach ($types as $type => $className) {
437 1
			$entityManagerDefinition->addSetup('Doctrine\DBAL\Types\Type::overrideType(?, ?);', [$type, $className]);
438
		}
439 1
	}
440
441 1
	private function processEventSubscribers(string $name): void
442
	{
443 1
		$builder = $this->getContainerBuilder();
444
445 1
		if ($this->hasEventManager($builder)) {
446
			$eventManagerDefinition = $builder->getDefinition($builder->getByType(EventManager::class))
447
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
448
		} else {
449 1
			$eventManagerDefinition = $builder->addDefinition($name . '.eventManager')
450 1
				->setType(EventManager::class)
451 1
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
452
		}
453
454 1
		foreach (array_keys($builder->findByType(EventSubscriber::class)) as $serviceName) {
455 1
			$eventManagerDefinition->addSetup('addEventSubscriber', ['@' . $serviceName]);
456
		}
457 1
	}
458
459 1
	private function processFilters(): void
460
	{
461 1
		$builder = $this->getContainerBuilder();
462
463 1
		$configurationService = $builder->getDefinitionByType(Configuration::class);
464 1
		foreach ($builder->findByType(SQLFilter::class) as $name => $filterDefinition) {
465
			$configurationService->addSetup('addFilter', [$name, $filterDefinition->getType()]);
466
		}
467 1
	}
468
469 1
	private function hasSymfonyConsole(): bool
470
	{
471 1
		return class_exists(Application::class);
472
	}
473
474 1
	private function hasEventManager(ContainerBuilder $containerBuilder): bool
475
	{
476 1
		$eventManagerServiceName = $containerBuilder->getByType(EventManager::class);
477 1
		return $eventManagerServiceName !== null && strlen($eventManagerServiceName) > 0;
478
	}
479
}
480