Completed
Push — master ( 1b8418...be1cb1 )
by Tomáš
05:38
created

src/Adapter/Nette/DI/DoctrineExtension.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Console\Adapter\Nette\DI\ConsoleExtension;
48
use Portiny\Doctrine\Adapter\Nette\Tracy\DoctrineSQLPanel;
49
use Portiny\Doctrine\Cache\DefaultCache;
50
use Portiny\Doctrine\Contract\Provider\ClassMappingProviderInterface;
51
use Portiny\Doctrine\Contract\Provider\EntitySourceProviderInterface;
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')
0 ignored issues
show
Deprecated Code introduced by
The method Nette\DI\ServiceDefinition::setClass() has been deprecated with message: Use setType() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
127
				->setClass($config['repositoryFactory']);
128
			$configurationDefinition->addSetup('setRepositoryFactory', ['@' . $name . '.repositoryFactory']);
129
		}
130 1
		if ($config['sqlLogger']) {
131
			$builder->addDefinition($name . '.sqlLogger')
0 ignored issues
show
Deprecated Code introduced by
The method Nette\DI\ServiceDefinition::setClass() has been deprecated with message: Use setType() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
132
				->setClass($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 Portiny/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($name);
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->hasPortinyConsole()) {
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 ($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...
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(string $name): void
0 ignored issues
show
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...
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 hasPortinyConsole(): bool
453
	{
454 1
		return class_exists(ConsoleExtension::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