Passed
Push — master ( b562f8...22d897 )
by Tomáš
03:37
created

DoctrineExtension::getConfigSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 49
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 1

Importance

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