Completed
Push — master ( e2ca9d...bf4fc0 )
by Tomáš
03:57
created

DoctrineExtension::beforeCompile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 4.0105

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 21
cts 23
cp 0.913
rs 9.36
c 0
b 0
f 0
cc 4
nc 4
nop 0
crap 4.0105
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\EventManager;
10
use Doctrine\Common\EventSubscriber;
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\Tools\Console\Command\ImportCommand;
13
use Doctrine\ORM\Cache\CacheConfiguration;
14
use Doctrine\ORM\Cache\CacheFactory;
15
use Doctrine\ORM\Cache\DefaultCacheFactory;
16
use Doctrine\ORM\Cache\Logging\CacheLogger;
17
use Doctrine\ORM\Cache\Logging\CacheLoggerChain;
18
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
19
use Doctrine\ORM\Cache\RegionsConfiguration;
20
use Doctrine\ORM\Configuration;
21
use Doctrine\ORM\EntityManager;
22
use Doctrine\ORM\EntityManagerInterface;
23
use Doctrine\ORM\EntityRepository;
24
use Doctrine\ORM\Events;
25
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
26
use Doctrine\ORM\Query\Filter\SQLFilter;
27
use Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand;
28
use Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand;
29
use Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand;
30
use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand;
31
use Doctrine\ORM\Tools\Console\Command\GenerateEntitiesCommand;
32
use Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand;
33
use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand;
34
use Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand;
35
use Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand;
36
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
37
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
38
use Doctrine\ORM\Tools\ResolveTargetEntityListener;
39
use Nette\DI\CompilerExtension;
40
use Nette\DI\ContainerBuilder;
41
use Nette\DI\ServiceDefinition;
42
use Nette\DI\Statement;
43
use Nette\PhpGenerator\ClassType;
44
use Nette\Utils\AssertionException;
45
use Nette\Utils\Validators;
46
use Portiny\Console\Adapter\Nette\DI\ConsoleExtension;
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\Helper\HelperSet;
52
use Tracy\IBarPanel;
53
54
class DoctrineExtension extends CompilerExtension
55
{
56
	/**
57
	 * @var string
58
	 */
59
	private const DOCTRINE_SQL_PANEL = DoctrineSQLPanel::class;
60
61
	/**
62
	 * @var array
63
	 */
64
	private static $defaults = [
65
		'debug' => '%debugMode%',
66
		'dbal' => [
67
			'type_overrides' => [],
68
			'types' => [],
69
			'schema_filter' => NULL,
70
		],
71
		'prefix' => 'doctrine.default',
72
		'proxyDir' => '%tempDir%/cache/proxies',
73
		'sourceDir' => NULL,
74
		'entityManagerClassName' => EntityManager::class,
75
		'defaultRepositoryClassName' => EntityRepository::class,
76
		'repositoryFactory' => NULL,
77
		'namingStrategy' => UnderscoreNamingStrategy::class,
78
		'targetEntityMappings' => [],
79
		'metadata' => [],
80
		'functions' => [],
81
		// caches
82
		'metadataCache' => 'default',
83
		'queryCache' => 'default',
84
		'resultCache' => 'default',
85
		'hydrationCache' => 'default',
86
		'secondLevelCache' => [
87
			'enabled' => FALSE,
88
			'factoryClass' => DefaultCacheFactory::class,
89
			'driver' => 'default',
90
			'regions' => [
91
				'defaultLifetime' => 3600,
92
				'defaultLockLifetime' => 60,
93
			],
94
			'fileLockRegionDirectory' => '%tempDir%/cache/Doctrine.Cache.Locks',
95
			'logging' => '%debugMode%',
96
		],
97
	];
98
99
	private $entitySources = [];
100
101
	private $classMappings = [];
102
103
	/**
104
	 * {@inheritdoc}
105
	 */
106 1
	public function loadConfiguration(): void
107
	{
108 1
		$config = $this->parseConfig();
109
110 1
		$builder = $this->getContainerBuilder();
111 1
		$name = $config['prefix'];
112
113 1
		$configurationDefinition = $builder->addDefinition($name . '.config')
114 1
			->setType(Configuration::class)
115 1
			->addSetup('setFilterSchemaAssetsExpression', [$config['dbal']['schema_filter']])
116 1
			->addSetup('setDefaultRepositoryClassName', [$config['defaultRepositoryClassName']]);
117
118 1
		if ($config['repositoryFactory']) {
119
			$builder->addDefinition($name . '.repositoryFactory')
0 ignored issues
show
Deprecated Code introduced by
The method Nette\DI\ServiceDefinition::setClass() has been deprecated.

This method has been deprecated.

Loading history...
120
				->setClass($config['repositoryFactory']);
121
			$configurationDefinition->addSetup('setRepositoryFactory', ['@' . $name . '.repositoryFactory']);
122
		}
123
124 1
		if ($config['metadataCache'] !== FALSE) {
125 1
			$configurationDefinition->addSetup(
126 1
				'setMetadataCacheImpl',
127 1
				[$this->getCache($name . '.metadata', $builder, $config['metadataCache'])]
128
			);
129
		}
130
131 1
		if ($config['queryCache'] !== FALSE) {
132 1
			$configurationDefinition->addSetup(
133 1
				'setQueryCacheImpl',
134 1
				[$this->getCache($name . '.query', $builder, $config['queryCache'])]
135
			);
136
		}
137
138 1
		if ($config['resultCache'] !== FALSE) {
139 1
			$configurationDefinition->addSetup(
140 1
				'setResultCacheImpl',
141 1
				[$this->getCache($name . '.ormResult', $builder, $config['resultCache'])]
142
			);
143
		}
144
145 1
		if ($config['hydrationCache'] !== FALSE) {
146 1
			$configurationDefinition->addSetup(
147 1
				'setHydrationCacheImpl',
148 1
				[$this->getCache($name . '.hydration', $builder, $config['hydrationCache'])]
149
			);
150
		}
151
152 1
		$this->processSecondLevelCache($name, $config['secondLevelCache']);
153
154 1
		$builder->addDefinition($name . '.connection')
155 1
			->setType(Connection::class)
156 1
			->setFactory('@' . $name . '.entityManager::getConnection');
157
158 1
		$builder->addDefinition($name . '.entityManager')
159 1
			->setType($config['entityManagerClassName'])
160 1
			->setFactory(
161 1
				$config['entityManagerClassName'] . '::create',
162 1
				[$config['connection'], '@' . $name . '.config', '@Doctrine\Common\EventManager']
163
			);
164
165 1
		$builder->addDefinition($name . '.namingStrategy')
166 1
			->setType($config['namingStrategy']);
167
168 1
		$builder->addDefinition($name . '.resolver')
169 1
			->setType(ResolveTargetEntityListener::class);
170
171 1
		if ($this->hasIBarPanelInterface()) {
172 1
			$builder->addDefinition($this->prefix($name . '.diagnosticsPanel'))
173 1
				->setType(self::DOCTRINE_SQL_PANEL);
174
		}
175
176
		// import Doctrine commands into Portiny/Console if exists
177 1
		$this->registerCommandsIntoConsole($builder, $name);
178 1
	}
179
180
	/**
181
	 * {@inheritdoc}
182
	 */
183 1
	public function beforeCompile(): void
184
	{
185 1
		$config = $this->getConfig(self::$defaults);
186 1
		$name = $config['prefix'];
187
188 1
		$builder = $this->getContainerBuilder();
189 1
		$cache = $this->getCache($name, $builder, 'default');
190
191 1
		$configDefinition = $builder->getDefinition($name . '.config')
192 1
			->setFactory(
193 1
				'\Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration',
194
				[
195 1
					array_values($this->entitySources),
196 1
					$config['debug'],
197 1
					$config['proxyDir'],
198 1
					$config['metadataCache'] !== FALSE ? $cache : NULL,
199
					FALSE,
200
				]
201
			)
202 1
			->addSetup('setNamingStrategy', ['@' . $name . '.namingStrategy']);
203
204 1
		foreach ($config['functions'] as $functionName => $function) {
205 1
			$configDefinition->addSetup('addCustomStringFunction', [$functionName, $function]);
206
		}
207
208 1
		foreach ($this->classMappings as $source => $target) {
209
			$builder->getDefinition($name . '.resolver')
210
				->addSetup('addResolveTargetEntity', [$source, $target, []]);
211
		}
212
213 1
		$this->processDbalTypes($name, $config['dbal']['types']);
214 1
		$this->processDbalTypeOverrides($name, $config['dbal']['type_overrides']);
215 1
		$this->processEventSubscribers($name);
216 1
		$this->processFilters($name);
217 1
	}
218
219
	/**
220
	 * {@inheritdoc}
221
	 */
222 1
	public function afterCompile(ClassType $classType): void
223
	{
224 1
		$initialize = $classType->methods['initialize'];
225 1
		if ($this->hasIBarPanelInterface()) {
226 1
			$initialize->addBody('$this->getByType(\'' . self::DOCTRINE_SQL_PANEL . '\')->bindToBar();');
227
		}
228
229 1
		$initialize->addBody(
230 1
			'$filterCollection = $this->getByType(\'' . EntityManagerInterface::class . '\')->getFilters();'
231
		);
232 1
		$builder = $this->getContainerBuilder();
233 1
		foreach ($builder->findByType(SQLFilter::class) as $name => $filterDefinition) {
234
			$initialize->addBody('$filterCollection->enable(\'' . $name . '\');');
235
		}
236 1
	}
237
238 1
	protected function processSecondLevelCache($name, array $config): void
239
	{
240 1
		if (! $config['enabled']) {
241 1
			return;
242
		}
243
244
		$builder = $this->getContainerBuilder();
245
246
		$cacheService = $this->getCache($name . '.secondLevel', $builder, $config['driver']);
247
248
		$builder->addDefinition($this->prefix($name . '.cacheFactory'))
249
			->setType(CacheFactory::class)
250
			->setFactory($config['factoryClass'], [
251
				$this->prefix('@' . $name . '.cacheRegionsConfiguration'),
252
				$cacheService,
253
			])
254
			->addSetup('setFileLockRegionDirectory', [$config['fileLockRegionDirectory']]);
255
256
		$builder->addDefinition($this->prefix($name . '.cacheRegionsConfiguration'))
257
			->setFactory(RegionsConfiguration::class, [
258
				$config['regions']['defaultLifetime'],
259
				$config['regions']['defaultLockLifetime'],
260
			]);
261
262
		$logger = $builder->addDefinition($this->prefix($name . '.cacheLogger'))
263
			->setType(CacheLogger::class)
264
			->setFactory(CacheLoggerChain::class)
265
			->setAutowired(FALSE);
266
267
		if ($config['logging']) {
268
			$logger->addSetup('setLogger', ['statistics', new Statement(StatisticsCacheLogger::class)]);
269
		}
270
271
		$cacheConfigName = $this->prefix($name . '.ormCacheConfiguration');
272
		$builder->addDefinition($cacheConfigName)
273
			->setType(CacheConfiguration::class)
274
			->addSetup('setCacheFactory', [$this->prefix('@' . $name . '.cacheFactory')])
275
			->addSetup('setCacheLogger', [$this->prefix('@' . $name . '.cacheLogger')])
276
			->setAutowired(FALSE);
277
278
		$configuration = $builder->getDefinitionByType(Configuration::class);
279
		$configuration->addSetup('setSecondLevelCacheEnabled');
280
		$configuration->addSetup('setSecondLevelCacheConfiguration', ['@' . $cacheConfigName]);
281
	}
282
283 1
	private function getCache(string $prefix, ContainerBuilder $containerBuilder, string $cacheType): string
284
	{
285 1
		$cacheServiceName = $containerBuilder->getByType(Cache::class);
286 1
		if ($cacheServiceName !== NULL && strlen($cacheServiceName) > 0) {
287 1
			return '@' . $cacheServiceName;
288
		}
289
290 1
		$cacheClass = ArrayCache::class;
291 1
		if ($cacheType) {
292
			switch ($cacheType) {
293 1
				case 'default':
294
				default:
295 1
					$cacheClass = DefaultCache::class;
296 1
					break;
297
			}
298
		}
299
300 1
		$containerBuilder->addDefinition($prefix . '.cache')
301 1
			->setType($cacheClass);
302
303 1
		return '@' . $prefix . '.cache';
304
	}
305
306
	/**
307
	 * @throws AssertionException
308
	 */
309 1
	private function parseConfig(): array
310
	{
311 1
		$config = $this->getConfig(self::$defaults);
312 1
		$this->classMappings = $config['targetEntityMappings'];
313 1
		$this->entitySources = $config['metadata'];
314
315 1
		foreach ($this->compiler->getExtensions() as $extension) {
316 1
			if ($extension instanceof ClassMappingProviderInterface) {
317
				$entityMapping = $extension->getClassMapping();
318
				Validators::assert($entityMapping, 'array');
319
				$this->classMappings = array_merge($this->classMappings, $entityMapping);
320
			}
321
322 1
			if ($extension instanceof EntitySourceProviderInterface) {
323
				$entitySource = $extension->getEntitySource();
324
				Validators::assert($entitySource, 'array');
325 1
				$this->entitySources = array_merge($this->entitySources, $entitySource);
326
			}
327
		}
328
329 1
		if ($config['sourceDir']) {
330
			$this->entitySources[] = $config['sourceDir'];
331
		}
332
333 1
		return $config;
334
	}
335
336 1
	private function hasIBarPanelInterface(): bool
337
	{
338 1
		return interface_exists(IBarPanel::class);
339
	}
340
341 1
	private function hasPortinyConsole(): bool
342
	{
343 1
		return class_exists(ConsoleExtension::class);
344
	}
345
346 1
	private function hasEventManager(ContainerBuilder $containerBuilder): bool
347
	{
348 1
		$eventManagerServiceName = $containerBuilder->getByType(EventManager::class);
349 1
		return $eventManagerServiceName !== NULL && strlen($eventManagerServiceName) > 0;
350
	}
351
352 1
	private function processDbalTypes(string $name, array $types): void
353
	{
354 1
		$builder = $this->getContainerBuilder();
355 1
		$entityManagerDefinition = $builder->getDefinition($name . '.entityManager');
356
357 1
		foreach ($types as $type => $className) {
358 1
			$entityManagerDefinition->addSetup(
359 1
				'if ( ! Doctrine\DBAL\Types\Type::hasType(?)) { Doctrine\DBAL\Types\Type::addType(?, ?); }',
360 1
				[$type, $type, $className]
361
			);
362
		}
363 1
	}
364
365 1
	private function processDbalTypeOverrides(string $name, array $types): void
366
	{
367 1
		$builder = $this->getContainerBuilder();
368 1
		$entityManagerDefinition = $builder->getDefinition($name . '.entityManager');
369
370 1
		foreach ($types as $type => $className) {
371 1
			$entityManagerDefinition->addSetup('Doctrine\DBAL\Types\Type::overrideType(?, ?);', [$type, $className]);
372
		}
373 1
	}
374
375 1
	private function processEventSubscribers(string $name): void
376
	{
377 1
		$builder = $this->getContainerBuilder();
378
379 1
		if ($this->hasEventManager($builder)) {
380
			$eventManagerDefinition = $builder->getDefinition($builder->getByType(EventManager::class))
381
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
382
		} else {
383 1
			$eventManagerDefinition = $builder->addDefinition($name . '.eventManager')
384 1
				->setType(EventManager::class)
385 1
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
386
		}
387
388 1
		foreach (array_keys($builder->findByType(EventSubscriber::class)) as $serviceName) {
389 1
			$eventManagerDefinition->addSetup('addEventSubscriber', ['@' . $serviceName]);
390
		}
391 1
	}
392
393 1
	private function processFilters(string $name): void
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
394
	{
395 1
		$builder = $this->getContainerBuilder();
396
397 1
		$configurationService = $builder->getDefinitionByType(Configuration::class);
398 1
		foreach ($builder->findByType(SQLFilter::class) as $name => $filterDefinition) {
399
			$configurationService->addSetup('addFilter', [$name, $filterDefinition->getType()]);
400
		}
401 1
	}
402
403 1
	private function registerCommandsIntoConsole(ContainerBuilder $containerBuilder, string $name): void
404
	{
405 1
		if ($this->hasPortinyConsole()) {
406
			$commands = [
407
				ConvertMappingCommand::class,
408
				CreateCommand::class,
409
				DropCommand::class,
410
				GenerateEntitiesCommand::class,
411
				GenerateProxiesCommand::class,
412
				ImportCommand::class,
413
				MetadataCommand::class,
414
				QueryCommand::class,
415
				ResultCommand::class,
416
				UpdateCommand::class,
417
				ValidateSchemaCommand::class,
418
			];
419
			foreach ($commands as $index => $command) {
420
				$containerBuilder->addDefinition($name . '.command.' . $index)
421
					->setType($command);
422
			}
423
424
			$helperSets = $containerBuilder->findByType(HelperSet::class);
425
			if ($helperSets) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $helperSets of type Nette\DI\ServiceDefinition[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
426
				/** @var ServiceDefinition $helperSet */
427
				$helperSet = reset($helperSets);
428
				$helperSet->addSetup('set', [new Statement(EntityManagerHelper::class), 'em']);
429
			}
430
		}
431 1
	}
432
}
433