Completed
Push — master ( c12550...96f7d2 )
by Tomáš
03:22
created

DoctrineExtension::parseConfig()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.3183

Importance

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

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
489
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
490
		} else {
491 1
			$eventManagerDefinition = $builder->addDefinition($name . '.eventManager')
492 1
				->setType(EventManager::class)
493 1
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
494
		}
495
496 1
		foreach (array_keys($builder->findByType(EventSubscriber::class)) as $serviceName) {
497 1
			$eventManagerDefinition->addSetup('addEventSubscriber', ['@' . $serviceName]);
498
		}
499 1
	}
500
501
502 1
	private function processFilters(): void
503
	{
504 1
		$builder = $this->getContainerBuilder();
505
506 1
		$configurationService = $builder->getDefinitionByType(Configuration::class);
507 1
		foreach ($builder->findByType(SQLFilter::class) as $name => $filterDefinition) {
508
			$configurationService->addSetup('addFilter', [$name, $filterDefinition->getType()]);
509
		}
510 1
	}
511
512
513 1
	private function hasSymfonyConsole(): bool
514
	{
515 1
		return class_exists(Application::class);
516
	}
517
518
519 1
	private function hasEventManager(ContainerBuilder $containerBuilder): bool
520
	{
521 1
		$eventManagerServiceName = $containerBuilder->getByType(EventManager::class);
522 1
		return $eventManagerServiceName !== null && strlen($eventManagerServiceName) > 0;
523
	}
524
525
}
526