Completed
Push — master ( 4e8223...162bbd )
by Tomáš
07:39
created

DoctrineExtension::hasSymfonyConsole()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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