Completed
Push — master ( ac2e46...b1f3f0 )
by Tomáš
01:19
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
		],
106
	];
107
108
	/**
109
	 * {@inheritdoc}
110
	 */
111 1
	public function loadConfiguration(): void
112
	{
113 1
		$config = $this->parseConfig();
114
115 1
		$builder = $this->getContainerBuilder();
116 1
		$name = $config['prefix'];
117
118 1
		$configurationDefinition = $builder->addDefinition($name . '.config')
119 1
			->setType(Configuration::class)
120 1
			->addSetup('setFilterSchemaAssetsExpression', [$config['dbal']['schema_filter']])
121 1
			->addSetup('setDefaultRepositoryClassName', [$config['defaultRepositoryClassName']]);
122
123 1
		if ($config['repositoryFactory']) {
124
			$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...
125
				->setClass($config['repositoryFactory']);
126
			$configurationDefinition->addSetup('setRepositoryFactory', ['@' . $name . '.repositoryFactory']);
127
		}
128 1
		if ($config['sqlLogger']) {
129
			$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...
130
				->setClass($config['sqlLogger']);
131
			$configurationDefinition->addSetup('setSQLLogger', ['@' . $name . '.sqlLogger']);
132
		}
133
134 1
		if ($config['metadataCache'] !== FALSE) {
135 1
			$configurationDefinition->addSetup(
136 1
				'setMetadataCacheImpl',
137 1
				[$this->getCache($name . '.metadata', $builder, $config['metadataCache'])]
138
			);
139
		}
140
141 1
		if ($config['queryCache'] !== FALSE) {
142 1
			$configurationDefinition->addSetup(
143 1
				'setQueryCacheImpl',
144 1
				[$this->getCache($name . '.query', $builder, $config['queryCache'])]
145
			);
146
		}
147
148 1
		if ($config['resultCache'] !== FALSE) {
149 1
			$configurationDefinition->addSetup(
150 1
				'setResultCacheImpl',
151 1
				[$this->getCache($name . '.ormResult', $builder, $config['resultCache'])]
152
			);
153
		}
154
155 1
		if ($config['hydrationCache'] !== FALSE) {
156 1
			$configurationDefinition->addSetup(
157 1
				'setHydrationCacheImpl',
158 1
				[$this->getCache($name . '.hydration', $builder, $config['hydrationCache'])]
159
			);
160
		}
161
162 1
		$this->processSecondLevelCache($name, $config['secondLevelCache']);
163
164 1
		$builder->addDefinition($name . '.connection')
165 1
			->setType(Connection::class)
166 1
			->setFactory('@' . $name . '.entityManager::getConnection');
167
168 1
		$builder->addDefinition($name . '.entityManager')
169 1
			->setType($config['entityManagerClassName'])
170 1
			->setFactory(
171 1
				$config['entityManagerClassName'] . '::create',
172 1
				[$config['connection'], '@' . $name . '.config', '@Doctrine\Common\EventManager']
173
			);
174
175 1
		$builder->addDefinition($name . '.namingStrategy')
176 1
			->setType($config['namingStrategy']);
177
178 1
		$builder->addDefinition($name . '.resolver')
179 1
			->setType(ResolveTargetEntityListener::class);
180
181 1
		if ($this->hasIBarPanelInterface()) {
182 1
			$builder->addDefinition($this->prefix($name . '.diagnosticsPanel'))
183 1
				->setType(self::DOCTRINE_SQL_PANEL);
184
		}
185
186
		// import Doctrine commands into Portiny/Console if exists
187 1
		$this->registerCommandsIntoConsole($builder, $name);
188 1
	}
189
190
	/**
191
	 * {@inheritdoc}
192
	 */
193 1
	public function beforeCompile(): void
194
	{
195 1
		$config = $this->getConfig(self::$defaults);
196 1
		$name = $config['prefix'];
197
198 1
		$builder = $this->getContainerBuilder();
199
200 1
		$configDefinition = $builder->getDefinition($name . '.config')
201 1
			->setFactory(
202 1
				'\Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration',
203 1
				[array_values($this->entitySources), $config['debug'], $config['proxyDir'], NULL, FALSE]
204
			)
205 1
			->addSetup('setNamingStrategy', ['@' . $name . '.namingStrategy']);
206
207 1
		foreach ($config['functions'] as $functionName => $function) {
208 1
			$configDefinition->addSetup('addCustomStringFunction', [$functionName, $function]);
209
		}
210
211 1
		foreach ($this->classMappings as $source => $target) {
212
			$builder->getDefinition($name . '.resolver')
213
				->addSetup('addResolveTargetEntity', [$source, $target, []]);
214
		}
215
216 1
		$this->processDbalTypes($name, $config['dbal']['types']);
217 1
		$this->processDbalTypeOverrides($name, $config['dbal']['type_overrides']);
218 1
		$this->processEventSubscribers($name);
219 1
		$this->processFilters($name);
220 1
	}
221
222
	/**
223
	 * {@inheritdoc}
224
	 */
225 1
	public function afterCompile(ClassType $classType): void
226
	{
227 1
		$initialize = $classType->methods['initialize'];
228 1
		if ($this->hasIBarPanelInterface()) {
229 1
			$initialize->addBody('$this->getByType(\'' . self::DOCTRINE_SQL_PANEL . '\')->bindToBar();');
230
		}
231
232 1
		$initialize->addBody(
233 1
			'$filterCollection = $this->getByType(\'' . EntityManagerInterface::class . '\')->getFilters();'
234
		);
235 1
		$builder = $this->getContainerBuilder();
236 1
		$filterDefinitions = $builder->findByType(SQLFilter::class);
237 1
		foreach (array_keys($filterDefinitions) as $name) {
238
			$initialize->addBody('$filterCollection->enable(\'' . $name . '\');');
239
		}
240 1
	}
241
242 1
	protected function processSecondLevelCache($name, array $config): void
243
	{
244 1
		if (! $config['enabled']) {
245 1
			return;
246
		}
247
248
		$builder = $this->getContainerBuilder();
249
250
		$cacheService = $this->getCache($name . '.secondLevel', $builder, $config['driver']);
251
252
		$builder->addDefinition($this->prefix($name . '.cacheFactory'))
253
			->setType(CacheFactory::class)
254
			->setFactory($config['factoryClass'], [
255
				$this->prefix('@' . $name . '.cacheRegionsConfiguration'),
256
				$cacheService,
257
			])
258
			->addSetup('setFileLockRegionDirectory', [$config['fileLockRegionDirectory']]);
259
260
		$builder->addDefinition($this->prefix($name . '.cacheRegionsConfiguration'))
261
			->setFactory(RegionsConfiguration::class, [
262
				$config['regions']['defaultLifetime'],
263
				$config['regions']['defaultLockLifetime'],
264
			]);
265
266
		$logger = $builder->addDefinition($this->prefix($name . '.cacheLogger'))
267
			->setType(CacheLogger::class)
268
			->setFactory(CacheLoggerChain::class)
269
			->setAutowired(FALSE);
270
271
		if ($config['logging']) {
272
			$logger->addSetup('setLogger', ['statistics', new Statement(StatisticsCacheLogger::class)]);
273
		}
274
275
		$cacheConfigName = $this->prefix($name . '.ormCacheConfiguration');
276
		$builder->addDefinition($cacheConfigName)
277
			->setType(CacheConfiguration::class)
278
			->addSetup('setCacheFactory', [$this->prefix('@' . $name . '.cacheFactory')])
279
			->addSetup('setCacheLogger', [$this->prefix('@' . $name . '.cacheLogger')])
280
			->setAutowired(FALSE);
281
282
		$configuration = $builder->getDefinitionByType(Configuration::class);
283
		$configuration->addSetup('setSecondLevelCacheEnabled');
284
		$configuration->addSetup('setSecondLevelCacheConfiguration', ['@' . $cacheConfigName]);
285
	}
286
287
	/**
288
	 * @throws AssertionException
289
	 */
290 1
	private function parseConfig(): array
291
	{
292 1
		$config = $this->getConfig(self::$defaults);
293 1
		$this->classMappings = $config['targetEntityMappings'];
294 1
		$this->entitySources = $config['metadata'];
295
296 1
		foreach ($this->compiler->getExtensions() as $extension) {
297 1
			if ($extension instanceof ClassMappingProviderInterface) {
298
				$entityMapping = $extension->getClassMapping();
299
				Validators::assert($entityMapping, 'array');
300
				$this->classMappings = array_merge($this->classMappings, $entityMapping);
301
			}
302
303 1
			if ($extension instanceof EntitySourceProviderInterface) {
304
				$entitySource = $extension->getEntitySource();
305
				Validators::assert($entitySource, 'array');
306 1
				$this->entitySources = array_merge($this->entitySources, $entitySource);
307
			}
308
		}
309
310 1
		if ($config['sourceDir']) {
311
			$this->entitySources[] = $config['sourceDir'];
312
		}
313
314 1
		return $config;
315
	}
316
317 1
	private function getCache(string $prefix, ContainerBuilder $containerBuilder, string $cacheType): string
318
	{
319 1
		$cacheServiceName = $containerBuilder->getByType(Cache::class);
320 1
		if ($cacheServiceName !== NULL && strlen($cacheServiceName) > 0) {
321 1
			return '@' . $cacheServiceName;
322
		}
323
324 1
		$cacheClass = ArrayCache::class;
325 1
		if ($cacheType) {
326
			switch ($cacheType) {
327 1
				case 'redis':
328
					$cacheClass = RedisCache::class;
329
					break;
330
331 1
				case 'default':
332
				default:
333 1
					$cacheClass = DefaultCache::class;
334 1
					break;
335
			}
336
		}
337
338 1
		$cacheDefinition = $containerBuilder->addDefinition($prefix . '.cache')
339 1
			->setType($cacheClass);
340
341 1
		if ($cacheType === 'redis') {
342
			$config = $this->parseConfig();
343
			$redisConfig = $config['cache']['redis'];
344
345
			$containerBuilder->addDefinition($prefix . '.redis')
346
				->setType('\Redis')
347
				->setAutowired(FALSE)
348
				->addSetup('connect', [
349
					$redisConfig['host'] ?? '127.0.0.1',
350
					$redisConfig['port'] ?? null,
351
					$redisConfig['timeout'] ?? 0.0,
352
					$redisConfig['reserved'] ?? null,
353
					$redisConfig['retryInterval'] ?? 0,
354
				])
355
				->addSetup('select', [$redisConfig['database'] ?? 1]);
356
357
			$cacheDefinition->addSetup('setRedis', ['@' . $prefix . '.redis']);
358
		}
359
360 1
		return '@' . $prefix . '.cache';
361
	}
362
363 1
	private function hasIBarPanelInterface(): bool
364
	{
365 1
		return interface_exists(IBarPanel::class);
366
	}
367
368 1
	private function registerCommandsIntoConsole(ContainerBuilder $containerBuilder, string $name): void
369
	{
370 1
		if ($this->hasPortinyConsole()) {
371
			$commands = [
372
				ConvertMappingCommand::class,
373
				CreateCommand::class,
374
				DropCommand::class,
375
				GenerateEntitiesCommand::class,
376
				GenerateProxiesCommand::class,
377
				ImportCommand::class,
378
				MetadataCommand::class,
379
				QueryCommand::class,
380
				ResultCommand::class,
381
				UpdateCommand::class,
382
				ValidateSchemaCommand::class,
383
			];
384
			foreach ($commands as $index => $command) {
385
				$containerBuilder->addDefinition($name . '.command.' . $index)
386
					->setType($command);
387
			}
388
389
			$helperSets = $containerBuilder->findByType(HelperSet::class);
390
			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...
391
				/** @var ServiceDefinition $helperSet */
392
				$helperSet = reset($helperSets);
393
				$helperSet->addSetup('set', [new Statement(EntityManagerHelper::class), 'em']);
394
			}
395
		}
396 1
	}
397
398 1
	private function processDbalTypes(string $name, array $types): void
399
	{
400 1
		$builder = $this->getContainerBuilder();
401 1
		$entityManagerDefinition = $builder->getDefinition($name . '.entityManager');
402
403 1
		foreach ($types as $type => $className) {
404 1
			$entityManagerDefinition->addSetup(
405 1
				'if ( ! Doctrine\DBAL\Types\Type::hasType(?)) { Doctrine\DBAL\Types\Type::addType(?, ?); }',
406 1
				[$type, $type, $className]
407
			);
408
		}
409 1
	}
410
411 1
	private function processDbalTypeOverrides(string $name, array $types): void
412
	{
413 1
		$builder = $this->getContainerBuilder();
414 1
		$entityManagerDefinition = $builder->getDefinition($name . '.entityManager');
415
416 1
		foreach ($types as $type => $className) {
417 1
			$entityManagerDefinition->addSetup('Doctrine\DBAL\Types\Type::overrideType(?, ?);', [$type, $className]);
418
		}
419 1
	}
420
421 1
	private function processEventSubscribers(string $name): void
422
	{
423 1
		$builder = $this->getContainerBuilder();
424
425 1
		if ($this->hasEventManager($builder)) {
426
			$eventManagerDefinition = $builder->getDefinition($builder->getByType(EventManager::class))
427
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
428
		} else {
429 1
			$eventManagerDefinition = $builder->addDefinition($name . '.eventManager')
430 1
				->setType(EventManager::class)
431 1
				->addSetup('addEventListener', [Events::loadClassMetadata, '@' . $name . '.resolver']);
432
		}
433
434 1
		foreach (array_keys($builder->findByType(EventSubscriber::class)) as $serviceName) {
435 1
			$eventManagerDefinition->addSetup('addEventSubscriber', ['@' . $serviceName]);
436
		}
437 1
	}
438
439 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...
440
	{
441 1
		$builder = $this->getContainerBuilder();
442
443 1
		$configurationService = $builder->getDefinitionByType(Configuration::class);
444 1
		foreach ($builder->findByType(SQLFilter::class) as $name => $filterDefinition) {
445
			$configurationService->addSetup('addFilter', [$name, $filterDefinition->getType()]);
446
		}
447 1
	}
448
449 1
	private function hasPortinyConsole(): bool
450
	{
451 1
		return class_exists(ConsoleExtension::class);
452
	}
453
454 1
	private function hasEventManager(ContainerBuilder $containerBuilder): bool
455
	{
456 1
		$eventManagerServiceName = $containerBuilder->getByType(EventManager::class);
457 1
		return $eventManagerServiceName !== NULL && strlen($eventManagerServiceName) > 0;
458
	}
459
}
460